Runtime compatibility  / field guide
Practitioner field guide · 13 September 2026

Ten years of changing a runtime you are not allowed to break

Cloudflare ships its JavaScript runtime every single day to code it did not write and cannot ask anyone to change. This guide reconstructs how that became possible, and what it costs, from the one file where the whole contract is written down, plus the package registries that date every step of the surrounding decade.

24 primary sources 5 production runtimes 6 incidents Evidence through September 2026 Read: 30 min
01

The territory

A shared execution environment, other people's code already running on it, and a release every day. The problem is not how to build the platform; it is how to keep changing it once you have lost the right to ask anyone to change with you.

187
behaviour flags in the runtime, of which 90 carry a date that turns them on by default
41
patches carried against upstream V8 and rebased on every engine bump
1/day
release cadence; the version number is the date it supports
2026-08-04
the date Node.js compatibility stopped being opt-in, after a first attempt was closed unmerged

The problem, stated without naming anything: you operate an environment that runs other people's programs, you release changes to it continuously, and you have no mechanism to make those programs change in step. Every hosted runtime, every database with a query planner, every long-lived API sits somewhere on this problem. What makes it hard is not the first change. It is the fiftieth, once the accumulated promises of the previous forty-nine are load bearing.

Five organisations have shipped an answer and left it in public. Cloudflare stamps every deployment with a date and freezes the semantics of that date forever. The Node.js project numbers its removals and does remove them: 206 deprecation codes, of which 131 are marked End-of-Life. Fastly swapped the entire JavaScript engine underneath its customers in one release and shipped a flag to turn the swap off. Vercel keeps a separate repository whose stated purpose is "Developing, testing, and defining the runtime Web APIs for Edge infrastructure", which is an admission that the APIs were not defined anywhere else. And the committee the runtime vendors formed for that purpose, WinterCG, moved itself out of the W3C and into Ecma in December 2024 because a Community Group cannot actually produce a standard.

This guide is about the Cloudflare instance, because it is the one where the contract is a file you can read, count and date. That file is compatibility-date.capnp in the workerd repository, and it is the most honest architecture document any of these companies has published, precisely because nobody wrote it to be read as one. It contains sentences like "We're worried that fixing this could break existing workers", attached to a bug that has sat unfixed since the flag list began.

The finding that surprised me

I expected the interesting artefact of Cloudflare's decade to be the Rust rewrite of the proxy. It is not. The interesting artefact is that the runtime's own record shows the opposite of a platform converging: the flag namespace grows faster every year, not slower. Four flags took a default date in 2021; 26 have taken one in the first nine months of 2026. Half the flags in the file, 97 of 187, have never been given a date at all, and 146 carry a permanent escape hatch that lets any customer pin the old behaviour indefinitely. The contract did not stabilise the platform. It made instability affordable, and the bill is a behaviour matrix that only ever gets wider.

Scope. This guide covers the tenant-facing behaviour contract of Cloudflare's developer platform between 2016 and 2026, the language and tooling decisions visible around it, and the comparable contracts at Fastly, Vercel, Node.js and WinterTC. It deliberately does not cover Cloudflare's network architecture, its configuration distribution plane, its pricing, or its published outage postmortems: the session that produced this page could reach code hosts and package registries only, so every claim here rests on repository and registry artefacts rather than on anybody's blog. Where that limit bites, the page says so.

Figure 1 · Five contracts for the same problem

Other people's code
already deployed

Cloudflare Workers
date stamp + 187 flags
nothing is ever removed

Fastly Compute
engine swapped, opt-out flag
3.19.0, 2024-07-29

Node.js
semver + 206 DEP codes
131 already removed

Vercel edge-runtime
a repo that defines
the API surface

WinterTC / Ecma TC55
common API across runtimes
moved from W3C, Dec 2024

Other people's code
already deployed

Cloudflare Workers
date stamp + 187 flags
nothing is ever removed

Fastly Compute
engine swapped, opt-out flag
3.19.0, 2024-07-29

Node.js
semver + 206 DEP codes
131 already removed

Vercel edge-runtime
a repo that defines
the API surface

WinterTC / Ecma TC55
common API across runtimes
moved from W3C, Dec 2024

Notice that only two of the five let the vendor ever remove anything. Sources: workerd flag file, Node.js deprecations, Fastly changelog 3.19.0, vercel/edge-runtime, WinterTC standardization note.
Diagram source
02

How it is actually built

Seven moving parts, six of which are in one Cap'n Proto schema file. The seventh is the rule that a flag must be documented before its date arrives.

The contract works like this. Every deployed Worker carries an immutable compatibility_date chosen by its author, plus an optional list of named flags. The runtime resolves that pair into a set of booleans and then branches on those booleans wherever behaviour has ever changed. The runtime's own version number is a date: v1.20260913.1 shipped on 13 September 2026, and the README states the property plainly, "Updating workerd to a newer version will never break your JavaScript code. workerd's version number is simply a date, corresponding to the maximum compatibility date supported by that version."

What makes this more than a feature toggle system is the second direction. Nearly every flag is declared twice: an enable name for people who want the new behaviour early, and a disable name for people who need the old behaviour permanently. The file's own comment is careful about which is which: an enable flag is "used to enable the feature before it becomes the default, probably for testing purposes", while a disable flag is "used when a worker needs to keep long-term backwards compatibility with one bug but doesn't want to hold back everything else. This is hopefully rare! Most features should have a disable-flag defined." Those two sentences are in tension, and the count settles it: 146 of 187 flags carry a disable name. The rare case is the common case.

Figure 2 · What a compatibility contract is made of

8 flags, per subrequest

Deployment
script + compat date
+ explicit flags

Upload validation
unknown flag names
are rejected

Flag resolution
date implies flags
explicit names override

Runtime branches
187 booleans

Observable behaviour
frozen at the chosen date

Legacy proxy
needs some flags too

Docs gate
flag documented
before its enable date

8 flags, per subrequest

Deployment
script + compat date
+ explicit flags

Upload validation
unknown flag names
are rejected

Flag resolution
date implies flags
explicit names override

Runtime branches
187 booleans

Observable behaviour
frozen at the chosen date

Legacy proxy
needs some flags too

Docs gate
flag documented
before its enable date

The interesting edge is the dashed one: eight flags have to be forwarded to the older proxy on every subrequest, so the contract does not end at the runtime boundary. Reconstructed from compatibility-date.capnp and docs/api-updates.md.
Diagram source

The date stamp, not the version

A deployment pins a date, never a runtime version. That inverts the usual dependency: the operator upgrades daily and the tenant never does, which is the only arrangement that scales to millions of deployments nobody can contact.

Stated at: workerd README

Implication, so dates stay readable

46 flags are declared as implied by another flag after a date, for example nodeJsCompatV2 is implied by nodeJsCompat after 2024-09-23. The mechanism exists because a date alone cannot express "new behaviour, but only for people who already opted into this family".

Counted in: the flag file

An experimental class that never dates

39 flags are marked experimental and take no date. This is how the platform ships unfinished surface without promising it, and it is also where flags go to be forgotten: 12 fields are now named obsoleteNN, retained as dead numbers because removing a field would renumber the schema.

Counted in: the flag file

Two details raise this from a pattern to an architecture. The first is the documentation gate. The repository's contributor doc says compatibility flags "MUST be documented BEFORE their enable date", and puts the obligation on the author: "It is the responsibility of whoever introduces the flag to update the documentation at the same time as the flag." Each flag gets its own markdown file in the public docs repository. The contract is therefore enforced in three places, the schema, the docs tree, and upload validation, and a flag that exists in only one of them is a defect a reviewer can see.

The second is the global escape hatch, and how little it is used. One annotation, compatEnableAllDates, turns a flag on for every date including the past. Its own comment reads: "This is almost NEVER what you actually want because you're most likely breaking back compat. Note that workers uploaded with the flag will fail validation, so this will break uploads for anyone still using the flag." In 187 flags it has been used exactly once, on the R2 public beta bindings. A mechanism that exists, is documented, and is used once in five years is a good sign: the team wrote down the unsafe path so that taking it would be a deliberate, reviewable act.

Figure 3 · The life of a behaviour flag

39 flags

named enable flag

gets a date (90 flags)

disable name kept (146 flags)

no date, ever (97 flags)

behaviour retired (12 fields)

Experimental

OptIn

Dated

Permanent

Undated

Obsolete

39 flags

named enable flag

gets a date (90 flags)

disable name kept (146 flags)

no date, ever (97 flags)

behaviour retired (12 fields)

Experimental

OptIn

Dated

Permanent

Undated

Obsolete

The terminal state is not "removed", it is "kept forever as a disable name", which is why the file only grows. Counts from the flag file, 13 September 2026.
Diagram source

Where the reconstruction gets interesting is the eight flags annotated neededByFl, which the schema describes as "A tag to tell us which fields we'll need to propagate to FL on subrequests and responses." FL is the older proxy tier. Neither the schema nor the README explains the relationship, but the flags that need propagating are the ones about HTTP surface, Brotli content encoding, custom ports, cache-rule overrides and gRPC conversion, and one of the oldest flags in the file is a 2021 note that the original fetch() "happily sent any scheme to FL in the X-Forwarded-Proto header". The inference, which neither document states, is that the new runtime never replaced the old proxy: it was slotted in beside it, and per-request behaviour has to be negotiated across that seam. If you are planning a rewrite, that seam is the part nobody budgets for.

03

The decisions that matter

Six forks visible in the record, with the condition that would flip each one for a team that is not Cloudflare.

Decision: how do you version behaviour when you cannot version the client?

Chosen
  • A date per deployment, frozen forever, with named overrides in both directions
  • Lets the operator ship daily while the tenant holds still
Rejected
  • Semantic versioning of the runtime, the Node.js answer
  • Semver assumes the tenant chooses when to upgrade; a hosted runtime has no such moment
Flips when
  • You can force or schedule tenant upgrades, or tenants can pin a full runtime version themselves
  • Then you can delete code: Node has removed 131 deprecated APIs, Cloudflare has removed none

Decision: does a compatibility break get a date, a name, both, or nothing?

Chosen
  • Both, in 90 of 187 cases: a name to opt in early and a date that flips the default
  • A disable name survives the date, so nobody is ever forced
Rejected
  • Fixing the bug outright for everyone
  • Tried once, on Symbol.toStringTag, and reversed: "it turns out that was more breaking than expected"
Flips when
  • The bug is a security defect, where the old behaviour cannot be offered at all
  • Or when nobody has depended on it yet, which you can only know if you can query your own tenants' code

Decision: rewrite the local emulator, or ship the real runtime to laptops?

Chosen
  • Ship the runtime. workerd was opened up in 2022 and Miniflare 3.0.0 (17 May 2023) rebuilt the local simulator on it
  • One implementation to be correct about, and the date contract now covers local runs too
Rejected
  • Continuing Miniflare as an independent reimplementation, which is what it was from May 2021
  • An emulator's bugs are indistinguishable from platform bugs to the person hitting them
Flips when
  • Your runtime cannot be made to run off your infrastructure, because it is entangled with storage, identity or hardware
  • Then the emulator is unavoidable and its divergences are your support cost forever

The fourth decision is the one most teams get wrong in the other direction. Cloudflare is the company that put Rust in the network path: quiche on crates.io since 24 January 2019, boringtun since 22 March 2019, lol_html since 26 November 2019, pingora first published 7 December 2022 and opened to the public in 2024, foundations since 23 January 2024. Its own README gives the rule: use Pingora if "Security is your top priority: Pingora is a more memory safe alternative for services that are written in C/C++". And yet the runtime is C++, because it is V8, and the CLI went the other way entirely. Wrangler v1 was Rust, published as @cloudflare/wrangler from 1 June 2019; wrangler v2 is TypeScript, published as wrangler from 8 May 2022; the v1 repository was archived on 3 August 2023 with the notice "This repository is for Wrangler version 1, which is deprecated and no longer updated." The pattern is not "rewrite in Rust". It is "put the language where its cost is paid": memory safety in the per-request path, ecosystem gravity in the developer tool.

DecisionChosenRejectedBecauseFlips whenEvidence
Runtime versioningDate per deploymentSemver runtimeNo tenant upgrade moment existsYou can schedule tenant upgradesworkerd README
Release cadenceDaily, version is the dateBatched releasesFreezing semantics decouples cadence from riskYour rollback unit is bigger than a dayreleases
Engine upgrades41 patches rebased onto each V8Forking V8Patches keep you on the upstream release trainYour patches stop rebasing cleanlyv8-updates.md
CLI languageTypeScript, from 2022-05-08Rust, 2019 to 2023The tool lives in the users' ecosystemThe CLI becomes a performance-critical daemonwrangler-legacy
Node.js APIsDefault on from 2026-08-04A purely web-standard surfaceEcosystem cost exceeded the purity benefitYour users' libraries do not assume NodePR 6444
Local developmentShip the runtime itselfIndependent emulatorTwo implementations means two behavioursThe runtime cannot leave your infrastructureminiflare registry record

The Node.js decision deserves its own paragraph, because the argument is public and it is about exactly the thing this guide is about. On 27 March 2026 a Cloudflare engineer opened PR 6444, "Enable nodejs_compat by default", adding a date of 2026-04-14 to the nodeJsCompat flag. A reviewing engineer listed the side effects: Buffer and process on the global scope, setImmediate on the global scope, performance on the global scope, and "setTimeout and setInterval returning an object rather than a number", which he flagged as the one that "likely has the greatest potential for introducing problems". The PR was closed unmerged on 17 April 2026. The flag carries a date today, 2026-08-04, four months later than proposed. A platform that had no date mechanism would have had to choose between shipping the break and never shipping it; this one could take the time to argue, then encode the outcome as a date in a file.

Figure 4 · Does this change need a flag, a date, or both?

no

yes

yes

no

no

yes

Can any deployed
program observe
the difference?

Ship it. No flag.

Is the old behaviour
unsafe to keep
offering?

Fix for everyone.
No disable name.
Announce.

Is the new behaviour
finished?

Enable name only.
Mark experimental.
No date.

Enable name + date
+ disable name kept
forever. Document first.

no

yes

yes

no

no

yes

Can any deployed
program observe
the difference?

Ship it. No flag.

Is the old behaviour
unsafe to keep
offering?

Fix for everyone.
No disable name.
Announce.

Is the new behaviour
finished?

Enable name only.
Mark experimental.
No date.

Enable name + date
+ disable name kept
forever. Document first.

Every terminal box is an action. The one teams skip is "Fix for everyone": a security fix does not get an opt-out. Derived from the annotation semantics in compatibility-date.capnp.
Diagram source
04

What broke in production

Five failures, in three classes. None of them is the failure people expect, which is a customer's code breaking because the platform changed underneath it.

An evidence limit first, because it changes how you should read this section. Cloudflare's own incident postmortems are published on its blog, and the blog was not reachable from the environment this guide was researched in. Everything below comes from security advisories and issue threads on the code host, which are incident records of a narrower kind: they tell you what broke and what was patched, but not how long it lasted, how many tenants were affected, or what the team changed in its process afterwards. Treat the blast radius rows as the weakest claims on the page.

Postmortem

A dated default turned a dependency bug into a remote crash

AssumptionTurning on a protocol feature by date is safe because the feature itself is well understood.
What happenedWebSocket compression became the default for compatibility dates on or after 2023-08-15. A vulnerability in Cap'n Proto, a dependency, then meant "a remote client may be able to induce workerd to crash" for Workers handling WebSocket messages.
Blast radiusWorkers accepting WebSocket connections and processing or forwarding messages, all versions before v1.20231121.0. Cloudflare states production was patched before disclosure, with no evidence of exploitation.
FixPatched dependency, released 21 November 2023 as v1.20231121.0, CVE-2023-48230.
Design ruleA date that enables a feature also enables that feature's entire dependency surface. Your flag inventory is not the same as your attack surface inventory; enumerate what each default pulls in.
Source

A spec-compliance fix was "more breaking than expected"

AssumptionMaking an object graph standards compliant is a correctness improvement, not a behaviour change.
What happenedSetting Symbol.toStringTag on all runtime objects fixed several compliance bugs and broke deployed code that was type-sniffing. The remedy was a new flag whose comment says it plainly: "Unfortunately it turns out that was more breaking than expected."
Blast radiusNot published. The repair is dated 2024-09-26, so every deployment with an earlier date keeps the old behaviour permanently.
FixA retroactive flag, set_tostring_tag with do_not_set_tostring_tag, restoring the original behaviour below the date.
Design ruleAny change to how an object identifies itself is an API change, because the ecosystem detects features by sniffing. Budget a rollback flag before you ship the correctness fix, not after.
Source

The same fix had to ship twice because the first flag missed a call site

AssumptionOne flag covers one behaviour change.
What happenedThe WHATWG-compliant URL parser shipped behind url_standard with a date of 2022-10-31. A second flag had to follow for redirects, with the comment "the specCompliantUrl flag did not contemplate the redirect usage".
Blast radiusNot published. Deployments between the two dates ran a mixed implementation: compliant parsing, legacy redirect handling.
FixA second flag, response_redirect_url_standard, with its own disable name.
Design ruleGate the implementation, not the call sites. If a flag guards a component that several code paths use, enumerate those paths in the flag's own review, or you will ship the same fix again under a different name.
Source

Behaviour arrived from upstream and bypassed the contract entirely

AssumptionNew JavaScript globals reach tenants only through flags the platform controls.
What happenedA V8 upgrade exposed the Temporal API in the deployed runtime with its clock unvirtualised: "Temporal.Now.instant().epochMilliseconds returns 0 even at request time and after I/O, while Date.now() correctly returns real time." Code guarding a polyfill with typeof Temporal === 'undefined' silently stopped polyfilling, and issued JWTs stamped 1970 that "downstream verifiers correctly reject".
Blast radiusReported 31 July 2026 by an affected developer, against the deployed runtime only; the reporter notes open-source workerd builds V8 without Temporal, so the two had diverged.
FixIssue closed; the reporter's request was either to disable Temporal until its clock matches the virtualised time source, or to make Temporal.Now track Date.now().
Design ruleA behaviour contract covers the behaviour you wrote. Anything inherited from an engine, a kernel or a library needs an allowlist at the boundary, because feature detection in user code turns a silent addition into a silent behaviour change.
Postmortem

Memory safety, in the runtime that is not written in Rust

AssumptionAn integer count of collection elements cannot realistically overflow.
What happenedA FormData instance exceeding 2^31 elements made forEach() "read from the wrong location in memory while iterating over elements", typically a crash, theoretically undefined behaviour.
Blast radiusRated moderate, CVSS 6.5. Exploitation needed roughly 160GB of RAM and tens of gigabytes of form-encoded upload, which the advisory calls impractical on Cloudflare's platform but possible elsewhere.
FixReleased 19 April 2023 as v1.20230419.0, CVE-2023-2512.
Design ruleThe memory-safety argument that justifies a Rust proxy applies with equal force to the C++ runtime, and the runtime is where tenant-controlled input arrives. Know which of your components the safety argument has not reached yet, and why.
Source

The fix that has never shipped, five years on

AssumptionA known bug can wait for the next convenient default date.
What happenedThe HTML rewriter treats an esi:include element as needing an end tag, which is wrong. The flag that fixes it, html_rewriter_treats_esi_include_as_void_tag, is the third entry in the file and carries no date. Its comment: "We're worried that fixing this could break existing workers."
Blast radiusEvery deployment that has not explicitly opted in, since the flag list began in 2021. Nobody has published how many that is.
FixNone. The flag exists, opt-in only, and the wrong behaviour remains the default in 2026.
Design ruleAn opt-in fix with no date is a permanent fork in your behaviour, and it gets harder to date every year. Give every flag a date at creation, even a distant one, or record why it can never have one.

Figure 5 · How a silent addition becomes a production failure

Downstream verifierTenant WorkerDeployed runtimeV8 upgradeDownstream verifierTenant WorkerDeployed runtimeV8 upgradeclock virtualisation coversDate.now() onlypolyfill silentlydisabledships Temporal globaltypeof Temporal === 'undefined' ?definedTemporal.Now.instant()epoch 0JWT with iat = 1970rejected
Downstream verifierTenant WorkerDeployed runtimeV8 upgradeDownstream verifierTenant WorkerDeployed runtimeV8 upgradeclock virtualisation coversDate.now() onlypolyfill silentlydisabledships Temporal globaltypeof Temporal === 'undefined' ?definedTemporal.Now.instant()epoch 0JWT with iat = 1970rejected
Nothing in this path is a bug in any single component; the failure is created by feature detection meeting an unflagged addition. From workerd issue 6907.
Diagram source

Group those six and three classes fall out. Class one, the default that pulls in more than it says: the WebSocket compression date. Class two, the correctness fix that is a behaviour change: toStringTag and the two URL flags, both repaired with more flags. Class three, behaviour that enters from outside the contract: Temporal. Only class two is the failure mode the contract was designed for, and it is the only one the contract actually contained. That is the honest summary of what a compatibility date buys you: it defends the surface you wrote, on the axis you anticipated, and nothing else.

There is a fourth class the public record cannot show, and naming it is the most useful thing this section can do. No published artefact describes what happens to a deployment that never moves its date. The ESI card above is the visible end of it: a bug nobody dares fix by default because the population that depends on it is unknown. Somewhere there are Workers running 2021 semantics on a runtime released today. Nobody has published how many, how they are monitored, or what would happen if a dependency forced that behaviour to change. If you adopt this pattern, that population is your real liability, and you should instrument it on day one.

05

Numbers you can plan against

Everything countable in the record, with what it was counted from and when. The counts are reproducible: fetch the file and run the same greps.

MetricValueAtContextAs ofSource
Behaviour flags in the runtime187CloudflareBool fields in the compatibility schema2026-09-13flag file
Flags with a default date90CloudflareThe rest are opt-in only2026-09-13flag file
Flags with a permanent opt-out146CloudflareDisable names, kept after the date2026-09-13flag file
Dated flags per year4 / 11 / 10 / 17 / 20 / 26Cloudflare2021 through 2026 to date; the last is nine months, not twelve2026-09-13flag file
Flags forwarded to the older proxy8CloudflareneededByFl annotation2026-09-13flag file
Uses of the break-everyone escape hatch1CloudflareR2 public beta bindings2026-09-13flag file
Patches carried against V841CloudflareRebased on every engine update2026-09-13patches/v8
Node.js deprecation codes206Node.js131 End-of-Life, 23 runtime, 42 docs-only2026-09-13deprecations.md
Rust CLI lifetime2019-06-01 to 2023-08-03CloudflareFirst publish of @cloudflare/wrangler to repository archive2026-09-13wrangler-legacy
TypeScript CLI major versions2.0.0 / 3.0.0 / 4.0.0Cloudflare2022-05-08, 2023-05-17, 2025-03-132026-09-11npm registry
Local simulator, first to workerd-based2021-05-20 to 2023-05-17CloudflareMiniflare 0.1.0 to 3.0.02026-09-11npm registry
Rust in the network path, first publish dates2019-01-24 / 2019-03-22 / 2019-11-26 / 2022-12-07 / 2024-01-23Cloudflarequiche, boringtun, lol_html, pingora, foundations2026-09-13crates.io
Pingora crate downloads9,176,279CloudflareAdoption of the framework, not of the proxy2026-09-13crates.io
Requests per second served by Pingora40,000,000+CloudflareVendor claim in the project README, no independent measurement found2026-09-13pingora README
Read these carefully

Measured here: every flag count, the patch count and the publication dates, all taken from files fetched on 13 September 2026 and countable again in a minute. Claimed: the 40 million requests per second and the memory-safety framing around it are the project's own marketing, unverified by anything in this corpus. Derived: the per-year flag growth is my count of enable dates grouped by year, and 2026 is a partial year, so the trend is stronger than the table makes it look. Unknown: how many deployments sit on old dates, the size of the oldest cohort, and whether any published mechanism retires one. That gap is the single most important thing missing from the public record.

06

The evidence wall

Every source behind this page, graded. Note the shape of it: this is a corpus of repository artefacts, because that is what the research environment could reach.

Source Cloudflare2026-09

workerd compatibility-date.capnp

The whole contract in one schema: 187 flags, 90 with dates, 146 with permanent opt-outs, 39 experimental, 12 retired to dead field numbers, and a running commentary of admissions about bugs that could not be fixed outright.

Carry forwardIf you version behaviour, keep the versioning in one file that a reviewer can read end to end.
raw.githubusercontent.com/cloudflare/workerd
Decision record Cloudflare2026-09

workerd docs/api-updates.md

The rule that makes the contract auditable: flags "MUST be documented BEFORE their enable date", and the author of the flag owns the docs change in the same window.

Carry forwardTie the documentation obligation to the flag's date, not to the release.
raw.githubusercontent.com/cloudflare/workerd
Decision record Cloudflare2026-09

workerd docs/v8-updates.md

The twelve-step procedure for moving the engine underneath everyone, including rebasing the local patch series onto each new V8 and an automation script the doc says "has not been thoroughly tested".

Carry forwardWrite the upgrade procedure down as steps a stranger can run; the patch series is a dependency you own.
raw.githubusercontent.com/cloudflare/workerd
Source Cloudflare2026-09

workerd patches/v8

41 patch files, including promise context tagging and cross-request promise resolution, which are the pieces that make one V8 isolate serve many tenants' requests safely.

Carry forwardCount your upstream patches; the number is the true cost of staying on someone else's release train.
github.com/cloudflare/workerd
Postmortem Cloudflare2023-11

GHSA-g76q-p3jj-mm56, CVE-2023-48230

A Cap'n Proto vulnerability reachable because WebSocket compression is on by default for compatibility dates from 2023-08-15. Production was patched before disclosure.

Carry forwardEvery dated default silently enlarges the dependency surface of every tenant past that date.
github.com/cloudflare/workerd
Postmortem Cloudflare2023-05

GHSA-8vx6-69vg-c46f, CVE-2023-2512

Integer overflow in FormData iteration reading from the wrong memory, impractical to exploit on Cloudflare's own platform, patched in v1.20230419.0.

Carry forwardSelf-hosting a runtime inherits its memory-safety profile without inheriting the operator's resource limits.
github.com/cloudflare/workerd
Source Cloudflare2026-07

workerd issue 6907: Temporal with a clock stuck at epoch 0

An engine upgrade exposed a new global whose clock bypassed the runtime's virtualised time, and feature detection in tenant code turned that into JWTs stamped 1970.

Carry forwardInherited behaviour needs an allowlist at the boundary; your flag file only covers what you wrote.
github.com/cloudflare/workerd
Decision record Cloudflare2026-04

workerd PR 6444: Enable nodejs_compat by default, closed unmerged

The argument in public: globals added, and "setTimeout and setInterval returning an object rather than a number" as the change with "the greatest potential for introducing problems". Closed 17 April 2026; the flag carries 2026-08-04 today.

Carry forwardA date mechanism buys you the option to argue for four months instead of shipping or abandoning.
github.com/cloudflare/workerd
Source Cloudflare2026-09

workerd README and release tags

The promise, "Updating workerd to a newer version will never break your JavaScript code", alongside a release every day whose version number is that day's date, and the warning that workerd alone "is not a hardened sandbox".

Carry forwardDaily releases and frozen semantics are the same decision viewed from two ends.
github.com/cloudflare/workerd
Source Cloudflare2023-08

cloudflare/wrangler-legacy, archived

The Rust CLI, 2,775 commits and 3.2k stars, archived on 3 August 2023 with a deprecation notice pointing at the TypeScript rewrite.

Carry forwardDeveloper tooling belongs in the users' ecosystem even when the company's centre of gravity is elsewhere.
github.com/cloudflare/wrangler-legacy
Source Cloudflare2026-09

npm registry: wrangler and @cloudflare/wrangler

Dates the reversal precisely: Rust CLI first published 1 June 2019 and last on 21 February 2023; the TypeScript line starts at 2.0.0 on 8 May 2022, so the two ran side by side for nine months.

Carry forwardRegistry metadata dates a migration more reliably than any blog post about it.
registry.npmjs.org
Source Cloudflare2026-09

npm registry: miniflare

0.1.0 on 20 May 2021, 3.0.0 on 17 May 2023 the same day as wrangler 3, and current builds versioned by calendar date, 5.20260911.0-alpha.

Carry forwardWhen the emulator's version scheme starts matching the runtime's, the emulator has stopped being a separate implementation.
registry.npmjs.org
Source Cloudflare2026-09

crates.io: pingora, quiche, boringtun, lol_html, foundations

The Rust arc with dates: QUIC in January 2019, WireGuard in March 2019, the HTML rewriter in November 2019, the proxy framework in December 2022, the service library in January 2024.

Carry forwardA language migration is legible as a sequence of first-publish dates, and the order tells you where the pain was.
crates.io
Vendor Cloudflare2026-09

pingora README

The claim of more than 40 million requests per second, and the stated reason to adopt it: a memory-safe alternative for services written in C or C++.

Carry forwardTreat a project README's scale figure as a claim; this corpus contains no independent measurement of it.
raw.githubusercontent.com/cloudflare/pingora
Vendor Cloudflare2026-09

quiche README

"quiche powers Cloudflare edge network's HTTP/3 support", with the library also shipping on Android, which is the rare case of a vendor's infrastructure component becoming someone else's platform dependency.

Carry forwardComponents that leave the building acquire compatibility obligations of their own.
raw.githubusercontent.com/cloudflare/quiche
Source Node.js2026-09

Node.js doc/api/deprecations.md

The opposite contract, written down: 206 numbered deprecations with 131 already at End-of-Life, which is what becomes possible when the user chooses the runtime version.

Carry forwardIf you can make users choose a version, you can delete code; if you cannot, you cannot.
raw.githubusercontent.com/nodejs/node
Source Fastly2024-07

js-compute-runtime CHANGELOG 3.19.0

"use StarlingMonkey by default, --disable-starlingmonkey flag": an entire JavaScript engine swapped under customer code in one release, with a named way back.

Carry forwardThe opt-out flag is the unit of trust in a hosted runtime, whatever the size of the change behind it.
raw.githubusercontent.com/fastly/js-compute-runtime
Decision record WinterTC2024-12

wintercg/admin standardization.md

Why the runtime vendors' interoperability group left the W3C for Ecma: "technical standards development work will need to take place in a TC or WG, rather than a W3C CG".

Carry forwardCross-vendor compatibility needs a venue that can actually publish a specification, or each vendor keeps its own flag file.
raw.githubusercontent.com/wintercg/admin
Source Vercel2026-09

vercel/edge-runtime

Still active, 915 stars, describing itself as "Developing, testing, and defining the runtime Web APIs for Edge infrastructure": a third vendor maintaining a fourth definition of the same surface.

Carry forwardWhere several vendors each define the surface, portability claims are about the intersection, and nobody publishes it.
github.com/vercel/edge-runtime
Vendor Cloudflare2026-09

cloudflare-docs: per-flag documentation tree

Each compatibility flag has its own markdown file in the public docs repository, which is the artefact the contributor rule in api-updates.md points at.

Carry forwardOne file per flag makes the contract diffable by outsiders, which is what turns a policy into an audit trail.
github.com/cloudflare/cloudflare-docs
Source Cloudflare2026-09

workerd commit history: daily release commits

"Release 2026-09-13", "Release 2026-09-12", and so on, cut by an automation account between ordinary pull request merges from named engineers.

Carry forwardA daily release train is only safe when the semantics a tenant sees are pinned somewhere other than the release.
github.com/cloudflare/workerd
Source Cloudflare2026-09

workerd Python compatibility annotations

Python Workers carry their own annotation class, pythonSnapshotRelease, for changes that break memory snapshots, and pin a Pyodide generation by flag, for example python_workers_20250116 implied after 2025-09-29.

Carry forwardA second language runtime needs its own compatibility axis; dates alone will not carry snapshot or ABI concerns.
raw.githubusercontent.com/cloudflare/workerd
Decision record Cloudflare2026-09

workerd docs/hardening.md

The C++ rules the runtime is held to: bounds and null checking enabled everywhere, raw references described as "incredibly dangerous", smart pointers required by default.

Carry forwardIf the safety argument cannot reach a component, write the compensating rules down and enforce them in review.
raw.githubusercontent.com/cloudflare/workerd
Source Cloudflare2026-09

cloudflare/foundations

The service library the Rust components share: logging, tracing, metrics, memory profiling and seccomp sandboxing, published on crates.io since January 2024 and now a dependency of Pingora's own telemetry crate.

Carry forwardA rewrite programme needs a shared operability library early, or each rewritten service invents its own observability.
github.com/cloudflare/foundations
What this corpus is missing

No conference talk, no peer-reviewed paper and no engineering blog post is cited on this page, and that is not because none exist. The research environment for this guide could reach code hosts and package registries only, so Cloudflare's own blog, the archive, the usual conference video hosts and the paper repositories were all out of reach. The upside is a corpus nobody wrote for an audience: schema comments, patch filenames, archive banners and registry timestamps. The downside is that motives and outcomes are inferred rather than reported, and none of Cloudflare's published outage postmortems could be consulted. If you extend this work, start with the blog posts named in the READMEs cited above.

07

Build a miniature, then productionise it

Six rungs. The line between a toy and the real thing is rung four, where you stop being able to delete anything.

Stamp a date on a deployment and branch on it

Take any service with a handler interface and add an immutable behaviour_date to each deployed unit. Implement one behaviour change twice, old and new, selected by that date.

Done when: two units deployed on the same binary produce different documented outputs for the same input.  Teaches: semantics are per-deployment state, not per-release state.

Add both flag directions and reject unknown names

Give the change an enable name and a disable name, and make deployment fail on any flag name the binary does not know, the way Cloudflare rejects uploads carrying retired flags.

Done when: a deployment with a typo in a flag name is refused at upload, not at request time.  Teaches: the flag namespace is an API, so it needs validation and a deprecation story of its own.

Write the resolution order down and test it

Date implies a flag set; explicit names override; implication rules like "flag B is implied by flag A after date D" come last. Write a table-driven test over dates and flag combinations.

Done when: the test enumerates every flag against three dates and fails if a new flag is added without a case.  Teaches: the resolution order is the part people get wrong, and the only defence is exhaustive tests.

Ship it to someone else, then try to delete a flag

Publish the interface to a second team, wait for them to deploy, then attempt to remove the oldest behaviour. Measure what you have to do to find out whether anyone depends on it.

Done when: you can answer "how many live deployments would this removal change" with a query rather than a guess.  Teaches: the cost of the contract is not the branching, it is that removal now requires a census.

Take an upstream dependency bump and diff the surface

Upgrade an engine, library or kernel your behaviour is built on and enumerate every observable difference it introduces. Gate the ones your contract does not already cover.

Done when: the upgrade's new observable surface is listed in the changelog, and anything unflagged is a deliberate, recorded choice.  Teaches: the Temporal failure, before it happens to you.

Instrument the cohort that never moves

Emit the resolved date and flag set with every request, then build the distribution: oldest date in production, number of deployments more than two years behind, flags that only pre-2022 deployments still use.

Done when: a dashboard answers "what breaks if we retire behaviour X" in one query.  Teaches: the liability of a compatibility contract is a population, and populations are only manageable once they are measured.

08

Keep hunting

What actually worked here, in an environment where engineering blogs were unreachable and only code hosts and registries answered. The method transfers to any company with a public repository.

Find the file where the contract lives

  • site:github.com <org> compatibility flag schema
  • curl https://raw.githubusercontent.com/<org>/<repo>/main/docs/api-updates.md
  • grep -c 'compatEnableDate' compatibility-date.capnp

Surface the arguments, not the announcements

  • https://github.com/<org>/<repo>/pulls?q=is%3Apr+is%3Aclosed+is%3Aunmerged+<topic>
  • https://github.com/<org>/<repo>/issues?q=is%3Aissue+regression+OR+broke+<feature>
  • https://github.com/<org>/<repo>/security/advisories

Date a migration without a blog post

  • curl https://registry.npmjs.org/<package> | jq '.time | {created, "1.0.0", "2.0.0"}'
  • curl https://crates.io/api/v1/crates/<crate> | jq '.crate.created_at'
  • site:github.com <org> archived "no longer maintained"

Measure the seams nobody documents

  • grep -n 'neededByFl\|Unfortunately\|worried' compatibility-date.capnp
  • https://github.com/<org>/<repo>/tree/main/patches
  • https://github.com/<org>/<repo>/commits/main

Two habits did most of the work. First, read the comments rather than the code: "We're worried that fixing this could break existing workers" is a design position nobody would put in a blog post. Second, prefer artefacts with timestamps you did not have to trust: a registry publication date, an archive banner, a release tag. When the narrative sources are unavailable, the bookkeeping is still there, and the bookkeeping is harder to polish.

09

References

  1. Cloudflare, workerd compatibility-date.capnp GitHub. Checked 2026-09-13.
  2. Cloudflare, workerd docs: JavaScript API Updates GitHub. Checked 2026-09-13.
  3. Cloudflare, workerd docs: V8 Updates GitHub. Checked 2026-09-13.
  4. Cloudflare, workerd docs: Hardening GitHub. Checked 2026-09-13.
  5. Cloudflare, workerd patches against V8 GitHub, 41 files. Checked 2026-09-13.
  6. Cloudflare, workerd README GitHub. Checked 2026-09-13.
  7. Cloudflare, workerd releases GitHub. Checked 2026-09-13.
  8. Cloudflare, workerd commit history GitHub. Checked 2026-09-13.
  9. Cloudflare, workerd PR 6444: Enable nodejs_compat by default GitHub, opened 2026-03-27, closed unmerged 2026-04-17. Checked 2026-09-13.
  10. Cloudflare, workerd issue 6907: native Temporal with a clock stuck at epoch 0 GitHub, 2026-07-31. Checked 2026-09-13.
  11. Cloudflare, GHSA-g76q-p3jj-mm56: WebSocket message can cause crash GitHub Security Advisories, 2023-11-21, CVE-2023-48230. Checked 2026-09-13.
  12. Cloudflare, GHSA-8vx6-69vg-c46f: Buffer under-read in workerd GitHub Security Advisories, 2023-05-12, CVE-2023-2512. Checked 2026-09-13.
  13. Cloudflare, wrangler-legacy repository GitHub, archived 2023-08-03. Checked 2026-09-13.
  14. npm registry metadata for wrangler npm, Inc. Checked 2026-09-13.
  15. npm registry metadata for @cloudflare/wrangler npm, Inc. Checked 2026-09-13.
  16. npm registry metadata for miniflare npm, Inc. Checked 2026-09-13.
  17. crates.io metadata for pingora Rust Foundation. Checked 2026-09-13.
  18. crates.io metadata for quiche Rust Foundation. Checked 2026-09-13.
  19. crates.io metadata for boringtun Rust Foundation. Checked 2026-09-13.
  20. crates.io metadata for lol_html Rust Foundation. Checked 2026-09-13.
  21. Cloudflare, pingora README GitHub. Checked 2026-09-13.
  22. Cloudflare, quiche README GitHub. Checked 2026-09-13.
  23. Cloudflare, foundations repository GitHub. Checked 2026-09-13.
  24. Cloudflare, per-flag documentation tree GitHub. Checked 2026-09-13.
  25. Node.js, deprecated APIs OpenJS Foundation. Checked 2026-09-13.
  26. Fastly, js-compute-runtime CHANGELOG GitHub, release 3.19.0 dated 2024-07-29. Checked 2026-09-13.
  27. WinterTC, Standardization Update GitHub, 2024. Checked 2026-09-13.
  28. Vercel, edge-runtime repository GitHub. Checked 2026-09-13.