Skip to content

perf(api): speed up the first GraphQL request on a cold Lambda - #5899

Merged
adrians5j merged 1 commit into
nextfrom
claude/perf-schema-build
Oct 5, 2026
Merged

adrians5j merged 1 commit into
nextfrom
claude/perf-schema-build

Conversation

@adrians5j

@adrians5j adrians5j commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

What changed

  • GraphQLEngine passes updateResolversInPlace: true to makeExecutableSchema. Without it, addResolversToSchema runs mapSchema, which builds a full copy of the schema just to attach the resolvers. On a cold Lambda that copy took about 164 ms of the 323 ms schema build. The schema built from typeDefs is brand new and nothing else holds a reference to it, so attaching the resolvers in place is safe. graphql-tools rebuilds enum types and runs healSchema afterwards, as designed.
  • Asset delivery no longer calls new S3(...) in register(). That runs in the per-request container, so every GraphQL request built an S3 client, even ones that never touch files. It now uses registerFactory(S3Client, () => createS3()): the client is created when something resolves it and cached per process.

What updateResolversInPlace does

makeExecutableSchema works in two steps:

  1. Build the schema from the SDL text. This produces types and fields, but no field has a resolver yet.
  2. Attach the resolvers (addResolversToSchema).

By default, step 2 copies. graphql-tools treats schemas as immutable, so mapSchema walks every type and field and builds a new GraphQLObjectType for each one, with the resolvers set on the new fields, then a new GraphQLSchema around them. The step 1 schema is thrown away, so the schema is effectively built twice.

With updateResolversInPlace: true, step 2 sets the resolvers directly on the existing field objects (field.resolve = resolverFn). It also swaps in the custom scalar functions, rebuilds the enum types and repairs references to them with healSchema. Nothing else is rebuilt.

The copy only protects a schema that other code also holds, where changing it in place would change it for them. Here the step 1 schema is created inside the same makeExecutableSchema call from our own SDL, and nothing else ever sees it.

Results

Measured on a 1024 MB Lambda with a { __typename } request: 8 forced cold starts and 25 warm requests per run, original and fixed builds alternating twice. The function ran without --enable-source-maps, which is how production runs (see below).

Init First request Init + first Warm p50 Warm p90
Before 1139 / 1189 ms 877 / 906 ms 2032 / 2089 ms 46 / 39 ms 112 / 104 ms
After 1165 / 1186 ms 767 / 764 ms 1936 / 1944 ms 44 / 35 ms 71 / 64 ms

Medians from each of the two runs. A cold start is about 120 ms faster, almost all of it in the first request. Warm p90 also dropped.

Testing

  • api-graphql: 50 of 50 pass.
  • api-core: 228 of 228 pass.
  • api-headless-cms: 975 pass and 16 are skipped, the same result as on next.
  • api-file-manager-s3: 9 of 9 pass.
  • Asset delivery has no tests, so I checked it on the deployed stack. The original file, a ?width=128 resize and a cache-busted request all returned 200 with a PNG, all CloudFront misses, and the function logged no errors.

Found while measuring, not changed here

  • --enable-source-maps costs about 250 ms of init. lambdaEnvVariables.ts only sets it when DEBUG=true at deploy time. That applies to dev deploys, not production. So comparisons with 6.4.11 that came from a DEBUG=true deploy overstated the gap. With the flag off, next init is about 1175 ms against 970 ms for 6.4.11.
  • AWS SDK clients: all 11 load on every cold start through static imports, about 330 ms of init. I tried loading the 8 that aren't DynamoDB lazily, by patching the bundle. Init dropped by about 225 ms, but most of that moved into the first request, so the whole cold start only improved by 50 to 90 ms. A real version would be a bundler change, which seems more risk than that's worth. Holding it for now.
  • Other heavy code that loads eagerly: cheerio and its bundled undici, which ai-powerups only needs to translate pages (about 53 ms of module evaluation on Lambda, about 600 KB). The AI SDK (ai, @ai-sdk/*, @vercel/oidc, @workflow/serde, about 40 ms). nodemailer and iconv-lite (about 320 KB). These are candidates for loading on first use.

Workspace: wby-next4 · ✨ Event Van Handler 🎸

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved asset delivery setup so its storage connection is created when needed.
    • Updated GraphQL schema handling to keep resolver changes in place, supporting more consistent schema behavior.

- GraphQLEngine attaches resolvers to the schema it just built instead of
  letting graphql-tools copy the whole schema to add them
  (updateResolversInPlace). That copy was about half of the schema build.
- Asset delivery no longer builds an S3 client in register(), which runs on
  every request. The client is now created when something resolves it, and
  reused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adrians5j adrians5j added the evh-cleanups event-handler DI/transport-agnostic cleanups label Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

🚓 Slop Cop

✅ Nothing worth flagging. The diff looks consistent with the PR's stated intent and the code-style rules.

Small, focused perf PR that matches its stated intent; no integrity or style issues found.

Automated, non-blocking heads-up from an LLM. It can be wrong — use your judgment. Regenerates on every push.

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: fe8b9403-b682-4e50-9968-63a4d8027c2f

📥 Commits

Reviewing files that changed from the base of the PR and between f0d3d3e and abf2907.

📒 Files selected for processing (2)
  • packages/api-file-manager-s3/src/assetDelivery/feature.ts
  • packages/api-graphql/src/engine/GraphQLEngine.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

The pull request changes S3 client registration to use a factory and configures static GraphQL schema creation to update resolvers in place.

Changes

S3 asset delivery

Layer / File(s) Summary
Register the S3 client factory
packages/api-file-manager-s3/src/assetDelivery/feature.ts
The feature registers a factory that calls createS3() when S3Client is resolved. The import changes from the S3 class to the createS3 factory.

GraphQL schema

Layer / File(s) Summary
Update static schema resolvers in place
packages/api-graphql/src/engine/GraphQLEngine.ts
buildStaticSchema sets updateResolversInPlace: true when it creates the executable schema.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Refactor

Suggested reviewers: pavel910

Merge Risk: ⚪ Minimal · up to abf29

No actionable issue is established for asset delivery or GraphQL schema creation; the change is mergeable after normal checks.

Architecture Summary

Architecture risk: 🔵 Low · up to abf29

The change affects 2 systems.

Changed systems: packages/api-file-manager-s3, packages/api-graphql

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — packages/api-file-manager-s3 (library) was modified; 1 changed file maps to changed impact.
  • observed — packages/api-graphql (library) was modified; 1 changed file maps to changed impact.

Before / after behavior

  • observed — Modified behavior in packages/api-file-manager-s3/src/assetDelivery/feature.ts: The imported S3 client API changes from the S3 class to the createS3 factory.
  • observed — Modified behavior in packages/api-file-manager-s3/src/assetDelivery/feature.ts: S3Client changes from an eagerly created S3 instance configured with AWS_REGION to a registered factory that calls createS3() when the client is resolved.
  • observed — Modified behavior in packages/api-graphql/src/engine/GraphQLEngine.ts: buildStaticSchema now updates resolvers directly on the newly created schema instead of allowing graphql-tools to rebuild the schema to attach them; the comments note the schema is newly built and unshared.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the performance goal: speeding up the first GraphQL request on a cold Lambda. This matches the schema construction and deferred S3 client creation changes.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@adrians5j
adrians5j merged commit 8f9156f into next Oct 5, 2026
20 checks passed
adrians5j added a commit that referenced this pull request Oct 5, 2026
…oles-ui

Brings in 21 commits from next, among them the encryption key cache
(#5900), the cold-Lambda GraphQL speed-up (#5899), the audit logs
use-case split (#5877, #5882) and access check (#5886), scheduler
access gated on each app's permissions (#5907) and the release/6.5.0
entry form fixes (#5865). No conflicts.

api-event-handler-standalone/src/createWebinyApiHandler.ts changed on
both sides and merged on its own. It keeps the
NodeHttpAssumePermissionsDecorator registration and next's new root
EncryptionKeyCacheFeature, which are unrelated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

evh-cleanups event-handler DI/transport-agnostic cleanups

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant