Huny's Dev Blog
huny.dev

Cloudflare Workers November 17 (2026-11-17) Default Changes: New Module Registry, Request.signal, Cache-Control Preservation, and pipeTo Error Propagation

Prepare for the four Cloudflare Workers compatibility flags becoming default on November 17, 2026 (new_module_registry, enable_request_signal, cache_option_preserves_cache_control, and pipe_to_backward_propagation), including top-level await require() restrictions, origin Cache-Control preservation, and pipeTo() rejection handling.

Hun Jang
Hun Jang Nov 15, 2026

Cloudflare Workers 2026-11-17 Four Default Compatibility Flags Architecture Summary Diagram (Conceptual SVG Illustration)

Starting November 17, 2026 (compatibility_date = "2026-11-17"), Cloudflare Workers enables four runtime compatibility flags by default (Workers will use the new module registry by default on November 17, Request.signal will be enabled by default in Workers on November 17, Cloudflare Workers Compatibility flags documentation). Existing deployed Workers keep their current behavior until you update their compatibility_date or flags, but any new Worker created on or after November 17—or any existing Worker bumped to 2026-11-17 or later—automatically activates ① the new JavaScript module registry (new_module_registry), ② incoming client disconnection signals (enable_request_signal), ③ origin Cache-Control preservation on Request.cache (cache_option_preserves_cache_control), and ④ backward stream error/close propagation in pipeTo() (pipe_to_backward_propagation).

The core migration takeaway is direct: when bumping compatibility_date to "2026-11-17" or later, require() under new_module_registry rejects any module graph that uses top-level await (even if that graph has already finished evaluating) and returns an ES module's namespace unless "module.exports" is exported; fetch(url, { cache: "no-cache" }) no longer overwrites the origin's Cache-Control header (use cf: { cacheControl: "no-cache" } to override it intentionally); request.signal fires "abort" on client disconnect but does not enable request_signal_passthrough automatically; and pipeTo() now rejects with a TypeError or destination error instead of silently resolving when a downstream WritableStream closes early. This post synthesizes the official Cloudflare Changelogs and the Workers Compatibility flags reference, paired with a 6-check local Python behavioral simulation rather than live edge latency benchmarks.

1. The Four 2026-11-17 Default Compatibility Changes at a Glance

Cloudflare Workers gates potentially breaking spec-compliance and runtime upgrades behind compatibility_date. The table below summarizes all four flags taking effect on 2026-11-17, along with their opt-in and opt-out (rollback) flags (Compatibility flags — Flags history).

Runtime Area & Scope
Enable Flag /
Disable (Rollback) Flag
Legacy Behavior (Before 2026-11-17)
New Default Behavior (2026-11-17+)
1. Module Registry
(JS/TS Workers only)
new_module_registry /
legacy_module_registry
Legacy module loader; ignored unrecognized import attributes unless flagged; controlled by legacy CJS/ESM flags
URL-based module resolution (distinct ?query/#hash instances), import.meta.*, validated import attributes, require() rejects any TLA graph
2. Client Disconnection Signal
(Request.signal)
enable_request_signal /
disable_request_signal
request.signal did not fire abort on client disconnect unless opted in
request.signal fires abort by default when the client disconnects (does not enable request_signal_passthrough)
3. Subrequest Cache Header
(Request.cache)
cache_option_preserves_cache_control /
cache_option_overrides_cache_control
cache: "no-cache" overwrote origin Cache-Control (caching an origin's no-store response as no-cache)
cache: "no-cache" forces origin revalidation while preserving the origin's Cache-Control (use cf.cacheControl to override)
4. Stream Pipe Error Propagation
(pipeTo / pipeThrough)
pipe_to_backward_propagation /
no_pipe_to_backward_propagation
When a JS WritableStream destination closed/errored while waiting on source, pipeTo() resolved (or hung forever if preventCancel: true)
Cancels source (unless preventCancel: true) and rejects with destination error, TypeError on close, or source cancel error

2. Five Technical Boundaries and Migration Traps to Separate

Before updating compatibility_date to "2026-11-17", verify these five technical boundaries across your Worker codebase and dependencies:

  • URL query strings and fragments create separate module instances under new_module_registry: The new registry resolves module specifiers as URLs. Importing ./worker-plugin.js?tenant=alpha and ./worker-plugin.js?tenant=beta creates two distinct module instances in memory even though they reference the same underlying source file. In addition, import.meta.resolve("./helper.js") formats a bundle URL string without checking whether the module exists or loading it over the network (Workers will use the new module registry by default on November 17).
  • require(ESM) rejects top-level await graphs (even after evaluation) and returns namespaces by default: Under Node.js compatibility (nodejs_compat, enabled by default since 2026-08-04), calling require() on an ES module fails if that module or any of its dependencies uses top-level await—even if the module graph has already finished evaluating (use dynamic import() instead). For synchronous ES modules without top-level await, require() returns the module's namespace object unless the module exports a value named "module.exports" (built-in node: modules and bundles with the __cjsUnwrapDefault marker still unwrap the default export) (Compatibility flags — New module registry).
  • enable_request_signal is separate from request_signal_passthrough: While request.signal now fires an "abort" event by default when the downstream caller disconnects, this does not automatically abort outgoing fetch() subrequests. Unless you explicitly pass { signal: request.signal } to fetch() (or enable request_signal_passthrough), upstream API or LLM calls will continue running after the client leaves (Request.signal will be enabled by default in Workers on November 17).
  • Standard Request.cache revalidation is separate from cf.cacheControl header overriding: Previously, calling fetch(url, { cache: "no-cache" }) caused Cloudflare to cache the response as if the origin had sent Cache-Control: no-cache—even if the origin actually returned Cache-Control: no-store. With cache_option_preserves_cache_control enabled on 2026-11-17, cache: "no-cache" forces conditional revalidation with the origin while respecting the origin's Cache-Control response header. To override the origin's Cache-Control, omit cache and pass cf: { cacheControl: "no-cache" } (Compatibility flags — cache option does not override the origin's Cache-Control).

3. Production Code Patterns: TypeScript Worker & Wrangler Configuration

3.1 TypeScript Worker Adapted for 2026-11-17

The TypeScript Worker below demonstrates all four 2026-11-17 behaviors: import.meta introspection, explicit request.signal propagation to upstream fetch(), origin-preserving cache: "no-cache", and .catch() handling for pipeTo() backward rejection.

3.2 wrangler.jsonc Opt-Out Escape Hatches

If a third-party dependency fails under 2026-11-17 (for example, calling require() on an ES module graph that uses top-level await), you can selectively opt out of individual changes while keeping the 2026-11-17 compatibility date (Compatibility flags documentation).

4. Local Python Verification Script (6 Invariants)

The Python script below models and verifies six invariants across the four 2026-11-17 runtime flags: URL query/fragment module instance isolation, require() rejection of already-evaluated top-level await module graphs, require(ESM) namespace vs. "module.exports" vs. node: default return semantics, preservation of origin Cache-Control: no-store under cache: "no-cache", pipeTo() rejection on destination close (TypeError) or source cancel error, and explicit request.signal propagation boundaries.

5. Pre-Deployment Checklist for compatibility_date >= "2026-11-17"

Audit require() calls for top-level await in transitive dependencies: Under new_module_registry, calling require() on any module graph containing top-level await throws an error even if the graph has already evaluated. Switch those calls to dynamic await import(...).

Check require(ESM) callers expecting unwrapped default exports: Unless a required ES module exports "module.exports" or has a bundler __cjsUnwrapDefault marker, require() returns the module namespace object rather than module.default (and require_returns_default_export has no effect on the new registry).

Validate import attributes (with { type: ... }): Remove unsupported import attribute keys or mismatched module types that the legacy registry silently ignored.

Pass { signal: request.signal } explicitly to outgoing subrequests: Enabling request.signal by default does not turn on request_signal_passthrough. Pass the signal to upstream fetch() calls so disconnected clients stop consuming upstream compute.

Review fetch(url, { cache: "no-cache" }) and pipeTo() error handling: Replace cache: "no-cache" with cf: { cacheControl: "no-cache" } if you intentionally meant to overwrite the origin's Cache-Control header, and attach .catch() / try-catch handlers to pipeTo() calls that pipe into JavaScript WritableStream instances.

6. Official References (Verified 2026-11-15)

Korean version: Cloudflare Workers 11월 17일(2026-11-17) 4대 기본값 전환 해부: 신규 모듈 레지스트리·request.signal·Cache-Control 보존과 pipeTo 역방향 에러 전파 점검하기Cloudflare Workers 11월 17일(2026-11-17) 4대 기본값 전환 해부: 신규 모듈 레지스트리·request.signal·Cache-Control 보존과 pipeTo 역방향 에러 전파 점검하기

추천 글