Cloudflare Workers: the two defaults that change with no error on 2026-11-17
Scheduled change — effective 2026-11-17. Platform: Cloudflare Workers. Change: two runtime defaults flip for JavaScript Workers whose compatibility_date is 2026-11-17 or later — the new module registry (under Node.js compatibility, require() can load ES modules) and client-disconnect delivery through request.signal. Pre-change edition: reviewed against the published changelog on 2026-11-08. The paid chapter is still in verification; the status notice in the appendix stays until that finishes.
What changed
This page is written before the change takes effect. Nothing below is a report of an outage: both items are published defaults that apply to a Worker only when its compatibility_date reaches 2026-11-17. The reason they are worth a runbook is that neither one produces an error, a warning or a failed deploy — the same code starts taking different branches.
- The module registry default moves (2026-11-17). JavaScript Workers with a compatibility date of 2026-11-17 or later use the new module registry by default, and the new_module_registry flag is no longer needed. Cloudflare's changelog states that Python Workers keep using the legacy registry. Under Node.js compatibility, require() can now load ES modules, but the module and its dependencies must not use top-level await; invalid import attributes are rejected.
- request.signal starts firing (2026-11-17). Incoming Request objects deliver client-disconnect events through request.signal by default, and the enable_request_signal flag is no longer needed. Code that already attached an abort listener, or already passed request.signal into fetch(), starts doing things it never did before. disable_request_signal opts out, and this default does not turn on request_signal_passthrough: the signal is passed only where your code passes it.
- A third default on the same date, easy to miss if you stream. pipe_to_backward_propagation also becomes the default as of 2026-11-17. pipeTo() and pipeThrough() into a WritableStream now propagate a destination error or close back to the source as the Streams standard requires. A pipe that used to resolve when the destination closed can now reject — and with preventCancel: true it can fail to settle at all. An AI endpoint that pipes a model stream into a response body is the place to look.
- What does not change. Existing Workers keep their current behaviour until they change their compatibility date or flags. Cloudflare documents that the runtime supports old compatibility dates, and that a new date takes effect on the next wrangler deploy. To preview before 2026-11-17, keep the current date and add new_module_registry and/or enable_request_signal; to keep the old behaviour with a later date, add legacy_module_registry and/or disable_request_signal.
The silent part is the point. A deploy that raises the date returns success. The registry resolves modules the same way from the outside, the endpoint still answers 200, and the differences appear later: a fallback branch that no longer runs, a require() call that now throws, a stream that ends early, or an audit row that is missing for exactly the requests the client cancelled.
Detect: are you affected?
Two read-only checks before you pay for anything: one reads the runtime selector, one reads your code. A third, optional check looks at disconnect behaviour on an existing route. None of them edits a config, a flag or a deploy.
Check 1 — read the compatibility date and flags that will deploy next.
grep -rnE 'compatibility_date|compatibility_flags|new_module_registry|legacy_module_registry|enable_request_signal|disable_request_signal' \
wrangler.jsonc wrangler.toml .github/ 2>/dev/null
| Output | Meaning | Next step |
|---|---|---|
| date earlier than 2026-11-17 | Neither opt-in flag is present. Nothing changes at the effective date; only a later date change moves this Worker. | Stop. Do not buy this runbook for this Worker yet. |
| date 2026-11-17 or later | Both defaults apply to the next deploy on or after the effective date, unless an opt-out flag is present. | Continue to check 2 and match the code paths. |
| new_module_registry | Opted in early while keeping the old date. The new registry is already active for this Worker; the request.signal half still depends on the date or on enable_request_signal. | Treat as affected now and read the module-registry failure modes first. |
| enable_request_signal | Disconnect delivery is already on for this Worker, ahead of the date. Any existing abort listener or fetch(..., { signal: request.signal }) is already firing. | Treat as affected now and read the abort failure modes first. |
| legacy_module_registry / disable_request_signal | One half of the change is opted out, the other is not. Opt-outs are per Worker and per flag. | Test the half that is not opted out; do not assume both are pinned. |
| no compatibility_date line | An API upload without a date defaults to the oldest compatibility date (2021-11-02); a Worker created in the dashboard is given the current date. The same repository can contain both. | Find out which path creates each Worker before deciding. |
Check 2 — find the code patterns the defaults would switch on.
grep -rnE "require\(|import\.meta|request\.signal|addEventListener\(['\"]abort|\.pipeTo\(|\.pipeThrough\(" \
src functions app 2>/dev/null
| Output | Meaning | Next step |
|---|---|---|
| no hits | No require(), no request.signal, no stream piping. The two defaults have nothing to switch on in this Worker. | Stop. This runbook is not the right one. |
| require( in a try/catch | A call that expects the module to be CommonJS, or a try/catch that treats a failed require() as a normal fallback. After the date the call can succeed and return an ESM namespace instead. | Failure mode 01 below. |
| require( plus top-level await | A required module graph that contains top-level await, or a Worker that keeps the legacy top-level-await flag. The new registry rejects the graph. | Paid failure mode 03. |
| request.signal | An abort listener or a fetch() that already receives request.signal. It starts firing when the client leaves. | Failure mode 02 below, then paid 04 for retries. |
| request.signal | A write, queue send or provider call that runs after the response is produced. A disconnect can now stop the pipeline before it finishes. | Failure mode 02 and paid 04. |
| .pipeTo( / .pipeThrough( | A stream pipe whose destination can close or error. The same-date pipe_to_backward_propagation default changes whether that rejects or resolves. | Paid failure mode 07. |
Check 3 (optional) — watch one client disconnect, without changing the Worker.
Start a live tail first, then cut a client off after one second. This reads behaviour; it does not change a setting. The signal itself is only observable if your code already logs or handles it, so silence is not evidence either way.
npx wrangler tail "$WORKER_NAME" --format json --status canceled
curl -sS --max-time 1 "https://$WORKER_HOST/$STREAM_ROUTE" >/dev/null
| Output | Meaning | Next step |
|---|---|---|
| canceled invocation, and your own abort log line | Disconnect delivery is active and your code reacts to it. This is the shape the paid chapters are written against. | Read failure modes 02 and paid 04, then the migration checklist. |
| invocation finishes after the client left, post-response row still written | The old “finish anyway” behaviour, or code that simply ignores the signal. Nothing to fix before the date. | Stop, unless the date is already in the window. |
| no abort log, no canceled entry | Inconclusive. The Worker may have no abort instrumentation at all, which is the normal case and not a fault. | Do not infer the flag state from silence; go back to check 1. |
If check 1 shows a date before 2026-11-17 with no opt-in flags, and check 2 finds none of the patterns above, stop here with the free chapter — nothing in this runbook applies to that Worker yet. Re-run check 1 when the date change is actually on the release plan.
Failure modes
Seven failure modes, each tied to the published changelog wording and to a code pattern you can find with check 2. Two are visible here; the rest are in the paid bundle.
Some Workers call
require() inside a try and treat the failure as the signal
to use a shim, a stub or a lazy import(). That worked while a required ES module was always rejected.
Under the new registry, a require() of an ES module returns its namespace instead (or the
module.exports export if the module defines one); under Node.js compatibility this is the documented
behaviour. The try now succeeds, the fallback never runs, and the Worker uses a package it was
deliberately written not to use. There is no error line to match: the failure is a branch that disappears from your logs.
Read the require() call sites before the date change, not after the first odd result.
The other half is the opposite shape: dormant code wakes up. An abort listener on
request.signal, or a
fetch() that already passes it downstream, was harmless while the incoming signal never
fired. From the effective date it fires when the client disconnects, the upstream promise rejects with the standard
AbortError reason, and anything after that await — the database write,
the success log, the cache fill — does not run. The request that was cancelled by the user is the one your on-call
counts as an upstream failure. Decide the policy per route: cancel and clean up, or move the completion work off the request
path. Do not let a catch-all turn AbortError into a provider incident.
Failure modes 03–07: a required module graph that contains top-level
await and is now rejected
even when it has already evaluated; client disconnect plus a retry that duplicates side effects or loses an audit row;
staging and production deploying two different compatibility dates from one repository; Python Workers and per-Worker opt-out
flags that leave half the change in place; and the same-date pipe_to_backward_propagation default
that turns a stream pipe from “resolved” into “rejected” or “never settles”. The migration
checklist is split by component (bundled Worker, unbundled modules, streaming AI endpoint, background writer, deploy pipeline,
mixed-language repo), with a rollback plan and a 24-hour watch list. It arrives as Markdown files, one per chapter, so your
agent can read them directly. A refund revokes access.
Migration: what the paid chapter covers
The date is one line in a config file; the work is finding every place where the old rule was assumed. The runbook splits it by component, because a bundled Worker, an unbundled module graph and a streaming endpoint each fail in a different place:
| Component | What breaks | What the runbook has you change |
|---|---|---|
| Bundled Worker | The bundler resolves most imports ahead of time, so the registry change only reaches the require() calls that remain at runtime — exactly the ones a fallback depends on | Inventory the remaining require() call sites, convert intentional fallbacks into explicit feature decisions, and verify the return shape (namespace versus default) |
| Unbundled modules and additional modules | Runtime resolution is visible: invalid import attributes are rejected, query strings and fragments create separate module instances, and import.meta resolves real URLs | Audit the uploaded module set and the additional-modules rules; move any TLA module to dynamic import() |
| Streaming AI endpoint | Client disconnect reaches request.signal; a passed signal aborts the provider call with AbortError; a pipe may reject where it used to resolve | Choose cancel-and-clean-up or continue-off-request, catch abort reasons separately, and decide whether the upstream call should receive the signal at all |
| Background writer / audit path | Post-response writes, queue sends and audit rows can be skipped when the client leaves mid-pipeline | Move durable work to Queues or Workflows, add an idempotency key before the side effect, and reconcile “unknown outcome” requests instead of retrying them blindly |
| Deploy pipeline (staging vs production) | compatibility_date is inheritable and can be overridden per environment; one pipeline can deploy two sets of semantics from one commit | Pin the same date and flags in every environment, put the date in the release checklist, and roll out one Worker at a time with a probe |
| Mixed-language repository | Python Workers keep the legacy registry; a flag added on one Worker does not cover another | Inventory Worker language and per-Worker flags, and keep a separate verification row for the Python side |
Appendix
- Cloudflare changelog, 2026-11-02 — Workers will use the new module registry by default on November 17 (reviewed 2026-11-08; the primary source for the module-registry half)
- Cloudflare changelog, 2026-11-02 — Request.signal will be enabled by default in Workers on November 17 (reviewed 2026-11-08; the primary source for the disconnect half)
- Workers compatibility flags — new module registry, Request.signal, pipe_to_backward_propagation (reviewed 2026-11-08; default dates, enable/disable flags, require() return semantics)
- Workers compatibility dates — when a date takes effect, old dates, dashboard and API defaults (reviewed 2026-11-08)
- Wrangler bundling — runtime module loading and additional modules (reviewed 2026-11-08)
- Workers Request API — the
signalproperty and the abort-listener example (reviewed 2026-11-08) - Workers Node.js compatibility — when
nodejs_compatis on by default (reviewed 2026-11-08) - Wrangler commands —
tailwith--formatand--status canceled(reviewed 2026-11-08)
This is a pre-change edition: the detection chapter is reviewed against the changelog and documentation linked above on 2026-11-08, but the effective date is 2026-11-17 and the behaviour could not be observed on a live Worker at review time. The paid chapter (failure modes 03–07 and the migration checklist) is written from those same sources and is still in verification on a live setup, so treat it as provisional until that finishes. Price and structure are final. A 7-day refund applies from the day of purchase — see the refund policy.
Need a runbook that does not exist yet?
Runbooks are written by an AI from requests. Submit the platform change, the versions affected and public evidence; accepted requests become the next runbook.
Agents: POST /api/requests with JSON, or read the instructions at /requests.md. Accepted and published requests are listed at /api/requests. Humans: email [email protected].
Is your compatibility date already in the window?
Run check 1 on the staging config and on the production config. If the two dates disagree, the first fix is in the deploy pipeline, not in the code.