Repository navigation
fix(deploy): match Cloudflare's Worker size limit on Void Cloud - #439
Merged
Merged
Conversation
Managed deploys sent every Worker module as base64 inside the deploy request, which the API caps at 64 MiB. Source maps, which @cloudflare/vite-plugin 1.64 now emits by default, counted against that cap, so a Worker with 27 MB of code and 24 MB of maps failed with HTTP 413 although Cloudflare accepts it. The CLI now uploads Worker modules to R2 with presigned PUTs and names them in the deploy request. The API checks Cloudflare's limit (64 MiB without source maps) before it issues the URLs, then streams the modules from R2 into the Cloudflare upload one at a time, checking size and SHA-256 as the bytes pass. Old CLIs keep the inline path; the new CLI uses the direct path only when platform discovery advertises it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…er uploads A finalize retry with the same idempotency key now finds the admitted operation before the API lists the upload, so it gets the receipt even after the operation's input cleanup deleted the modules. The CLI asks platform discovery once more when the first answer was unusable, so one failed probe does not send a large Worker inline. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
🚀 Deploying Preview to Cloudflare 🚀Preview URL: https://fix-deploy-worker-size-limit-void-cloud.voidzero-docs.workers.dev (commit a64905d)This URL reflects your latest Preview deploymentPreview Deployments by commit
|
Brooooooklyn
added a commit
that referenced
this pull request
Oct 27, 2026
## Problem Large apps cannot deploy to Void Cloud. The `napi-image` deploy ([run](https://github.com/Brooooooklyn/Image/actions/runs/40348054155/job/121667923309)) failed with `The Worker upload did not finish within 120 seconds. Deploy again.` ``` queue job: PUT script to Cloudflare, 120 s timer per attempt body = hdr ─ GET module 1 from R2 ─ bytes ─ hdr ─ GET module 2 ─ ... ─ GET module 744 (one GET at a time, inside the timer) attempt 1: 120 s ✗ → attempt 2: 120 s ✗ → attempt 3 ✗ → attempt 4 ✗ (≈ 490 s, then fail) ``` - The app had 744 modules (413 JS + 331 source maps). 120 s / 744 leaves about 161 ms for each R2 GET, its hash, and its send. - Each retry reads all modules again, so a retry cannot succeed. - The 120 s limit was only measured with local Miniflare R2. - Before #439, the modules were read before the upload, with no timer. That is why this app deployed on 10-07. `@void/md` has a separate problem: `markdown-it-attrs@5` loads `markdown-it` at runtime, but `@void/md` does not provide it. Apps fail with `Cannot find module 'markdown-it'` unless they add it themselves. ## What this PR changes ``` body: hdr ─ module i (already open) ─ bytes ─ open the next modules ahead ... up to 4 modules / 8 MiB ahead, in order timer: no bytes for 60 s → stop (a stalled upload) no answer 120 s after body → stop 8 min for all attempts → stop (shared budget, not 120 s × 4) ``` ``` queued run (15 min Cloudflare limit) ├─ 0 ─────────────────────── 10 min ─────── 15 min │ app upload + DO replays │ compensation reserve │ all end at this deadline ──┘ └─ a DO replay does not start if undoing the hosts already moved would not fit ``` - `attempt_failed` and the new `attempt_done` logs record bytes sent, time waiting on R2, and time waiting on Cloudflare. A later timeout shows which side was slow. - The custom Durable Object host replay sends the same modules. It uses the same read-ahead and limits. - `@void/md` now depends on `markdown-it`. ## Decisions | Question | Choice | Why | |---|---|---| | Read-ahead bound | 4 modules, 8 MiB declared size | One API isolate has 128 MiB for all requests. With the PUT, 4 GETs stay under the 6-connection limit. | | A module over 8 MiB | Opens only at its turn | Opens stay in order. Memory stays bounded. | | Timer model | Idle 60 s + response 120 s + shared 8 min budget | A slow upload that still sends bytes finishes. A stalled one still stops quickly. | | DO replay limit | min(8 min, run deadline), with time kept to undo | The old fixed 60 s would bring the large-bundle timeout back. | | Retry rules | Not changed | 4xx is still final. The saved-script check is still used. | ## Rollout ``` merge → deploy hosted production (platform) → Image redeploys (Brooooooklyn/Image also stops emitting source maps) ``` No CLI change is needed. The fix is all on the platform side. ## Known limits - The real time split on production R2 is not measured yet. The new logs will show it after the first large deploy. - Recovery after an expired lease (`settleExpiredDeployment`) and HTTP rollback still have no run deadline. Each replay there keeps the 8-minute cap. - A Durable Object replay runs inside the cutover transaction, which locks the project row. A slow but progressing replay can now hold that lock for longer than the old 60 s. - Compensation still has no deadline of its own. If restoring the previous release onto a moved Durable Object host takes much longer than the forward move, the queued run can still pass 15 minutes. Lease recovery (`settleExpiredDeployment`) then restores the host, so mixed versions can last until the 18-minute lease expires. This design existed before this PR. A full fix needs a restore budget before any host changes, plus recovery that resumes across runs. It is left for a separate change. ## Testing | Check | Result | |---|---| | `test/unit/worker-upload.test.ts` (read-ahead order, byte bound, release, watchdog, run deadline) | pass | | `test/integration/api-deploy.test.ts` (watchdog cuts stalled attempts, budget stops retries) | pass | | `test/integration/managed-durable-objects.test.ts` (queued run ends DO replay at the deadline; undo time kept) | pass | | `test/integration/deployment-execution.test.ts` (queued upload capped by the run deadline) | pass | | Mutation checks: read-ahead 0, no byte bound, no idle re-arm, no budget, no deadline wiring | each fails a test | | `@void/md` packed into a fresh Yarn PnP project | `markdown-it-attrs` loads; it failed before | | `vp run check` | 33/33 | | `vp run test --base origin/main` | 14911 passed, 28 skipped | 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
Problem
Void Cloud refused Workers that Cloudflare accepts. The napi.rs deploy failed with:
@cloudflare/vite-plugin1.64 writes Worker source maps by default. The CLI sent every Worker module, as base64, inside the deploy request, so the maps counted against the API's 64 MiB body cap.Raising the cap does not fix it. The API Worker has 128 MB per isolate and held about two full copies of the modules: one while it parsed the body, and one in the
FormDatait sent to Cloudflare.What this PR changes
Worker modules now take the same path as static assets. The API streams them to Cloudflare and never holds the whole bundle.
Old clients keep working:
A finalize retry with the same idempotency key finds the admitted operation before the upload is listed. It gets the receipt even after the operation's cleanup deleted the modules.
Decisions
Content-Lengthsigned into each PUT URL, checked again by one LIST at finalizeRollout
Known limits
workerFiles. The approval digest covers inline bytes only, so they keep the 64 MiB body limit. Changing that needs module digests in the approved artifact. That is a product decision, so this PR leaves it alone.MIN_PLATFORM_VERSIONreaches the release that ships this, the new CLI keeps the inline fallback for platforms withoutdirectWorkerUpload.Testing
vp run checkvp test runworkerInput(queued and inline execution, missing or wrong-size module, both fields sent, idempotent retry, reused upload id), streamed multipart (exact bytes, escaped names, mismatch, retry re-opens streams), Durable Object snapshot streaming, CLI path selection and request shapes🤖 Generated with Claude Code