Bun Wants Real Threads in JavaScript. Here's the Catch.
An experimental JavaScriptCore fork gives JS shared-memory threads with no global lock — and reopens a class of bugs the language never had.
JavaScript has spent its entire life pretending threads don't exist. Run-to-completion semantics, a single-threaded heap, an event loop you grudgingly learn to love — that's the deal you signed. Workers came along, but they're separate JS instances that talk by copying data across a wall. Now Bun's creator Jarred Sumner has opened a pull request against Bun's WebKit fork that breaks the deal entirely: shared-memory threads inside JavaScriptCore, where new Thread(fn) runs a closure on another core, in the same heap, touching the same objects.
This is the most ambitious thing anyone has attempted in a JS engine in years. It's also, by the author's own admission, experimental, possibly broken in ways the test suite can't yet see, and may never merge. Both things are true at once, and the gap between them is the whole story. My read: this is a genuine foundational shift in what the language could be — and precisely because it's foundational, it reopens a category of bug JavaScript developers have never had to think about. Watch it closely. Don't build on it yet.
What's actually new
The headline is not "Bun added threads." Bun already has Worker — it shipped back in v0.7.0, and the docs describe a competent, server-tuned implementation with fast paths that make postMessage 2–241× faster than Node for simple payloads. The headline is that this PR removes the isolation. There is one heap. A function spawned on another thread is a real closure: it sees the variables it closed over, the modules you imported, the classes you defined. If it throws, join() rethrows the actual exception object with the actual stack.
The API is almost insultingly small:
const t = new Thread((a, b) => expensive(a, b), x, y);
t.join(); // blocks, returns the value or rethrows
await t.asyncJoin(); // same, as a promise
Thread.current; // your Thread
t.id; // engine thread id; main is 0
No separate worker file. No blob: URL. No onmessage protocol. No bundler entry point. Compare that to the state of the art today, which is genuinely this cursed: stringify your function's source, wrap it in self.onmessage, shove it into a Blob, URL.createObjectURL the thing, and pray your "function" doesn't close over anything, call anything it imported, or reference a class it didn't define inline — because it's a string now.
The technically hard part is buried in the status note: parallel JavaScript executes "through all four JIT tiers, with no global lock," and the thread test suite passes that way. That sentence is doing an enormous amount of work. A JS engine's heap, garbage collector, and JIT all assume a single mutator thread. Letting two cores execute optimized machine code against a shared object graph without serializing them behind one big lock means reworking allocation, GC safepoints, inline caches, and the structure ("hidden class") machinery to be thread-safe. The honest caveats — thread-sanitizer cleanup, fuzzing, one benchmark over budget, a long soak still pending — tell you exactly how green this is. The locked fallback mode and the threads-disabled build remain untouched and verified, which is the right call: you don't ship a memory model on vibes.
Why this has never happened
Every mature language with threads paid for them in design. Java, C#, and Go were built around a memory model from day one — Go gave you goroutines and a scheduler, the JVM gave you volatile and synchronized and a specification for what a racing read is allowed to observe. JavaScript has none of that for ordinary objects. The only defined concurrency primitive is SharedArrayBuffer plus Atomics, and that's deliberately a flat slab of bytes with a tightly specified memory model — exactly because the committee didn't want to define what happens when two threads race on a normal property.
The closest analogy is Python's long march to remove the GIL. CPython's free-threading work (PEP 703) took years precisely because "just remove the global lock" cascades into reference counting, the object allocator, and every C extension's assumptions. Bun's PR is JavaScript's version of that fight, with an extra twist: Python at least had threads behind the GIL. JS is going from zero shared mutable state to all of it. That's why "no global lock" is the flex — a global lock would have made this comparatively easy and comparatively useless, since you'd be back to serialized execution.
It's worth situating this in Bun's broader arc. The runtime is mid-rewrite from Zig to Rust, reportedly hitting 99.8% test compatibility on Linux x64 glibc, and Bun was acquired by Anthropic — Claude Code ships as a Bun executable. A company whose flagship product is Bun has both the motivation and the runway to attempt engine-level surgery that a weekend contributor never could. JavaScriptCore, notably, isn't changing in the Rust rewrite; it stays the execution engine. This PR is where Bun's ambitions actually touch the engine.
What it means if you write server-side JS
The target audience is narrow and real: people running CPU-bound work in a JS process. Think bulk transforms, parsing and serialization, image and video munging, embedding/feature pipelines, search over big in-memory structures. Today your options are all bad in specific ways, and the PR's examples map cleanly onto them:
- Parallel map over 100k objects. Today: chunk the input into per-worker messages, reassemble N result arrays, eat the structured-clone cost both ways. With threads: eight workers, an
Atomics.addcounter on a plain property, each writing straight into one sharedresultsarray. Nothing is copied, ever. - A shared cache. Today you pick your poison: N workers with N private caches recomputing each other's entries, a "cache server" worker behind
postMessageround-trips, or hand-rolling a hashmap with string interning and an allocator on top ofSharedArrayBuffer. People have shipped all three. None of them is aMap. The PR's version is aMapplus aLock. - Cancellation and progress. A
{ stop: false }flag the winning thread flips; a{ done, total }counter the main thread just reads on an interval. No termination protocol, nopostMessage({type:'progress'})events, no throttling so you don't flood the channel.
Here's the trade-off nobody should gloss over: you are now writing concurrent code, with all that implies. The examples lean on Lock, Condition, and Atomics because they have to. The moment two threads touch the same object without coordination, you have a data race — and JavaScript has no memory model that tells you what a racy read returns. That's a class of Heisenbug — torn reads, stale caches, lost updates — that JS developers have been structurally immune to for thirty years. Workers are clumsy, but their isolation is also a guarantee: nothing the worker does can corrupt your heap. Delete the wall and you delete the guarantee. The discipline that C, Rust, and Go programmers internalize — who owns this data, what lock guards it, is this read atomic — becomes table stakes for anyone who reaches for new Thread.
The verdict
If it pans out, this is the biggest expansion of JavaScript's capabilities since async/await, and it lands first in Bun — a meaningful moat over Node's worker_threads and Deno, neither of which can run a real closure on another core. The pitch is honest, the engineering is serious, and "tests pass through all four JIT tiers with no global lock" is not a claim you make lightly.
But "tests pass" and "safe under a fuzzer and a multi-day soak" are different universes, and the author says so plainly — this exists to be read and argued with, and it may never merge. Even in the best case, shipping it means convincing the wider ecosystem to accept shared mutable state, or quarantining it as a Bun-only superpower that fragments what "JavaScript" means. For now: read the PR, understand what it implies for the workloads you actually run, and keep your concurrency where the engine can't surprise you. The most interesting JS systems work in years is happening in the open. That's the reason to watch — and the reason not to deploy it.
Sources & further reading
- Bun has an open PR adding shared-memory threads to JavaScriptCore — github.com
- Why Bun is Rewriting in Rust (And What It Means for JavaScript Developers) - DEV Community — dev.to
- spike.news - simple news aggregator — spike.news
- Workers - Bun — bun.com
- Bun v0.7.0 | Bun Blog — bun.sh
Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.
Discussion 1
i'm curious how this will handle backfills and data consistency, especially when dealing with concurrent updates to shared objects - does the experimental fork provide any mechanisms for locking or transactional updates?