Inside the interpreter loop
Ignition runs bytecode with a dispatch loop: fetch the opcode, jump to its handler, do the work, advance the bytecode pointer, dispatch the next. The accumulator threads values between handlers, feedback slots fill in as types are observed
You have the bytecode. Now something has to run it — one instruction after another, millions of times a second, while quietly keeping a tally of how hot each function is getting. There is no magic here: it is a loop. Fetch the next opcode, jump to the code that handles it, do the work, advance, repeat. Understanding that loop explains both why interpreted JavaScript is predictable-but-not-fast and how the engine knows the exact moment to hand a function to the JIT.
The dispatch loop
The previous lesson left us with a function’s bytecode and a set of shared handlers (one per opcode, written in Torque). Execution is the act of running those handlers in sequence. Conceptually:
loop:
opcode = bytecode[pc] // fetch the current opcode
handler = handlerTable[opcode] // look up its handler
jump handler // do the work; handler advances pc, then dispatches the nextEach iteration fetches the opcode at the current bytecode pointer (pc), looks up the handler for that opcode in a table, and jumps to it. The handler reads its operands (from the register file and the accumulator), does the operation, writes the result (usually back to the accumulator), advances the pc past this instruction’s operands, and then dispatches the next instruction. Run that loop until Return, and you have executed the function.
How the jump is made fast: threaded dispatch
A naive interpreter would write this as a giant switch (opcode) inside a while loop. That works, but it has a hidden cost: after every handler the control flow returns to the top of the loop and re-enters the switch, and the CPU’s branch predictor sees one single indirect branch shared by all opcodes — so it predicts poorly and stalls.
Ignition uses threaded dispatch instead: each handler ends by directly dispatching to the next handler — historically via a computed goto (one indirect jump per handler), and in Ignition specifically via a tail call from one handler’s machine code straight into the next handler’s. The win is for branch prediction: each opcode’s dispatch site is its own indirect branch, so the predictor can learn the common successor of each opcode (e.g. a comparison is usually followed by a branch), instead of muddling one shared branch. Fewer mispredicts means a tighter, faster interpreter loop.
The accumulator is what makes the handoff between handlers clean: rather than passing operands explicitly, most handlers read and write the one shared accumulator register, so a value produced by one bytecode is sitting right where the next bytecode expects it. The dispatch loop and the accumulator are two halves of the same design.
Profiling while interpreting: feedback fills in
The loop is not just executing — it is learning. Recall that every type-sensitive bytecode carries a feedback-vector slot. As those handlers run, they fill in the slots with the maps (hidden classes) and types they actually observe: this LdaNamedProperty saw an object with map M; this Add saw two Smis; this Call always targeted function F. By the time a function has run a while, its feedback vector is a compact profile of how it is really used — exactly the information the optimiser needs. The interpreter is priming the JIT as a side effect of running the program.
The interrupt budget: how the loop decides to tier up
How does V8 know a function is hot enough to compile? It does not literally count every instruction. Each function carries an interrupt budget (a “feedback cell budget” / invocation-and-loop counter). The loop decrements that budget on the events that correlate with heat:
- Back-edges — every time control jumps backward to the top of a loop (one iteration of a
for/while). A tight loop chews through the budget fast. - Function calls / invocations — entering the function again.
When the budget hits zero, the loop raises a tier-up request: compile this function with the next tier (Sparkplug, then Maglev, then TurboFan — unit 04). For a loop that is still running when it goes hot, V8 can even swap the running code mid-loop via on-stack replacement (OSR), jumping from the interpreter into freshly compiled code without waiting for the loop to finish. Back-edges are the key signal: a long-running loop is the canonical “this is hot, optimise it now” trigger.
- Dispatch style in Ignition
- tail-call / threaded
- Value handed between handlers via
- the accumulator
- Budget decremented on
- back-edges + calls
- Budget hits zero ⇒
- tier-up request
- Hot loop swapped mid-flight by
- OSR
- Interpreting vs optimised: speed
- slower but predictable
Why interpreting first is the safe default
When you profile a function and see it spending time in the interpreter, that is not a bug — it is the engine doing exactly what it is supposed to do before it has enough data to speculate. Interpretation is slower per instruction than optimised machine code — there is a fetch-and-dispatch overhead on every bytecode. But it is predictable: there is no compile pause (the bytecode is ready immediately), and there is no deopt — the interpreter makes no speculative type assumptions, so it cannot be wrong about types and forced to bail out. Optimised code is faster but can deopt back to this loop when an assumption breaks (a deopt always lands you back in the interpreter). So Ignition is both the cheap starting point and the safety net the JIT falls back to. That is why every function begins its life right here, in the dispatch loop, and why this lesson sits at the seam between “how JS runs” and the JIT machinery of unit 04.
Why does Ignition use threaded (tail-call) dispatch instead of one big switch statement over the opcode?
A function contains a tight loop that runs millions of iterations. What in the interpreter triggers V8 to compile it, and via which event?
Order one iteration of the Ignition dispatch loop for a single bytecode.
- 1 Fetch the opcode at the current bytecode pointer (pc)
- 2 Look up that opcode's handler in the handler table
- 3 Jump to the handler; it reads operands and updates the accumulator
- 4 The handler advances the pc and dispatches the next bytecode
▸Why this works
Why does a deopt always land back in the interpreter rather than a lower JIT tier? Because the interpreter is the one tier that makes no speculative assumptions — it handles every type, every shape, every edge case the spec defines, just slowly. When optimised code discovers its bet was wrong (the value it assumed was a Smi turned out to be a HeapNumber), the only place guaranteed to run correctly from that exact bytecode offset is Ignition. So the dispatch loop is the universal fallback: slow, but always right. The JIT can re-optimise later once the feedback stabilises.
- 01Walk one iteration of the Ignition dispatch loop and name the role of the accumulator.
- 02What is threaded dispatch and why is it faster than a switch-based interpreter?
- 03How does the interpreter loop decide a function is hot, and what happens then?
Running bytecode is a dispatch loop: fetch the opcode at the bytecode pointer, look up its handler in the shared handler table, jump to the handler (which reads operands from the register file and the accumulator, computes, and writes the result back to the accumulator), advance the pc, and dispatch the next — repeating until Return. Ignition does not use a naive switch; it uses threaded, tail-call dispatch so each opcode has its own indirect-branch site, which the CPU branch predictor learns far better than one shared branch, cutting mispredicts. The accumulator is the clean handoff between handlers, the other half of the dispatch design. While interpreting, the loop also fills in the feedback-vector slots with the hidden classes and types it actually observes, profiling the program for free so the JIT can later specialise. To decide when to optimise, each function carries an interrupt budget decremented on back-edges and calls; when it hits zero the loop raises a tier-up request (and a still-running hot loop can be swapped via on-stack replacement). Interpreting is slower per instruction but predictable — no compile pause and no deopt — and it is the universal fallback a deopt always returns to, because the interpreter makes no speculative type assumptions. Now when you see a function stuck in interpreted mode longer than expected, you know exactly why: not enough back-edges have fired to exhaust the interrupt budget and trigger the JIT.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
appears again in184
- Why GraphQL gets N+1junior
- DataLoader mechanics: tick-boundary batchingmiddle
- Batch function contracts: ordering, shapes, errorsmiddle
- Federation and lookahead: batching beyond DataLoadermiddle
- Query complexity defences: depth, cost, persisted queriesmiddle
- Senior GraphQL API: scheduling contract, tenant isolation, observabilitysenior
- Why idempotency: making retries safejunior
- Server-side state machine: four states of an idempotency keymiddle
- Outbox and inbox: effectively-once across the dual-write boundarymiddle
- Concurrency and cache architecture for idempotency at scalesenior
- Observability, production failures, and global-scale designsenior
- The event loop: one thread, three queuesjunior
- Tasks, microtasks, and scheduler.yield()middle
- Microtask starvation, Long Tasks, and LoAFsenior
- Node.js event loop: phases, nextTick, and loop lagsenior
- React, Vue, and INP observability in productionsenior
- The render pipeline: six stages from bytes to pixelsjunior
- Stage costs and the renderer process modelmiddle
- Invalidation, dirty bits, and containmiddle
- Compositor layers: promotion, overlap, and GPU memorymiddle
- DevTools flame strip and the frame lifecyclemiddle
- Layout thrash: forced synchronous layoutsenior
- BeginMainFrame, compositor-driven animations, and GPU memorysenior
- Production observability: LoAF, INP, and the full attack surfacesenior
- What V8 is and why performance varies 100×junior
- V8''''s four-tier JIT pipeline and profile-guided tieringmiddle
- Hidden classes, transition trees, and memory layoutmiddle
- Inline caches, IC states, and deoptimizationmiddle
- Orinoco GC: parallel scavenger, concurrent marking, and write barriersmiddle
- TurboFan''''s speculative engine and the deopt-loop trapsenior
- V8 in production: isolates, pointer compression, and real failuressenior
- Service worker lifecycle and cache strategiesmiddle
- Service worker edge cases: version skew, durability, and navigation trapssenior
- What the reconciler does: render vs commitjunior
- The fiber object and the double-buffer treemiddle
- Render phase purity and commit phase sub-stepsmiddle
- Reconciliation: diffing heuristics and the key trapmiddle
- Priority lanes, time-slicing, and useTransitionmiddle
- Bailout, memoisation, and tearingsenior
- React Profiler, the Compiler, and production observabilitysenior
- Rendering strategies: SSG, SSR, ISR, streaming, and hydrationjunior
- SSG, SSR, ISR, streaming, and RSC — how each worksmiddle
- Hydration cost: selective, progressive, islands, resumabilitymiddle
- Hydration mismatch: causes, detection, and the determinism rulesenior
- RSC, per-route strategy, and production observabilitysenior
- Core Web Vitals: what LCP, INP, and CLS measurejunior
- CLS: why layout shifts happen and how to stop themmiddle
- Metric tradeoffs, RUM attribution, and the CI+field loopsenior
- The full picture: URL to LCP to INP as a relay racejunior
- Eight layers traced: from the service worker to the second navigationmiddle
- Five canonical breaks: where production reliably diessenior
- The three-track method: reading traces and building a monitored systemsenior
- What is a cache stampede and why it makes things worsejunior
- Lock and single-flight: bounding concurrent rebuildsmiddle
- XFetch: coordination-free probabilistic early expirationmiddle
- Stale-while-revalidate and CDN request coalescingmiddle
- Detecting stampedes and designing TTL for productionmiddle
- Metastable failure, fencing tokens, and production postmortemssenior
- What a relation is: tables, rows, keys, and constraintsjunior
- Constraints, keys, and Postgres data typesmiddle
- Normal forms, denormalization, and why schemas stickmiddle
- JSONB, arrays, and when a side table winsmiddle
- Heap storage, TOAST, and column alignmentsenior
- Schema integrity: deferral, versioning, and production failure modessenior
- Relational vs document, wide-column, graph, and key-valuesenior
- Index-only scans, the Visibility Map, and INCLUDEsenior
- Production failure modes and the index audit playbooksenior
- pg_statistic, ANALYZE, and production observabilitymiddle
- Production failure modes and plan stabilitysenior
- MVCC: why readers and writers never wait for each otherjunior
- Row versions and snapshots: the on-disk mechanicsmiddle
- HOT updates and isolation levels: what you gain and what you paymiddle
- Vacuum and bloat: keeping the storage tax boundedmiddle
- CLOG, XID wraparound, and MultiXact: deep visibility internalssenior
- SSI internals and production autovacuum tuningsenior
- Real-world MVCC failures, deployment patterns, and distributed snapshotssenior
- Connection pools: amortising the cost of a Postgres backendjunior
- PgBouncer session, transaction, and statement modesmiddle
- Pool sizing: the (cores × 2) + spindles formula and the two-layer stackmiddle
- Pool exhaustion and idle-in-transaction: the 3 AM failure modemiddle
- Migrating to transaction mode: rollout playbook and PgBouncer 1.21 prepared statementsmiddle
- The Postgres process model and why raising max_connections degrades throughputsenior
- Pooler landscape 2026, serverless connection storms, and the full failure-mode taxonomysenior
- What a schema migration is and why it replaces ad-hoc DDLjunior
- ADD COLUMN: instant in PG 11+ vs rewrite in older Postgresjunior
- The lock-queue failure mode: why instant DDL can freeze the databasemiddle
- Safe DDL patterns: NOT VALID, CONCURRENTLY, and unsafe-op fixesmiddle
- Expand-contract: zero-downtime for breaking schema changesmiddle
- Advisory locks, migration tools, and deploy coordinationsenior
- Migration failure taxonomy and production disciplinesenior
- Why sharding exists: the single-Postgres ceilingjunior
- Shard-key selection: hash, range, list, and directory strategiesmiddle
- Partitioning vs sharding: same word, two different thingsmiddle
- Co-location and Citus: the invariant that makes sharding usablemiddle
- The hot-shard failure mode: detection, isolation, and durable policymiddle
- Schema-based sharding and multi-tenancy alternativessenior
- Online resharding, 2PC, and the operational cost of shardingsenior
- The seven acts: from CREATE TABLE to Citusjunior
- Acts 1–3 in depth: schema, indexes, and planner statisticsmiddle
- Acts 4–6 in depth: MVCC bloat, connection pooling, and safe migrationsmiddle
- Act 7 in depth: sharding, co-location, and the seven-tier tradeoff cascademiddle
- Observability, anti-patterns, and production triagesenior
- Raft roles, terms, and why majority quorums prevent split brainjunior
- How Raft replicates a log entry and decides it is safe to commitmiddle
- Raft leader election: timeouts, voting rules, and the four safety propertiesmiddle
- Raft in the real world: partitions, slow disks, and client routingmiddle
- Raft extensions: pre-vote, learners, snapshots, and linearizable readssenior
- Raft in production: membership changes, Multi-Raft, and observabilitysenior
- Where data fetching happens — and why it decides LCPjunior
- Fetch waterfalls — diagnosis and the Promise.all curemiddle
- React Server Components and Suspense streamingmiddle
- Client-side cache: TanStack Query, SWR, and stale-while-revalidatemiddle
- LCP, prefetch, and race conditions in interactive fetchingmiddle
- Senior internals: RSC payload, caching layers, and production failure modessenior
- The three-way handshakejunior
- Sequence numbers and connection statemiddle
- DNS: what it does and why it existsjunior
- The resolver walk: referrals, record types, and gluemiddle
- TTL, caching, and DNS propagationmiddle
- The 1-RTT handshake: key shares and ECDHEmiddle
- Session resumption and 0-RTTmiddle
- WebSocket: the HTTP upgrade handshakejunior
- WebSocket frame format: opcodes, masking, fragmentationmiddle
- WebSocket backpressure: when clients can''''t keep upmiddle
- Reconnection: jittered backoff, thundering herd, message resumptionsenior
- WebSocket at scale: HTTP/2 multiplexing, permessage-deflate, C10Msenior
- WebSocket in production: proxies, security, and distributed architecturesenior
- What reverse proxies dojunior
- Health checks, connection draining, and slow startmiddle
- Session affinity, consistent hashing, and the right fixmiddle
- Retry storms, circuit breakers, and load sheddingsenior
- Resilient LB architecture: anycast, zone-aware routing, and observabilitysenior
- Why QUIC and not TCP+TLSjunior
- Connection IDs and network migrationmiddle
- 0-RTT resumption and packet encryptionsenior
- DDoS: what it is and why it worksjunior
- Amplification attacks and state exhaustionmiddle
- Rate limiting: algorithms and architecturemiddle
- WAFs, firewalls, mTLS, and HSTSmiddle
- DNS cache poisoning and BGP hijackingsenior
- Defense-in-depth architecture and attack economicssenior
- DNS, TCP, TLS in sequence: where the milliseconds gomiddle
- Proxy intercepts and security gates: rate limiters, WAF, mTLSmiddle
- Alternate paths: QUIC 0-RTT, WebSocket upgrade, connection migrationmiddle
- Observability: distributed traces, USE/RED, and samplingsenior
- Resilience: cascading retries, circuit breakers, and error budgetssenior
- What the three signals are: logs, metrics, and tracesjunior
- Why structured logs exist: the diary vs the spreadsheetjunior
- The production log schema: fields every line must carrymiddle
- PII redaction and log injectionsenior
- OTel Logs Data Model and audit logs as a subsystemsenior
- SLI, SLO, and the error budget: reliability by the numbersjunior
- Error budget policy, latency SLOs, and composite journeysmiddle
- Production SLO failures, self-observability, security, and the big picturesenior
- The incident loop: from pager to postmortem to preventionmiddle
- Cache lines, struct layout, and false sharingmiddle
- SIMD, SoA vs AoS, and memory bandwidthmiddle
- Cache-oblivious algorithms, PGO, and production failuressenior
- GC in production: observability, security, edge cases, and fleet governancesenior
- Batching: amortize fixed cost per operationjunior
- The batching window: size and wait timemiddle
- Batching in Kafka and Postgresmiddle
- io_uring and observability of batchingmiddle
- From Nagle to io_uring: evolution of batchingmiddle
- Backpressure, failure isolation, and batch security in productionsenior
- CI enforcement and RUM: making budgets stickmiddle
- V8 JIT pipeline, HTTP priorities, and bundle securitysenior
- The performance loop: discipline, not a projectjunior
- Classify and fix: matching bottleneck families to remediesmiddle
- Observability stack and CI gates: catching regressions before they shipmiddle
- Incident to enforcement: SLO burn to verified fix in 35 minutesmiddle
- Culture, economics, and org-scale performancesenior
- At-most-once, at-least-once, exactly-once: the three delivery contractsjunior
- The three failure legs — where duplicates and losses actually happenmiddle
- Consumer-side dedup: the cheapest path to exactly-once processingmiddle
- Kafka exactly-once semantics: idempotent producer and transactionsmiddle
- SQS visibility timeout, DLQ, and the outbox patternmiddle
- Exactly-once in production: impossibility proof, hybrid patterns, and real incidentssenior
- What OAuth is and why passwords are not the answerjunior
- Authorization code flow with PKCEmiddle
- ID token validation and JWKS cache managementmiddle
- Refresh token rotation and scope-based least privilegemiddle
- Sender-constrained tokens: DPoP and mTLSsenior
- OAuth in production: audience attacks, observability, and real failuressenior
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.