← Back
Home › Systems › Operating systems and runtimes

js-runtime

Embeds a JavaScript engine in a host program that adds files, sockets and timers around an event loop, so developers can run JavaScript on servers.

Node, Bun

Open the js-runtime pagejs-runtime Calling conventions abicompilers: System V, Windows x64 and AAPCS64 ABIs: argument registers, stack frames, struct passing, FFIDependency version resolutioncompilers: Pick package versions that satisfy semver constraints (PubGrub, SAT) with readable conflict errorsHeap snapshot startupcompilers: Serialize an initialized VM heap and deserialize at startup to cut boot timeJavascript event loopcompilers: Task and microtask queues, rendering steps, timers and promise job ordering in JS hostsModule resolutioncompilers: Resolve import specifiers to files: Node and ESM resolution, package exports, CJS interopWork-stealing dequeconcurrency: Per-worker deque: the owner pushes and pops at the bottom without contention, idle workers steal from the top; the core of Rayon, Tokio and Go.›d-ary heapdata-structures: Array heap with d children per node (d = 4 typical): half the binary heap's depth, siblings in one cache line, faster priority queues.›Hdr histogramdata-structures: Log-linear bucketed histograms with bounded relative error for latency percentilesDeterministic simulationtestingdistributed: Run the whole system in one thread under a seeded scheduler with simulated network, disk and clock, so every failure replays from its seed.›Primary-backup replicationdistributed: One node orders all writes and ships them to standbys; the acknowledgement rule (none, one, all) sets what a crash loses, and an external arbiter decides who takes over.›Buffer pools forzero-allocation I/Ohardware-os: Preallocate a fixed set of aligned I/O buffers and recycle them through a free list, so the hot path never mallocs, faults or unmaps. The bounded count doubles as back pressure.›File system watchinghardware-os: Treat OS change notices as hints, debounce them, and rescan and diff a snapshot whenever notices overflow or may be lost.›Process supervisionhardware-os: A parent process starts services, reaps their exits and restarts them under a delay and burst-limit policy, then stops them with SIGTERM before SIGKILL.›epoll event loopnetworking: One thread waits on thousands of non-blocking sockets with epoll_wait and handles only ready ones; the core of nginx, Redis and tokio.›HTTP/1.1 message parsingnetworking: Parse HTTP/1.1 requests and responses incrementally from a byte stream: RFC 9112 body-length rules, chunked decoding and the strictness that stops request smuggling.›io_uringnetworking: Linux async I/O through two shared-memory rings (submissions in, completions out), batching or eliminating syscalls for sockets and files alike.›WebSocketsnetworking: Full-duplex message framing over one upgraded HTTP connection; the standard way to push real-time data to browsers.›Zero-copy I/Onetworking: Move bytes between files, sockets and NICs by passing page references instead of copying through user space: sendfile, splice, MSG_ZEROCOPY, kTLS.›Coverage-guided fuzzingsecurity: Mutate inputs, run an instrumented target, and keep any input that reaches a new code edge; feedback turns blind guessing into incremental search.›Mutual TLSsecurity: TLS where the client also proves a certificate key, so every connection carries a verified, rotatable machine identity.›TLS 1.3security: The standard encrypted, authenticated transport; one round trip, only forward-secret AEAD suites, and little left to misconfigure.›Point-in-time recoverystorage: Periodic base backups plus a continuously archived change log let you rebuild the state as of any instant, then branch from there.›Graceful drainingsystems: On a termination signal, stop taking new work, finish in-flight work until a deadline, tell clients to reconnect elsewhere, then exit; clients resume from a token.›Semver range matchingcompilers: Semantic version parsing, ordering and range matchingTopological sortgraphs: Order the vertices of a DAG so every edge points forward; the core of build systems, schedulers and dependency resolution, with cycle detection for free.›CDCL SAT solvingoptimization: Search for a satisfying assignment, learn a new constraint from every dead end, and jump back past irrelevant choices; handles industrial formulas with millions of clauses.›Linking and relocationcompilers: The linker merges object files, matches each undefined name to one definition, assigns addresses and patches every recorded reference (relocation); dynamic linking defers part of that to load time.›BFS and DFSgraphs: The two basic graph traversals; BFS gives hop distances and levels, DFS gives discovery/finish order for cycles, components and topological order.›mmap and the page cachehardware-os: How read() and mmap reach file data through the kernel page cache, what readahead and madvise do, and why databases use their own buffer pool instead.›Hierarchical timing wheelsystems: Timers bucketed by expiry into wheels of slots, coarser wheels for far deadlines: O(1) insert and cancel for millions of timeouts.›Epoch-based reclamationconcurrency: Threads announce a global epoch while touching shared lock-free data; garbage retired in epoch e is freed once the epoch reaches e + 2.›Cooperative schedulingconcurrency: Green threads/coroutines yielding at await points on a user-space schedulerMemory orderingconcurrency: What Relaxed, Acquire, Release and SeqCst atomics actually guarantee, what they cost on x86 and ARM, and which one each pattern needs.›Fault injectiondistributed: Inject latency, errors, kills and partitions via proxies, netem or hooksRecord replaydistributed: Record every nondeterministic input (time, randomness, I/O, model and tool outputs, per-tick player input) and re-execute deterministically for debugging, replays and state rebuild+ 17 moreDrat proof certificatesoptimization: DRAT and LRAT proofs emitted by SAT solvers and checked independentlyTwo watched literalsoptimization: Unit propagation for SAT that watches just two literals per clause, so backtracking costs nothing and long clauses are rarely touched.›Arena allocatorsystems: Allocate by bumping a pointer through big chunks and free everything at once: a few instructions per allocation for objects sharing a lifetime.›Executable formats elf pecompilers: ELF, PE and Mach-O: sections, segments, symbol tables, relocations, dynamic loadingString interningdata-structures: Store each distinct string once and refer to it by idSwiss tabledata-structures: Open-addressing hash table that filters 8-16 slots at once with 1-byte hash tags and SIMD, giving fast lookups at 87.5 % load.›Sparse matrices (CSR)numerics: Compressed sparse row stores only nonzeros as row pointers, column indices and values; SpMV streams them once, so it is memory-bandwidth bound.›Virtual memory and the TLBhardware-os: How page tables and TLBs translate every address, what TLB reach means, and when 2 MiB or 1 GiB huge pages pay off on Linux.›Slab allocatorsystems: Pre-carve pages into equal-size slots per size class with an in-place free list: O(1) alloc and free, no external fragmentation, cache-warm reuse.›Transparent interceptionnetworking: Redirect traffic to a local proxy (iptables TPROXY/REDIRECT, eBPF) without app changesSyscall interceptionsecurity: Intercept syscalls via ptrace, seccomp user notify or a user-space kernel+ 8 moreEbpfhardware-os: Verified in-kernel programs on hooks (kprobes, XDP, tc) with mapsPtrace process controlhardware-os: Ptrace: attach, read and write registers and memory, single-step, intercept signalsSeccomp bpfsecurity: Syscall filtering with seccomp BPF programs+ 4 more

How it is built

Architecture overview

A server JavaScript runtime is a JavaScript engine embedded in a host program. The engine (V8 or JavaScriptCore) knows the language: objects, promises, a garbage collector, a JIT. It knows nothing about files, sockets or timers. The host adds those as native functions and drives the engine from an event loop: it waits for the operating system, calls a JavaScript callback when something is ready, and after each callback runs the queued promise jobs. Blocking work that has no non-blocking kernel interface goes to a small thread pool and comes back as an event. The one idea is a single JavaScript thread that never blocks, fed by an event loop that does all the waiting.

Node.js, Deno and Bun are three answers to the same host problem. Node pairs V8 with the C library libuv. Deno is written in Rust on V8 and Tokio. Bun is written in Rust around JavaScriptCore. The sections below follow the parts that every one of them has: engine, event loop, native I/O, modules, startup, and operational controls.

Parts:

  JS thread (one per isolate)                       libuv-style host
 ┌───────────────────────────────┐        ┌─────────────────────────────────────┐
 │ engine: heap, JIT, GC         │ native │ event loop: timers ─► poll ─► check │
 │ call stack ─► drain microtasks│◄──────►│ epoll/kqueue/IOCP, io_uring (files) │
 │ promise jobs queue            │ calls  │ thread pool (default 4): fs, DNS    │
 └──────────────▲────────────────┘        └──────────────┬──────────────────────┘
                │ callbacks                              │ readiness / completion
         modules, snapshot, permissions                 sockets, files, child processes

How the parts fit together

Engine and embedding

An embedder creates an isolate, "a VM instance with its own heap", and contexts inside it, and touches every JavaScript object through handles because the garbage collector may move objects (V8 embedder's guide). One isolate is used by one thread at a time; Node's worker threads each run their own JavaScript engine instance, which is how CPU-bound work gets parallelism, with memory sharing only through transferred ArrayBuffers or SharedArrayBuffers (Node.js worker_threads documentation). The Memory ordering rules matter at that boundary: atomics on shared buffers are the only synchronization JavaScript has. Everything about tiers, inline caches, deoptimization and the collector is the jit-vm system, which sits inside this box. The contract the host adds: native functions called from JavaScript run on the JavaScript thread and must return quickly.

Promise jobs and the event loop

The language specification only says that a settled promise enqueues a job through a host hook, HostEnqueuePromiseJob; when jobs run is the host's decision (ECMAScript specification). V8 exposes this as a microtask policy: MicrotasksPolicy::kExplicit, kScoped or kAuto (V8 include/v8-microtask.h). Browsers run a microtask checkpoint after each task (HTML specification). Node runs the same promise queue and adds its own process.nextTick queue: each time the next-tick queue is drained, the microtask queue is drained immediately after, so in CommonJS a nextTick callback runs before a promise callback, while in ES modules, which already run inside the microtask queue, the order flips (Node.js process documentation). The block javascript-event-loop (no page yet) describes the queues; the host-level loop under it is libuv's.

libuv's loop iterates in a fixed order: update the loop's notion of "now", run due timers, run pending callbacks, run idle and prepare handles, compute a poll timeout, block for I/O, run I/O callbacks, run check handles (setImmediate), run close callbacks (libuv design overview). The blocking step is an epoll event loop on Linux, kqueue on macOS and the BSDs, and IOCP on Windows. The timeout in the blocking step is the time until the next timer, which links the two halves: libuv keeps timers in a min-heap (src/timer.c, heap-inl.h), a Hierarchical timing wheel is the alternative when millions of timeouts are armed and cancelled. A d-ary heap (d = 4) is the cache-friendlier variant of that timer min-heap: with many pending timeouts, half the depth and siblings in one cache line make each insert and pop-min cheaper, and the poll timeout is still read from the heap's root. cooperative-scheduling (no page yet) is the mental model for the JavaScript side: tasks yield only at await and at the end of a callback.

Native I/O and the worker pool

Sockets are non-blocking and polled. Files are different: the libuv design overview says there are no platform-specific file I/O primitives it could rely on, so file system operations and DNS lookups (getaddrinfo, getnameinfo) run on a global thread pool, and user code can queue work there too. The default pool size is 4, settable with UV_THREADPOOL_SIZE up to 1024 (libuv thread pool documentation). Two refinements exist. On Linux libuv can now route file requests through io_uring, so some file work needs no pool thread (src/unix/linux.c and the ChangeLog show io_uring file requests and their cancellation). And the data path can avoid copies: Zero-copy I/O and Buffer pools for zero-allocation I/O are the blocks behind a Buffer that points at kernel-read bytes without a JavaScript-heap copy.

The Rust runtimes use a different pool. Deno states it is built on V8, Rust and Tokio (Deno README); Tokio's multi-thread scheduler is the typical home of a work-stealing deque, with epoch-based reclamation as one of the techniques used inside lock-free structures. The contract to JavaScript is unchanged: a completion becomes one callback on the JavaScript thread.

Network protocols

HTTP is parsed natively and the parsed pieces cross into JavaScript as a small number of strings and objects; Node's parser is llhttp, a port of http_parser to the llparse generator (nodejs/llhttp). HTTP/1.1 message parsing is the block; scanning for delimiters is where SIMD programming helps. WebSockets layer on the same connection after an upgrade. TLS 1.3 with its building blocks (AEAD, ECDH, HKDF, hashes) is not written by the runtime; Node's TLS API exposes the settings of its TLS library, such as cipher lists and a security level (Node.js tls documentation). Certificate chains and validation are x509-certificates (no page yet); client authentication is Mutual TLS.

Module system and packages

Code is found by name: require and import walk node_modules folders and package exports maps (Node.js modules and packages documentation), a step the graph calls module-resolution (no page yet). Modules form a graph with cycles; evaluation order is a depth-first post-order (BFS and DFS, Topological sort). A package manager picks versions that satisfy the ranges (dependency-version-resolution and semver-range-matching, both no page yet) and writes them into node_modules; the npm folder layout is documented in npm's folders page. Native addons are shared libraries loaded at run time, a case of Linking and relocation, and cross the boundary through an ABI-stable C API in Node (Node-API documentation); the platform's calling-conventions-abi (no page yet) applies below that.

Startup

A JavaScript runtime starts a fresh heap and runs bootstrap code before user code. The cost is cut by serializing an initialized heap and loading it (heap-snapshot-startup, no page yet). V8 supports custom startup snapshots (V8 blog, Custom startup snapshots); Node exposes them with --build-snapshot and the v8.startupSnapshot API, offers a module compile cache through NODE_COMPILE_CACHE, and can pack an application into a single executable (Node.js CLI and single-executable-application documentation). The snapshot file is read through the OS page cache (mmap and the page cache), so repeated starts are mostly page-cache hits. The constraint is that everything in a snapshot must be serializable: open sockets, timers and native handles cannot be.

Operations: supervise, drain, reload

One JavaScript thread means one crash takes out the process, so servers run several processes under a supervisor (Process supervision); Node ships a cluster module for this, and the usual alternative is the platform's init system. On a deploy or scale-in, the process stops accepting, finishes in-flight requests and then exits (Graceful draining). During development node --watch restarts on file changes (File system watching describes why the events are hints, not facts). Permissions are the other operational control: Deno runs with no file, network, environment or subprocess access unless granted by flags or prompts, and says that code at the same privilege level is not limited (Deno security documentation); Node has an opt-in --permission model; workerd (Cloudflare) isolates each Worker to the resources configured for it, but its README says it needs an outer sandbox for hostile code.

Reliability and verification

The runtime itself is stateless: a process that dies loses only in-flight requests, so durability belongs to the store the application writes to. Where that store is a primary with standbys (Primary-backup replication), the runtime's contract is to treat the primary as an address that can change: connections drop, the client reconnects to the new primary, and non-idempotent requests carry a key so a retry is safe. The same applies to Point-in-time recovery: an application bug that writes bad data through the runtime is repaired by rebuilding the store to the instant before it, not by anything inside the host.

Two testing blocks target the host's own parsers and loop. Coverage-guided fuzzing feeds mutated byte streams to the HTTP/1.1 parser, the WebSocket frame decoder and the TLS-facing glue, keeping any input that reaches a new code edge; these are the places where untrusted bytes first meet native code. Deterministic simulation testing runs the event loop with a simulated clock, sockets and disk under a seeded scheduler, so ordering bugs between timers, promise jobs and I/O completions replay from a seed. hdr-histogram (no page yet) records request latency and event-loop lag per callback with bounded relative error, so p99 and p999 are read from one fixed-size structure and drain or restart decisions can look at tail latency instead of the mean.

Data and control flow

One HTTP request in Node (single process, Linux):

  1. The listening socket becomes readable; the poll step of the loop returns and calls the connection callback with the new socket.
  2. The next poll reports data on the socket; the native read fills a buffer and the HTTP parser splits it into method, path, headers and body chunks.
  3. JavaScript runs the request handler on the one JavaScript thread; any await returns control and the loop goes on serving other sockets. Time goes into the handler, JSON handling and garbage collection, not into waiting.
  4. A database call or a file read is submitted: a socket write for the database, a thread-pool job (or io_uring request) for the file. The handler is suspended on a promise.
  5. The result arrives as an event; libuv calls the native completion, which resolves the promise; after the callback returns, the host drains the next-tick queue and then the microtask queue, so the handler continues before the loop looks at I/O again.
  6. The response is written to the socket (non-blocking; a partial write waits for the next writable event) and the loop moves on.

Process start with a snapshot:

  1. The binary maps the snapshot and deserializes the heap (page-cache reads).
  2. The host re-attaches native handles (stdout, the event loop) that cannot be in a snapshot.
  3. The entry point runs; the first request is served without bootstrap compile time.

Scaling

In order:

  1. The JavaScript thread. Handlers that do CPU work block everything on that isolate. Levers: move work to worker threads, shrink per-request work (parsing, serialization), keep the code in the optimized tier.
  2. The thread pool. File, DNS and crypto work share four threads by default; a slow disk or DNS stalls unrelated requests. Levers: raise UV_THREADPOOL_SIZE, io_uring for files, avoid sync file calls on the hot path.
  3. One process per core. A single event loop uses one core. Levers: run N processes behind a listening socket (cluster, containers), keep state out of the process, drain on deploy.
  4. Memory. Heap limits and buffers off the heap; the garbage collector pauses grow with heap size. Levers: smaller live heap, streams instead of whole bodies, worker resource limits (Node lets you set heap limits per worker).
  5. Connection count. Per-connection cost (parser state, TLS state, buffers). Levers: buffer pools, fewer copies, offload TLS to a proxy.

Failure modes

FailureSymptomCauseFix
Blocked event loopall requests stall together, latency spikesCPU-heavy or synchronous work on the JavaScript thread (Node's guide: "Don't block the Event Loop")move to a worker, split the work, avoid sync APIs and catastrophic regular expressions
Starved thread poolfile and DNS calls take seconds while CPU is idlemore concurrent pool jobs than 4 threadsraise the pool size, route heavy work elsewhere
Microtask starvationtimers and I/O callbacks never runa promise chain or nextTick loop that re-enqueues itself, since the queues are drained completelybound the recursion; yield with a macrotask (setImmediate)
Unhandled rejection or exceptionprocess exits or leaks statethe single thread has no isolation between requestshandlers with explicit error paths, a supervisor that restarts, health checks
Memory growthrising RSS, long GC pauses, out-of-memory killclosures retaining request data, unbounded caches, large buffersheap snapshots and profiling, bounded caches, streaming
Slow startautoscaled instances serve their first requests latebootstrap and compile on every startstartup snapshot, compile cache, single executable
Unbounded backpressurememory grows when a client reads slowlywriting without checking the writable signalhonour stream backpressure, pause the source
Native addon crashsegfault, no JavaScript stackABI mismatch or a bug below the engineABI-stable addon API, version the binaries, crash reports

Real implementations

namewhat it isrepolicencenotes
Node.jsV8 plus libuv plus a C++ binding layernodejs/nodeMIT (per LICENSE)event loop in libuv; worker threads, cluster, startup snapshots
libuvcross-platform event loop, thread pool and I/O library written for Node.jslibuv/libuvMITepoll, kqueue, event ports, IOCP; io_uring for Linux file requests
DenoJavaScript, TypeScript and WebAssembly runtime on V8, Rust and Tokiodenoland/denoMITpermissions on by default; core engine glue merged into this repo
Bunruntime, bundler, test runner and package manager in one binaryoven-sh/bunMIT, plus LGPL-2 for the statically linked JavaScriptCore (per its LICENSE.md)README: written in Rust, powered by JavaScriptCore
workerdCloudflare's runtime for Workers on V8cloudflare/workerdApache-2.0isolates per Worker, capability bindings
llhttpNode's HTTP parsernodejs/llhttpMITgenerated with llparse

The alternative: a thread-per-request or goroutine/async-task model in another language needs no single-thread discipline but pays per-connection memory; the JavaScript model wins when handlers are mostly waiting on I/O and teams share code with the browser.

Sources

Similar systems

Share the most building blocks with this one.