Skip to content

fix: avoid OOM when exporting many email templates#3518

Merged
felipefreitag merged 4 commits into
canaryfrom
fix-email-export-oom-large-templates
May 18, 2026
Merged

fix: avoid OOM when exporting many email templates#3518
felipefreitag merged 4 commits into
canaryfrom
fix-email-export-oom-large-templates

Conversation

@felipefreitag
Copy link
Copy Markdown
Contributor

@felipefreitag felipefreitag commented May 18, 2026

email export crashed with "JavaScript heap out of memory" once a project had a few hundred templates. Two distinct causes, in two distinct phases:

  1. Build phase: esbuild.build() was called once with every template as an entry point. With bundle: true, esbuild expands each entry's full dep graph in-memory (each template pulls ~1.5MB of react-email + transitive). The Go subprocess held all of that simultaneously.

  2. Render phase: every bundled .cjs is self-contained with its own copy of react-email, tailwind, css-tree, react. Sequentially requiring them accumulates V8 state that delete require.cache[...] cannot release; heap grew ~14MB per template and crashed at ~290.

Build phase now runs in batches of 10 entry points with esbuild.stop() between batches so the Go subprocess's RSS is released. Render phase runs batches of 25 templates inside worker_threads.Worker instances; each worker's V8 isolate is reclaimed on termination, so peak heap stays bounded by one batch's worth.

Closes #2887


Summary by cubic

Prevents OOM crashes in email export by batching esbuild builds and rendering in short‑lived worker_threads. Also adds a changeset for a react-email patch release.

  • Bug Fixes
    • Build: batch esbuild entry points (10 at a time) and call esbuild.stop() between batches.
    • Render: batch templates (25 per worker) in worker_threads; each worker exits to reclaim its V8 heap. Do not forward execArgv to avoid ERR_WORKER_INVALID_EXEC_ARGV with flags like --max-old-space-size.
    • Progress and errors: spinner updates per template; errors include the failing template name.
    • Cleanup: remove temporary .cjs bundles after rendering.

Written for commit 4ca81a9. Summary will update on new commits. Review in cubic

`email export` crashed with "JavaScript heap out of memory" once a project
had a few hundred templates. Two distinct causes, in two distinct phases:

1. Build phase: `esbuild.build()` was called once with every template as
   an entry point. With `bundle: true`, esbuild expands each entry's full
   dep graph in-memory (each template pulls ~1.5MB of react-email +
   transitive). The Go subprocess held all of that simultaneously.

2. Render phase: every bundled `.cjs` is self-contained with its own copy
   of react-email, tailwind, css-tree, react. Sequentially requiring
   them accumulates V8 state that `delete require.cache[...]` cannot
   release; heap grew ~14MB per template and crashed at ~290.

Build phase now runs in batches of 10 entry points with `esbuild.stop()`
between batches so the Go subprocess's RSS is released. Render phase runs
batches of 25 templates inside `worker_threads.Worker` instances; each
worker's V8 isolate is reclaimed on termination, so peak heap stays
bounded by one batch's worth.

Closes #2887
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-email Ready Ready Preview, Comment May 18, 2026 8:12pm
react-email-demo Ready Ready Preview, Comment May 18, 2026 8:12pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 18, 2026

🦋 Changeset detected

Latest commit: 4ca81a9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
react-email Patch
@react-email/editor Patch
@react-email/ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Member

@gabrielmfern gabrielmfern left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you also add a changeset before we merrge?

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file

Confidence score: 3/5

  • There is a concrete regression risk in packages/react-email/src/cli/commands/export.ts: explicitly forwarding process.execArgv to new Worker() can trigger ERR_WORKER_INVALID_EXEC_ARGV under common Node flags.
  • Given the issue’s relatively high severity/confidence (7/10, 8/10) and direct runtime impact, this is more than a minor cleanup and could affect CLI export behavior for users.
  • Pay close attention to packages/react-email/src/cli/commands/export.ts - remove explicit execArgv forwarding so worker startup remains compatible with inherited Node arguments.

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/react-email/src/cli/commands/export.ts Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

Passing process.execArgv explicitly causes ERR_WORKER_INVALID_EXEC_ARGV
when the parent runs with V8 flags like --max-old-space-size — which is
exactly the workaround users reach for when hitting OOM. Workers inherit
execArgv by default, and Node filters out flags that aren't worker-safe.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Requires human review: This change restructures the core export pipeline with batching and worker threads, introducing moderate risk and requiring human review to ensure edge cases around error handling, worker lifecycle, and file cleanup are handled correctly.

Re-trigger cubic

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Auto-approved: The change addresses an OOM crash by batching esbuild builds and rendering in worker threads, all within the isolated CLI export command—the blast radius is limited and the risk of breakage is low.

Re-trigger cubic

@felipefreitag felipefreitag marked this pull request as ready for review May 18, 2026 20:18
@felipefreitag felipefreitag merged commit 1a61cb0 into canary May 18, 2026
14 of 16 checks passed
@felipefreitag felipefreitag deleted the fix-email-export-oom-large-templates branch May 18, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

email export fails with JavaScript heap OOM when exporting a large number of templates

2 participants