twenty/packages
Charles Bochet 70a73534c0
perf(server): reuse ESM module cache across warm Lambda invocations of logic functions (#19830)
## Summary

Lambda warm-invocations of logic functions were spending **~440 ms**
re-parsing and re-evaluating the user bundle on every call. The executor
wrote the user code to a **randomly-named** temp file and `import()`-ed
it, so each warm call resolved to a new URL and Node's ESM cache could
never reuse the previous module record.

This PR makes the executor write to a **content-hash filename**, skip
the write when the file already exists, and stop deleting it. Identical
code now reuses the same module record across warm calls in the same
container, dropping warm-invocation overhead by **~30–40%**.

## What changed

- `executor/index.mjs`: temp filename derived from `sha256(code)`, write
skipped when file exists, no `fs.rm` on cleanup.
- `lambda.driver.ts`: single structured `[lambda-timing]` log per
invocation with `totalMs / buildExecutorMs / getBuiltCodeMs /
payloadBytes / invokeSendMs / reportDurationMs / billedMs /
initDurationMs / coldStart`. Goes through the standard NestJS `Logger`.

No behavioural change for callers: same input → same output, same error
semantics.

### Caveat: module-scope state now persists across warm calls

With a stable filename, the user bundle is evaluated **once per warm
container**. Any module-scoped state or top-level side-effects in user
code are now shared across invocations of the same container, instead of
re-running on every call. This is documented in the executor and is the
intended trade-off — module scope should be treated as a per-container
cache, not as per-call isolation.

## Findings — measured impact

Same logic function (`fetch-prs`, ~12k PRs to page through), same
workspace, same Lambda config (eu-west-3, 512 MB), token cache primed.

### Warm invocations

| Phase | Before fix | After fix | Δ |
| -------------------------------------- | ------------- |
-------------- | ------------ |
| Executor `import(userBundle)` | ~440 ms | **~0 ms** | **-440 ms** |
| Lambda billed duration | ~1.5–1.7 s | **~1.0–1.1 s** | **~30–40%** |
| Server-perceived round-trip | ~1.7–2.0 s | **~1.0–1.2 s** |
**~30–40%** |

### Cold starts

Unchanged — the cache helps subsequent warm calls in the same container,
not the first one. Init Duration stays ~130–170 ms; total cold call
~2.5–3.0 s.

### Stress

Could not reproduce the previously-reported \"every ~10th call times
out\" behaviour after the fix:

- 30 sequential calls: max 1.7 s, median ~1.1 s, 0 timeouts
- 50 concurrent calls: max 9.4 s (clear cold-start cluster), median ~1.5
s, 0 timeouts

Hypothesis: the warm-import overhead was eating into the headroom
against the function timeout under bursty load; removing it pushed
everything well below the limit.

## Observability

One structured log line per invocation, sent through the standard NestJS
logger:

\`\`\`
[lambda-timing] fnId=abc123 totalMs=1187 buildExecutorMs=2
getBuiltCodeMs=3 payloadBytes=1466321 invokeSendMs=1180
reportDurationMs=992 billedMs=1000 initDurationMs=n/a coldStart=false
\`\`\`

\`coldStart=true\` whenever Lambda spun up a fresh container; on warm
calls \`buildExecutorMs\` and \`getBuiltCodeMs\` collapse to
single-digit ms, confirming the cache fix is working.

## Test plan

- [ ] CI green.
- [ ] Deploy to a Lambda-backed env, trigger a logic function several
times in a row.
- [ ] Confirm \`[lambda-timing]\` warm invocations show \`totalMs\`
~30–40% lower than before, and \`coldStart=false\` after the first call
in a container.
- [ ] Push a new version of an app; confirm the next call shows higher
\`buildExecutorMs\` (new hash, new file written) followed by warm calls
again.
- [ ] Smoke test: errors thrown by the user handler are still surfaced
correctly.

Made with [Cursor](https://cursor.com)
2026-04-18 11:30:56 +02:00
..
create-twenty-app Release v1.22.0 for twenty-sdk, twenty-client-sdk, and create-twenty-app (#19751) 2026-04-16 08:29:50 +00:00
twenty-apps Twenty for twenty app (#19804) 2026-04-17 17:29:09 +00:00
twenty-cli 1774 extensibility v1 create an exhaustive documentation readme or dedicated section in twenty contributing doc (#16751) 2025-12-22 15:19:11 +01:00
twenty-client-sdk Disable reset to default when custom tab or widget (#19814) 2026-04-17 17:30:57 +00:00
twenty-companion Migrate twenty-companion from npm to yarn workspaces (#18946) 2026-03-25 10:45:43 +01:00
twenty-docker Add twenty-managed Docker target with AWS CLI for EKS deployments (#19816) 2026-04-17 17:54:10 +00:00
twenty-docs i18n - docs translations (#19710) 2026-04-14 22:35:59 +02:00
twenty-e2e-testing shouldIncludeRecordPageLayouts deprecation (#19774) 2026-04-17 11:32:10 +00:00
twenty-emails i18n - translations (#18956) 2026-03-25 14:23:30 +01:00
twenty-front i18n - translations (#19821) 2026-04-17 21:37:54 +02:00
twenty-front-component-renderer Add event forwarding stories to the front component renderer (#19721) 2026-04-16 08:55:49 +00:00
twenty-oxlint-rules add workspaceId to indirect entities (#19522) 2026-04-09 19:30:28 +00:00
twenty-sdk Surface structured validation errors during application install (#19787) 2026-04-17 11:29:33 +00:00
twenty-server perf(server): reuse ESM module cache across warm Lambda invocations of logic functions (#19830) 2026-04-18 11:30:56 +02:00
twenty-shared fix: replace slow deep-equal with fastDeepEqual to resolve CPU bottleneck (#19771) 2026-04-16 17:23:50 +02:00
twenty-ui Fix app design 1/2 (#19735) 2026-04-16 09:47:44 +00:00
twenty-utils Refactor dependency graph for SDK, client-sdk and create-app (#18963) 2026-03-26 10:56:52 +00:00
twenty-website Add AI as a public feature flag in the Lab (#19277) 2026-04-02 15:56:27 +00:00
twenty-website-new Add SVG export and refine halftone studio controls (#19813) 2026-04-17 16:53:38 +00:00
twenty-zapier Deprecate legacy RICH_TEXT field metadata type (#18623) 2026-03-13 17:25:40 +01:00