oven-sh/bun
Runtime
Active contributors: Jarred Sumner, Dylan Conway, Ciro Spaciari
The runtime is what makes bun ./script.ts, bun -e '...', and bun run start execute JavaScript and TypeScript. It owns the JavaScriptCore VM, the event loop, the module loader, the Web API surface, and the Node.js compatibility layer.
Purpose
Provide a Node.js-compatible JavaScript runtime that:
- Boots faster and uses less memory than Node by avoiding
napiindirection where possible. - Transpiles TypeScript, JSX, and TSX inline before evaluation.
- Implements Web APIs (
fetch,Request,Response,URL, streams,Blob,WebSocket) natively in Zig/C++ instead of in user-land JavaScript. - Exposes Bun-specific APIs (
Bun.serve,Bun.spawn,Bun.file,Bun.$,Bun.s3,Bun.sql,Bun.SQLite, ...) backed by native code.
Directory layout
src/
├── bun.js/
│ ├── VirtualMachine.zig # the runtime root struct (one per realm)
│ ├── ModuleLoader.zig # imports + require + dynamic import
│ ├── RuntimeTranspilerCache.zig # disk cache for parsed modules
│ ├── RuntimeTranspilerStore.zig # parallel transpile workers
│ ├── HardcodedModule.zig # registry of built-in module IDs
│ ├── ConsoleObject.zig # console.log + util.inspect formatter
│ ├── Debugger.zig # WebSocket inspector protocol
│ ├── ipc.zig # process.send / parent-child IPC
│ ├── hot_reloader.zig # --watch / --hot
│ ├── web_worker.zig # Worker threads
│ ├── event_loop.zig # task queue + microtask drain
│ ├── api/ # Bun.* namespace (server, ffi, ...)
│ ├── webcore/ # fetch, Blob, Request, Response, streams
│ ├── node/ # Node compat in Zig (fs, net, dns, ...)
│ ├── bindings/ # C++ JSC glue
│ ├── modules/ # _NativeModule entries
│ └── webview/ # Embedded webview (experimental)
├── cli/run_command.zig # bun run / bun ./script
├── cli/repl_command.zig # bun repl
└── js/ # JS/TS code that ends up bundled into the binaryKey abstractions
| Type | File | Purpose |
|---|---|---|
VirtualMachine |
src/bun.js/VirtualMachine.zig |
Root struct. Owns the JSC JSGlobalObject, the transpiler, the event loop, the module loader, the timer, hot-reload state, IPC, the source-map table. One per realm. |
ZigGlobalObject |
src/bun.js/bindings/ZigGlobalObject.cpp |
The JSC::JSGlobalObject subclass that JSC sees. Caches Bun-specific structures and ties Zig and JSC together. |
EventLoop |
src/bun.js/event_loop.zig |
Macro-task queue, microtask drain, deferred tasks across threads. Wraps uSockets / libuv. |
ModuleLoader |
src/bun.js/ModuleLoader.zig |
Implements import, require, import(). Dispatches to native loaders for hardcoded modules; resolves user code through src/resolver/. |
Transpiler |
src/transpiler.zig |
High-level wrapper around the parser + printer + sourcemap pipeline. The VM holds one. |
RuntimeTranspilerCache |
src/bun.js/RuntimeTranspilerCache.zig |
On-disk content-addressed cache of transpiled bytes (~/.bun/install/cache/). |
Strong / Weak |
src/bun.js/Strong.zig, Weak.zig |
Roots for JS values that outlive a single function call. |
WebWorker |
src/bun.js/web_worker.zig |
One worker thread = one full VirtualMachine instance. |
HotReloader |
src/bun.js/hot_reloader.zig |
Coordinates --watch and --hot; owns the file watcher subscription and re-evaluation logic. |
How a script runs
sequenceDiagram
participant CLI as src/cli/run_command.zig
participant VM as VirtualMachine.zig
participant Loader as ModuleLoader.zig
participant Cache as RuntimeTranspilerCache.zig
participant Parser as js_parser.zig
participant JSC as JavaScriptCore
participant EL as EventLoop
CLI->>VM: VirtualMachine.create(allocator, args)
VM->>JSC: ZigGlobalObject::create
VM->>Loader: load entry path
Loader->>Cache: lookup by content hash
alt cache miss
Loader->>Parser: parse to AST
Parser-->>Loader: AST + sourcemap
Loader->>Cache: write transpiled bytes + sourcemap
else cache hit
Cache-->>Loader: transpiled bytes
end
Loader->>JSC: JSModuleLoader::link + evaluate
JSC-->>VM: top-level await Promise
VM->>EL: drainMicrotasks + tick
loop until idle or process.exit
EL->>EL: poll uSockets / libuv
EL->>JSC: drainMicrotasks
end
VM-->>CLI: exit codeThe VM does not block on evaluate. Top-level await returns a Promise; the loop continues turning until the promise settles, all timers and listeners are gone, or process.exit is called.
Module loading
import and require flow through ModuleLoader.zig. The loader recognises three kinds of specifier:
- Hardcoded module —
node:fs,bun:sqlite, etc. Looked up inHardcodedModule.zigand dispatched to a native loader (Zig or C++). Some hardcoded modules are implemented in TypeScript undersrc/js/and bundled into the binary at build time bysrc/codegen/bundle-modules.ts. - NPM package —
lodash,react/jsx-runtime, etc. Resolved viasrc/resolver/resolver.zigagainstnode_modules/, applyingpackage.jsonexports/imports/main/module/browser. Auto-install can fetch missing packages on demand ifbunfig.tomlenables it. - Relative or absolute path —
./foo.ts,/abs/path.js,file:///.... Read from disk and transpiled.
CommonJS interop is handled in src/bun.js/bindings/CommonJSModuleRecord.cpp plus the parser's CJS detection. CJS modules expose a default export that aliases module.exports and let named imports work for static-analyzable shapes.
The transpiler cache (RuntimeTranspilerCache.zig) keys by (blake3_hash(source), parser_options_hash) and stores bytes under ~/.bun/install/cache/v1/jsc/. A second invocation of the same script skips parsing entirely on a cache hit.
See Module resolution and Built-in JS modules.
Bun.* API surface
The Bun global is built up in src/bun.js/api/BunObject.zig. Each property is one of:
- A native function (
Bun.spawn,Bun.write,Bun.file,Bun.gzipSync,Bun.deepEquals, ...). - A class constructor (
Bun.S3Client,Bun.SQLite,Bun.SQL,Bun.Glob, ...). - A getter (
Bun.argv,Bun.env,Bun.version).
The largest entries are:
Bun.* API |
Implementation |
|---|---|
Bun.serve (HTTP/WebSocket server) |
src/bun.js/api/server.zig (~170 KB) |
Bun.spawn / Bun.spawnSync |
src/bun.js/api/bun/process.zig |
Bun.file / Bun.write |
src/bun.js/webcore/Blob.zig (~189 KB) |
Bun.SQLite |
src/bun.js/bindings/sqlite/ |
Bun.SQL (Postgres / MySQL) |
src/sql/ + src/bun.js/api/sql.classes.ts |
Bun.S3 / Bun.S3Client |
src/s3/ + src/bun.js/webcore/S3Client.zig |
Bun.HTMLRewriter |
src/bun.js/api/html_rewriter.zig (lol-html wrapper) |
Bun.FFI (bun:ffi) |
src/bun.js/api/ffi.zig (~100 KB) + src/bun.js/api/FFIObject.zig |
Bun.$ (shell) |
src/shell/ |
Bun.Transpiler |
src/bun.js/api/JSTranspiler.zig |
Bun.Glob |
src/bun.js/api/glob.zig |
Bun.Cron |
src/bun.js/api/cron.zig |
Web APIs
src/bun.js/webcore/ implements the fetch/Request/Response/Blob/URL/Headers family plus the WHATWG Streams machinery:
| Web API | File |
|---|---|
fetch |
src/bun.js/webcore/fetch.zig |
Request, Response, Body |
Request.zig, Response.zig, Body.zig |
Blob, File |
Blob.zig |
Headers |
src/http/Headers.zig |
ReadableStream etc. |
streams.zig + builtins in src/js/builtins/ |
TextEncoder, TextDecoder, TextEncoderStream |
TextEncoder.zig, TextDecoder.zig, TextEncoderStreamEncoder.zig |
crypto.subtle |
Crypto.zig (uses BoringSSL) |
URL |
C++ binding into JSC's URL implementation; see Glossary for the host/hostname swap quirk. |
Cookies (Bun.Cookie, Bun.CookieMap) |
CookieMap.zig |
The Streams implementation is a hybrid: the public API (ReadableStream, WritableStream, TransformStream) is JSC builtins copied/adapted from WebKit (src/js/builtins/). Native sinks (FileSink, ArrayBufferSink, BunHTTPResponseSink, ...) are generated from src/codegen/generate-jssink.ts.
Node.js compatibility
Two layers cooperate:
- TypeScript implementations under
src/js/node/. These are the public ESM modules exposed asnode:fs,node:path,node:stream, etc. They are bundled into the binary at build time. Each file uses a domain-specific syntax ($cpp,$lazy,$bundled,$native) handled bysrc/codegen/builtin-parser.tsandsrc/codegen/bundle-modules.ts. - Native modules under
src/bun.js/node/. Performance-critical APIs (fs.read,dgramsocket internals,cryptohash classes,assert,net.connect's low-level path) are written in Zig and exposed as private bindings that the TS layer calls into.
The split is per-API: node:path is pure TS, node:fs.read calls a native binding, node:net uses native sockets via uSockets.
The set of supported APIs is tracked in test/js/node/ and the long-running node:test compat suite (bun run node:test).
Event loop and tasks
event_loop.zig exposes one main entry: tick(). A tick:
- Pulls all queued macrotasks off the
tasksqueue and runs them. - Drains microtasks via JSC.
- Polls uSockets (POSIX) or libuv (Windows) until something ready or all sources idle.
- Moves "next-tick" tasks (Node compat) into the macrotask queue.
Cross-thread events (HTTP responses, FS watcher events, worker postMessage) post to concurrent_tasks and signal the loop to wake.
The Timer subsystem (src/bun.js/api/Timer.zig) is built on a treap keyed by deadline. setTimeout, setInterval, setImmediate are all Timer flavors.
See Event loop.
Worker threads
new Worker(url) instantiates web_worker.zig::WebWorker, which:
- Spawns an OS thread.
- Creates a fresh
VirtualMachineandZigGlobalObjecton that thread. - Loads the worker script via the same
ModuleLoaderpipeline. - Sets up a
MessagePortpair using JSC structured-cloning.
The parent and worker share nothing except message channels; each has its own event loop.
Hot reload and watch
bun --watch script.ts and bun --hot route through hot_reloader.zig. The reloader:
- Subscribes to the file watcher (
src/Watcher.zig). - On a change, marks affected modules dirty in the VM's transpiler cache.
- For
--watchit tears down the VM and re-runs the entry script. - For
--hotit replaces only the changed module and re-runs the side effects.
Bake (the dev server) uses a different, more sophisticated HMR runtime in src/bake/client/hmr-*.ts. See Bake.
Integration points
- CLI — Created and torn down by
src/cli/run_command.zig,src/cli/repl_command.zig,src/cli/test_command.zig. - Bundler —
bun builddoes not start a VM unless plugins are configured; when it does, the sameVirtualMachineruns plugin code. - Package manager —
bun installrunspreinstall/postinstallscripts viasrc/install/lifecycle_script_runner.zig, which spawns a childbunrather than reusing the VM. - HTTP — Server requests dispatch into JS via
src/bun.js/api/server.zig; outgoingfetchposts toHTTPThread.zig. - Workers —
web_worker.zigreuses the VM machinery on a fresh thread.
Entry points for modification
- To add a Bun API, edit
src/bun.js/api/BunObject.classes.ts(signature) and a new or existing*.zigfile insrc/bun.js/api/. - To add a
node:*module, drop the TS file insrc/js/node/<name>.ts. Add an entry tosrc/js/CLAUDE.md's registry if it's new. Native bindings live insrc/bun.js/node/. - To change parsing behavior at evaluation time, edit
src/bun.js/RuntimeTranspilerStore.zigorsrc/transpiler.zig. Cache invalidation is keyed inRuntimeTranspilerCache.zig. - To add a Web API, prefer
src/bun.js/webcore/. Generate the host class via*.classes.tsandsrc/codegen/generate-classes.ts.
Key source files
| File | Purpose |
|---|---|
src/bun.js/VirtualMachine.zig |
Realm root; ~4,100 lines. |
src/bun.js/ModuleLoader.zig |
Module load + link + evaluate. |
src/bun.js/RuntimeTranspilerCache.zig |
On-disk transpile cache. |
src/bun.js/event_loop.zig |
Tick + microtasks. |
src/bun.js/api/BunObject.zig |
Bun.* namespace. |
src/bun.js/webcore/Blob.zig |
Bun.file, Blob, File. |
src/bun.js/api/server.zig |
Bun.serve HTTP/WebSocket server. |
src/cli/run_command.zig |
The bun run / bun ./file entry. |
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.