oven-sh/bun
Event loop
Bun runs a single-threaded event loop per JavaScript realm — same model as Node.js. The implementation is in src/bun.js/event_loop.zig plus the smaller helpers under src/bun.js/event_loop/.
Files
| File | Purpose |
|---|---|
src/bun.js/event_loop.zig |
The main EventLoop struct (~26 KB). |
src/bun.js/event_loop/Task.zig |
The Task tagged union (~28 KB). One variant per kind of work. |
src/bun.js/event_loop/ConcurrentTask.zig |
Cross-thread task queueing. |
src/bun.js/event_loop/AnyTask.zig, AnyTaskWithExtraContext.zig, ManagedTask.zig |
Adapters for tasks coming from non-Zig owners. |
src/bun.js/event_loop/CppTask.zig |
Tasks scheduled from C++ (scheduleAsync style). |
src/bun.js/event_loop/MiniEventLoop.zig |
A reduced loop used by bun build, bun install, and other commands that don't need a full VM. |
src/bun.js/event_loop/AnyEventLoop.zig |
Sum type that abstracts the full loop, the mini loop, and the worker loop. |
src/bun.js/event_loop/EventLoopHandle.zig |
A handle into whichever loop is current. |
src/bun.js/event_loop/JSCScheduler.zig |
Bridge so JSC microtasks tick on the loop. |
src/bun.js/event_loop/GarbageCollectionController.zig |
Runs JSC GC on a schedule when idle. |
src/bun.js/event_loop/PosixSignalHandle.zig |
POSIX signal-safe handoff to the loop. |
src/bun.js/event_loop/SpawnSyncEventLoop.zig |
A loop instance dedicated to Bun.spawnSync. |
src/bun.js/event_loop/DeferredTaskQueue.zig |
Tasks deferred to "after this tick". |
src/bun.js/event_loop/WorkTask.zig |
Wraps an off-thread job that resolves a JS promise. |
src/bun.js/event_loop/README.md |
In-tree design notes. |
What's in the loop
graph LR
Wake[wake source: I/O, timer, postMessage] --> Loop[EventLoop.tick]
Loop --> Tasks[drain task queue]
Tasks --> Macro[Bun macrotasks]
Tasks --> Cpp[CppTask queue]
Loop --> Micro[JSC microtasks]
Micro --> Promises[then/catch/finally]
Loop --> NextTick[Node nextTick]
Loop --> Defer[deferred queue]
Loop --> Idle{idle?}
Idle -->|no| Loop
Idle -->|yes| Poll[uSockets/libuv poll]
Poll --> WakeThe loop owns:
- A queue of pending macrotasks (
tasks). - A queue of cross-thread tasks (
concurrent_tasks) signalled via a wake fd. - A queue of tasks deferred to after the current tick (
deferred_tasks). - A microtask drain hook into JSC.
- The
Timer.Allinstance fromsrc/bun.js/api/Timer.zig. - The watcher thread's wake fd.
process.nextTickandqueueMicrotaskqueues.
The Task union
Task.zig enumerates every kind of work the loop can schedule. As of generation it includes ~60 variants — fetch responses, FS reads, FS watches, timers, subprocess events, postMessage payloads, GC ticks, transpile completions, network task results, server-rendered chunks, etc.
Adding a new task kind:
- Add a variant to the
Taskunion inTask.zig. - Implement a
run()for that variant in the appropriate site (often the source struct that produces the task). - Schedule via
event_loop.enqueueTask(...)orevent_loop.enqueueTaskConcurrent(...).
Microtask draining
After every macrotask, the loop calls JSC to drain the microtask queue. This ensures Promise.resolve().then(...) callbacks run in the right order and that await resumes promptly without an extra trip through setImmediate.
process.nextTick is its own queue at a slightly higher priority than JSC microtasks, mirroring Node's semantics.
Cross-thread
Workers (HTTP thread, bundler thread pool, install thread pool, watcher thread, web_worker.zig worker threads) post results via ConcurrentTask.zig. A ConcurrentTask is a node in an MPMC queue read by the main loop. The queue's wake-up uses an eventfd (Linux), a self-pipe (BSDs/macOS), or WakeByAddressSingle (Windows).
Mini event loop
Some commands don't need a full JS realm — bun install, bun build (without plugins), bun pack. They run on a MiniEventLoop (MiniEventLoop.zig). It implements just enough of the API surface for bun.spawn, async file I/O, and the HTTP thread to post back results. Tasks that target the mini loop are typed via AnyEventLoop.zig.
Garbage collection
GarbageCollectionController.zig schedules JSC's incremental GC as the loop goes idle. This avoids long GC pauses during request bursts. The controller exposes tunables that show up in bunfig.toml under [run].
Bun.spawnSync
Bun.spawnSync runs a child process synchronously without giving up the event loop entirely. It uses a dedicated SpawnSyncEventLoop (SpawnSyncEventLoop.zig) that ticks only the I/O backing the child's pipes — the JS event loop above stays parked.
Integration points
- JSC — JSC drains microtasks; we drive that drain.
- uSockets / libuv — Owns the I/O readiness loop; we feed it our timers and tasks.
- HTTP thread — Has its own tiny loop for outgoing HTTP; replies post back as
ConcurrentTasks. - Worker threads —
web_worker.ziginstantiates anEventLoopper worker.
Entry points for modification
- To add a task kind, edit
Task.zigand the producer. - To change microtask scheduling order, edit
EventLoop.drainMicrotasksinevent_loop.zig. - To change idle / GC behaviour, edit
GarbageCollectionController.zig.
Key source files
| File | Purpose |
|---|---|
src/bun.js/event_loop.zig |
Main loop. |
src/bun.js/event_loop/Task.zig |
Task variants. |
src/bun.js/event_loop/ConcurrentTask.zig |
Cross-thread queue. |
src/bun.js/event_loop/MiniEventLoop.zig |
VM-less loop for bun install/bun build. |
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.