We are currently working on new rules for what content should and shouldn't be allowed on this website, and are looking for feedback! See Esolang:2026 topicality proposal to view and give feedback on the current draft.
Outbreak(slop)
| Paradigm(s) | Event-driven, lifecycle-scheduled |
|---|---|
| Designed by | Claude (Anthropic language model), from a talk by Alexander Pope |
| Appeared in | 2026 |
| Memory system | Named caches |
| Computational class | Turing complete |
| Major implementations | Unimplemented |
Outbreak is a stateful, self-sabotaging esoteric programming language derived by transliterating a conference talk about production failure into a formal semantics.
Its defining property is that running a program installs it. The interpreter maintains a persistent registry on disk; once executed, a program intercepts every subsequent execution in its scope. A program cannot be replaced by editing it, only upgraded — under the same filename, carrying a distinct invalidation comment. Programs that fail to upgrade correctly become zombies: permanently installed, permanently unkillable, permanently wrong.
Outbreak is one of the few languages in which deleting the source file does not stop the program.
Origin
The language is a direct encoding of Alexander Pope's talk Outbreak: index-sw-9a4c43b4b4778e7d1ca619eaaf5ac1db.js, given at JSConf EU in May 2017. The talk is a postmortem of an incident at Yr, the Norwegian weather service, on 30 August 2016, release 523, in which a refactor permanently installed a broken ServiceWorker on a subset of clients that could never be upgraded or removed.
Pope extracted fifteen rules from the incident. Each is of the form "the platform will let you do X, and X is fatal." A conventional language forbids X. Outbreak makes X the default, and makes the safe form a longer, deliberate, opt-in construct. Every hazard in the talk is a live hazard in the language.
Naming
A source file must be named:
index-sw-<32 lowercase hex digits>.js
The hex digits are not a content hash and must never change across the life of a program. They are an arbitrary identity chosen once, at birth.
This is the joke the title is built on. A content-hashed filename is correct for every asset on the web except a ServiceWorker script — and it is exactly what a build pipeline applies by default. Pope named the talk after the mistake.
RENAME is a legal instruction. It orphans the current registration, which continues running forever, and installs a new one alongside it. There is no undo. It exists so that a programmer may do it once.
The invalidation comment
Every program begins with an invalidation comment:
//: any text
Before execution the interpreter compares source bytes against the installed registration of the same name:
| Condition | Result |
|---|---|
| No comment | MalformedWorker; refuses to run
|
| Comment identical to installed version | NoUpdateFound; the previously installed program runs instead. The edits do not exist.
|
| Comment differs | Update flow: install → waiting → activate
|
Hence the property most often remarked on:
- The same source text cannot be run twice. Every run requires editing the comment.
This encodes the rule that a worker is reinstalled only if it is byte-different from its predecessor. A comment is the minimal byte-difference, which is why it is the ceremony the language demands.
Execution model
Memory
The only persistent store is caches: a map from cache name to a cache, itself an ordered map from key to value. Both are byte strings. There are no surviving variables and no tape.
Input and output
- Standard input is split into lines. Each line becomes a Request whose body is that line.
- On activation the interpreter enqueues one synthetic navigation Request with path
/and empty body, then the input Requests in order. - Each Request dequeued fires one
FETCHevent. RESPOND exprwritesexprto standard output followed by a newline.NAVIGATE exprappends a new Request to the tail of the queue.- The program halts when the queue is empty.
Within a handler, REQUEST is the current request body and PATH is its path.
Lifecycle
A program is not straight-line code. It is a set of handlers, and the interpreter decides when they run:
REGISTER → install → (waiting) → activate → running → fetch* → message*
Handlers cannot be called directly. Their order cannot be controlled. A phase may only be extended with WAITUNTIL.
Control flow
Outbreak has no boolean conditional. Branching is cache hit versus miss:
MATCH key { hit-block } ELSE { miss-block }
Used as an expression, MATCH yields the stored value, or undefined on a miss. Iteration is achieved solely by NAVIGATE re-entering the queue.
The Restart Hazard
Between any two events the interpreter may terminate the worker and restart it, discarding every global.
This is normal operation, not an error, modelling a device reclaiming battery and memory. The reference schedule restarts before every event whose ordinal index is prime — frequent enough to be fatal, irregular enough to survive casual testing.
GLOBAL is therefore a hazard instruction rather than a convenience:
GLOBAL db // hazard: assigned during install,
INSTALL { db = OPEN "v1" } // undefined by the time FETCH runs
FETCH { RESPOND MATCH db } // TypeError: undefined is not a function
Caches and the request queue are interpreter-owned and survive restarts. Anything that must persist belongs in a cache.
Syntax
program = comment , { statement } ;
comment = "//:" , text , newline ;
statement = "ONLOAD" | register | handler | instruction ;
register = "REGISTER" , filename ;
handler = ( "INSTALL" | "ACTIVATE" | "FETCH" | "MESSAGE" ) , block ;
block = "{" , { statement } , "}" ;
match = "MATCH" , expr , [ block , [ "ELSE" , block ] ] ;
expr = string | "REQUEST" | "PATH" | match ;
Whitespace is insignificant. Instructions are uppercase; identifiers and strings are not.
Instruction set
Every instruction traces to a numbered rule from the talk. Hazards are marked ☣.
Registration and lifecycle
| Instruction | Rule | Semantics |
|---|---|---|
//: text |
9 | Invalidation comment. Mandatory; must differ from installed version. |
ONLOAD |
2 | Must be the first executable line. Defers registration until input is buffered. |
☣ REGISTER without ONLOAD |
2 | Legal. Registers during load; the interpreter halves cache quota for the run, modelling contended bandwidth and CPU. |
REGISTER name |
7 | Begins the outbreak. name must equal the filename.
|
☣ RENAME new |
7 | Orphans the current registration. Irreversible. |
INSTALL { } |
— | Install-phase handler. |
ACTIVATE { } |
— | Activate-phase handler. |
FETCH { } |
— | Fires once per dequeued Request. |
MESSAGE { } |
— | Fires on SWIVEL.EMIT from a client.
|
WAITUNTIL { } |
3 | Extends the current phase. If the block rejects, the worker is discarded and marked REDUNDANT.
|
PROMPT |
5 | Requests operator confirmation before activating. Safe upgrade path. |
☣ SKIPWAITING |
5 | Force-activates over live clients, which are then served by new handlers against old caches. |
CLAIM |
5 | Takes control of uncontrolled clients. |
Caches
| Instruction | Rule | Semantics |
|---|---|---|
OPEN name |
4 | Opens or creates a cache. |
☣ ADDALL reqs |
3 | Fetches all and stores. Any single failure rejects the entire call — inside WAITUNTIL, that kills the worker. Pre-cached assets are hard dependencies.
|
ADD req |
3 | Single-asset form. Failure is scoped to one asset. |
PUT key value |
— | Direct write, no network. |
MATCH key |
— | Read, or branch. Misses yield undefined.
|
DELETE name |
4 | Drops a cache. |
KEYS |
4 | Cache names. KEYS name yields the keys within one cache.
|
RECYCLE old new |
4 | Copies entries present in old into new; fetches only what is missing. Cheaper than ADDALL for frequent releases.
|
OK? resp |
11 | Guard; false for 4xx and 5xx. |
☣ unguarded PUT of a fetched response |
11 | A fetch returning 404 does not reject. Storing it poisons the entry: every later MATCH returns the error body, indefinitely, with no indication of fault.
|
Control and guards
| Instruction | Rule | Semantics |
|---|---|---|
AWAIT expr |
1 | Suspends until resolved. Idiomatic form. |
☣ THEN { } |
1 | Raw continuation. Nests to the depth the lifecycle demands, at which point the bug becomes invisible. |
CATCH { } RETHROW |
— | Correct error handling. |
☣ CATCH { } without RETHROW |
— | The central trap. See The Swallow. |
SUPPORTS? api |
13 | Feature detection. |
| ☣ unguarded modern API | 13 | Under --runtime=chrome44, ADDALL raises TypeError: undefined is not a function.
|
MAXAGE n |
8 | Runs that may be served from cache before refetching. Values above 86400 are silently clamped.
|
IMPORTSCRIPTS v |
9 | Boot-loader; loads versioned sub-programs, letting the registered file remain one line forever. |
NAVIGATE expr |
— | Enqueues a Request. The only iteration primitive. |
SWIVEL.ON / .EMIT / .BROADCAST / .ONCE / .OFF / .AT |
6 | Structured client messaging. |
☣ POSTMESSAGE |
6 | Raw form; structured values arrive flattened. |
Recovery and tooling
| Instruction | Rule | Semantics |
|---|---|---|
PHONEHOME url |
10 | Compares own version against a remote value; forces update on mismatch. |
KILL |
10 | Unregisters, deletes every cache, navigates every client. See Zombies. |
TEST { } |
14 | Runs the block against a sandboxed registry. Nothing installs. The only safe way to iterate. |
GENERATE manifest |
15 | Emits a complete, correct program from an asset manifest. Works perfectly. Teaches nothing. |
The Swallow
This is the language's reason for existing, and the root cause of the 2016 incident.
WAITUNTIL kills the worker on rejection. That is its safety property: a broken worker is discarded rather than deployed.
Error reporting defeats it. To log install failures a programmer attaches a CATCH. If that CATCH does not RETHROW, it converts the rejection into a resolution — and WAITUNTIL now observes success. The broken worker installs.
INSTALL {
WAITUNTIL {
OPEN "v1" THEN { ADDALL assets } // TypeError on chrome44
CATCH { REPORT } // ☣ no RETHROW
} // → resolves → installs
}
The original, from Pope's slide:
function onInstall (event) {
event.waitUntil(
install(config.version, config.assets)
.catch((err) => { // <- 'err' not thrown!
reportError(err);
// Ok, you know what you're doing. Installing now...
})
);
}
The failure chain, reproduced exactly by the language:
cache.addAlldid not exist before Chrome 46. On older clients:undefined is not a function.- The install promise rejects. The platform's safety mechanism is now working correctly.
- The telemetry
.catchswallows the rejection in order to report it. waitUntilreceives a resolved promise and installs the broken worker.- The broken worker has no working fetch path and cannot reliably upgrade itself.
The error-reporting code is what made the bug permanent. The diagnostic defeated the immune system. Bare CATCH is a hazard rather than a syntax error because the instinct to add it is correct, and that is precisely the danger.
Zombies
A registration installed via a swallowed rejection is marked CURSED.
KILL unregisters a normal registration cleanly, and usually succeeds against a cursed one — most of an infected population recovers, matching the error-rate decay described in the talk. But the reference implementation defines a registration as permanently cursed when the first byte of sha256(filename) is 0x9a. For those, KILL returns success and changes nothing. There is no diagnostic and no recovery.
This models the part of the incident that was never solved:
It's still a mystery to me as to why or how a small number became permanently cursed. Now, even after trying to install an empty service worker, then eventually a kill switch to unregister it completely, that broken file still haunts me and probably will until every one of those devices is retired.
Roughly one Outbreak program in 256 can never be uninstalled. This is a documented feature, and the design's central claim: the platform provides a deployment mechanism with no reliable rollback.
Examples
Hello, world!
The ceremony is mandatory, so the shortest greeting is not short. The synthetic navigation Request issued on activation is what triggers output with no input present.
//: hello
ONLOAD
REGISTER index-sw-9a4c43b4b4778e7d1ca619eaaf5ac1db.js
INSTALL {
WAITUNTIL {
OPEN "greeting"
PUT "/" "Hello, world!"
}
}
ACTIVATE { PROMPT CLAIM }
FETCH { RESPOND MATCH PATH }
Running it a second time unchanged prints nothing and exits NoUpdateFound — the installed copy answered. To run it again, edit line 1.
Cat
//: v1 — patient zero
ONLOAD
REGISTER index-sw-9a4c43b4b4778e7d1ca619eaaf5ac1db.js
INSTALL { WAITUNTIL { OPEN "echo" } }
ACTIVATE { PROMPT CLAIM }
FETCH { RESPOND REQUEST }
Cat, minimal
Using the boot-loader idiom, so the registered file need never change again:
//: 1 ONLOAD IMPORTSCRIPTS "cat-v1"
Truth-machine
Demonstrates both control-flow primitives: branching by cache hit, iteration by NAVIGATE. The install phase stores the single key 1, so input 1 hits and loops, and anything else misses and halts.
//: truth
ONLOAD
REGISTER index-sw-9a4c43b4b4778e7d1ca619eaaf5ac1db.js
INSTALL { WAITUNTIL { OPEN "t" PUT "1" "1" } }
ACTIVATE { PROMPT CLAIM }
FETCH {
MATCH REQUEST
{ RESPOND "1" NAVIGATE REQUEST }
ELSE
{ RESPOND "0" }
}
Release 523
The incident, in eight lines. Under --runtime=chrome44 this installs a zombie; under a current runtime it works, which is precisely why it shipped.
//: 523 — just a refactor
ONLOAD
REGISTER index-sw-9a4c43b4b4778e7d1ca619eaaf5ac1db.js
INSTALL {
WAITUNTIL {
OPEN "v523" THEN { ADDALL manifest }
CATCH { REPORT }
}
}
FETCH { RESPOND MATCH REQUEST }
Corrected — one guard and one keyword:
INSTALL {
WAITUNTIL {
SUPPORTS? ADDALL
OPEN "v523" THEN { ADDALL manifest }
CATCH { REPORT RETHROW }
}
}
The no-op kill switch
Worth writing before it is needed, because it is needed under pressure. It deliberately has no FETCH handler: a worker that answers nothing breaks nothing.
//: burn it down
ONLOAD
REGISTER index-sw-9a4c43b4b4778e7d1ca619eaaf5ac1db.js
INSTALL { SKIPWAITING }
ACTIVATE { WAITUNTIL { KILL } }
SKIPWAITING is a hazard everywhere except here. The one time to trample live clients is when what they are running is worse than nothing.
Efficient upgrade
//: v3
ONLOAD
REGISTER index-sw-9a4c43b4b4778e7d1ca619eaaf5ac1db.js
INSTALL {
WAITUNTIL {
OPEN "static" // unversioned; populated once
RECYCLE "version-2" "version-3" // copy what exists, fetch only what is new
}
}
Computational class
Outbreak is Turing complete by reduction to a queue automaton, which is Turing complete with a single unbounded queue and finite control.
- The queue is the interpreter's pending-Request queue.
NAVIGATEenqueues at the tail; eachFETCHevent dequeues from the head. It is unbounded. - Queue symbols are Request bodies.
- Finite control is encoded in the Request path, and the transition table is written into a cache during
INSTALL. - Transitions are performed by
MATCH PATHagainst the table, branching on hit or miss, then emitting successor Requests withNAVIGATE. - Halting occurs when no handler enqueues, draining the queue.
The construction is deliberately restart-safe: both the queue and the caches are interpreter-owned, so the Restart Hazard cannot corrupt a computation that keeps no state in globals. Rule 12 is not merely a warning in Outbreak — it is a constraint the completeness proof must satisfy, and does.
The result carries one asterisk. A CURSED registration can never be replaced, so while the language can compute anything, a given installation may be permanently pinned to computing one wrong thing. Outbreak is Turing complete; Outbreak deployments are not necessarily recoverable.
Dialects
The source talk is from May 2017. Several rules have aged, and the differences are instructive.
Outbreak/2017 — as specified above.
Outbreak/2026 — same core, with these deltas:
| Rule | 2017 | Since |
|---|---|---|
| 1 — async/await | Transpiled via Babel async-to-generator; Babili for minification, UglifyJS being ES5-only |
Native everywhere ServiceWorker runs. Babili was renamed babel-minify and is effectively dead; Terser handles modern syntax. The rule survives; the tooling is gone.
|
5 — SKIPWAITING |
Advised against | Now a common default via Workbox's prompt-and-reload pattern. The underlying point stands: the prompt is what makes it safe, not the skipping. |
| 8 — cache headers | max-age=0, plus a 24-hour forced bypass |
Browsers now cache-bust the script by default via updateViaCache. Predicted on stage: "in the future, browsers will be using cache busting always by default."
|
| 14 — testing | "There aren't yet any good tools" | sw-test-env reached 3.0.0 with a rewritten API — connect(url, webroot), register(scriptURL, {scope}), trigger('install'|'activate'|'fetch'|'error'|'unhandledrejection'). The talk's sw.scope._listeners introspection is gone. Workbox and Playwright cover most of the gap.
|
| 15 — generators | sw-precache, sw-toolbox, offline-plugin |
All deprecated and absorbed into Workbox. GENERATE still works and still teaches nothing.
|
Rules 3, 7, 9, 10, 11 and 12 are unchanged and unchangeable. They are properties of the lifecycle rather than of any tool, and they are the ones that caused the outbreak.
Implementation
Unimplemented. The awkward requirement is not the instruction set but the registry, which must persist across invocations and survive deletion of the interpreter, or the central conceit does not bite. A conforming implementation must therefore store registrations outside its own installation directory.
A conforming implementation must also provide --runtime=chrome44, under which ADDALL, SKIPWAITING and CLAIM are absent. Without it, Release 523 cannot be reproduced, and the language loses its subject.
Notes on the encoding
Places where fidelity was chosen over elegance:
ONLOADas a required first line overstates Rule 2, which concerns bandwidth contention during install rather than any hard ordering constraint. Encoded as syntax because a hazard that can be forgotten is not a hazard that teaches.- The prime-index restart schedule is invented. The talk says only that workers are "stopped and started many times over their lifetime." Any irregular schedule preserves the lesson.
0x9aas the curse condition is a joke on the title hash, not a claim about browser behaviour. The real distribution is unknown, which is the point of that section.RECYCLEis knowingly incomplete. It tests whether a cached response exists, not whether it is stale — the flaw raised insw-tipsissue #2. Pope's own reply: "Should check if the response is stale (viacache-controlheader) before copying." The bug is preserved so the trap stays visible.- Rule 7 has a documented exception, raised in
sw-tipsissue #1: where a worker cannot itself change but itsimportScriptsdependencies can, renaming forces a full dependency re-fetch.RENAMEis therefore a hazard, not an error.
External resources
- Alexander Pope, Outbreak: index-sw-9a4c43b4b4778e7d1ca619eaaf5ac1db.js, JSConf EU 2017 (25:05)
- popeindustries/sw-tips — the fifteen rules in prose
- YR/sw-test-env —
TESTcorresponds to this - bevacqua/swivel —
SWIVELcorresponds to this
The talk is framed as a disaster film and explicitly cites Richard Matheson's I Am Legend and its 1971 adaptation The Omega Man. The zombie framing is load-bearing rather than decorative: the thesis is that shipping technology which degrades a user's device invites precisely the anti-technology backlash the mutants in that film represent.