Open-Source Wikis

/

Bun

/

Systems

/

Event loop

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 --> Wake

The 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.All instance from src/bun.js/api/Timer.zig.
  • The watcher thread's wake fd.
  • process.nextTick and queueMicrotask queues.

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:

  1. Add a variant to the Task union in Task.zig.
  2. Implement a run() for that variant in the appropriate site (often the source struct that produces the task).
  3. Schedule via event_loop.enqueueTask(...) or event_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 threadsweb_worker.zig instantiates an EventLoop per worker.

Entry points for modification

  • To add a task kind, edit Task.zig and the producer.
  • To change microtask scheduling order, edit EventLoop.drainMicrotasks in event_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.

Event loop – Bun wiki | Factory