fix: prevent media upload OOM on Workers for large images#262
Merged
ascorbic merged 4 commits intoemdash-cms:mainfrom Apr 6, 2026
Merged
fix: prevent media upload OOM on Workers for large images#262ascorbic merged 4 commits intoemdash-cms:mainfrom
ascorbic merged 4 commits intoemdash-cms:mainfrom
Conversation
…er safety net Large image uploads (5MB+) crash Cloudflare Workers (128MB limit) because generatePlaceholder() decodes entire images to raw RGBA pixels. A 4000x3000 JPEG becomes ~48MB RGBA, exceeding the isolate memory budget. Two-layer fix: - Client-side: browser generates a 64px canvas thumbnail for oversized images and sends it alongside the upload. Server generates blurhash from the thumbnail (~16KB RGBA) instead of decoding the full image. - Server-side: reads dimensions from image headers via image-size and skips placeholder generation when estimated decoded size exceeds 32MB. This covers API/CLI uploads that don't provide thumbnails.
🦋 Changeset detectedLatest commit: 9e6513a The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
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 |
There was a problem hiding this comment.
Pull request overview
This PR addresses out-of-memory crashes during large image uploads on Cloudflare Workers by avoiding full-resolution image decoding for blurhash/LQIP generation when the decoded RGBA size would exceed a 32MB budget.
Changes:
- Add a server-side “safety net” in placeholder generation: read image dimensions from headers and skip placeholder generation for oversized images.
- Generate and upload a small client-side thumbnail for oversized images, and use it for server-side placeholder generation.
- Add unit tests covering the new placeholder skip behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
packages/core/src/media/placeholder.ts |
Adds header-based dimension probing + optional dimensions fallback to skip placeholder generation above a decoded-byte threshold. |
packages/core/src/astro/routes/api/media.ts |
Accepts optional thumbnail multipart field and uses it for placeholder generation; otherwise passes client dims to the safety net. |
packages/core/src/components/InlinePortableTextEditor.tsx |
Generates a 64px PNG thumbnail for oversized images and includes it in upload form data. |
packages/core/src/visual-editing/toolbar.ts |
Mirrors thumbnail generation behavior for the visual editing upload flow. |
packages/core/tests/unit/media/placeholder.test.ts |
Adds tests to ensure oversized-dimension images return null placeholder without decoding. |
.changeset/fix-media-upload-oom.md |
Patch changeset documenting the OOM fix strategy. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/blocks
@emdash-cms/cloudflare
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
Naive sizing (thumbW=64, thumbH=(h/w)*64) could produce an enormous canvas for very tall or very wide images — e.g. a 100x840000 image would allocate a 64x537600 canvas client-side, reintroducing the memory blowup this feature exists to prevent. Extract computeThumbnailSize() that fits the image within a 64x64 box by scaling against max(width, height), wrap canvas allocation and drawImage in try/catch with a no-thumbnail fallback, and add unit tests covering extreme aspect ratios.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Fixes media upload crashing on Cloudflare Workers (128MB memory limit) when uploading large images.
generatePlaceholder()decodes entire images to raw RGBA pixels — a 4000×3000 JPEG becomes ~48MB RGBA, which combined with the original buffer exceeds the Worker isolate limit.Two-layer fix:
<img>for dimension detection. For oversized images (decoded RGBA > 32MB), it now generates a 64px canvas thumbnail and sends it alongside the upload. The server generates blurhash from the thumbnail (~16KB RGBA) instead of decoding the full image.image-sizeand skips placeholder generation when estimated decoded size exceeds 32MB. Images upload fine, just without LQIP.Closes #261
Type of change
Checklist
pnpm typecheckpassespnpm --silent lint:json | jq '.diagnostics | length'returns 0pnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
All 2098 tests pass, including 3 new tests for the safety net:
returns null when image dimensions from headers exceed memory budget— minimal JPEG with SOF0 declaring 5000×4000returns null when fallback dimensions exceed memory budget— unrecognizable buffer with oversized fallback dimsstill generates placeholder for small images with dimensions param— regression checkFiles changed
packages/core/src/media/placeholder.tsimage-sizedimension check +dimensionsparam; skip decode when RGBA > 32MBpackages/core/src/astro/routes/api/media.tsthumbnailfrom form data; pass togeneratePlaceholderinstead of full bufferpackages/core/src/components/InlinePortableTextEditor.tsxpackages/core/src/visual-editing/toolbar.tspackages/core/tests/unit/media/placeholder.test.ts