Browser & Frontend Runtime
Orinoco GC: parallel scavenger, concurrent marking, and write barriers
A Node.js service runs fine for a week, then every 60 seconds there is a 5ms pause that shows up in p99 latency. The application code changed nothing. The GC cycle crossed an old-generation threshold. Understanding Orinoco is the difference between guessing and diagnosing.
The heap layout
The V8 heap is divided into two generations:
- Young generation — newly allocated objects, typically 1–8 MB. Collected frequently (minor GC).
- Old generation — long-lived objects, can be hundreds of MB. Collected infrequently (major GC).
Objects start in young gen. Those that survive two minor GCs are promoted to old gen. This generational design exploits the observation that most objects die young — minor GC can be fast because the live set is small.
Minor GC: the parallel Scavenger
The scavenger uses a copying collector with two semispaces:
- From-space holds live objects; to-space is empty.
- On collection, V8 walks roots (stack, globals, registers), copies reachable young objects to to-space, updates pointers.
- Dead objects are abandoned in from-space.
- Survivors of two minor GCs are promoted to old generation.
The scavenger has been parallel since Orinoco’s first phase (V8 6.2, 2017): multiple worker threads share the work via dynamic work-stealing. Main-thread pauses are ~1ms because the young-gen live set is small and parallelism keeps wall-clock time low.
Major GC: Mark-Compact with concurrent marking
Old-generation collection traces all live objects, sweeps dead ones, and compacts survivors to reduce fragmentation. Pre-Orinoco this was a stop-the-world operation lasting hundreds of ms on large heaps.
Orinoco’s key innovation: concurrent marking — a background thread walks the heap while JavaScript executes on the main thread. The main thread pays a brief final-marking pause (single-digit ms) plus the sweep/compact phase (parallel across worker threads but still blocking). Result: main-thread pause reduced ~50% on WebGL-heavy workloads.
- Minor GC (Scavenger) pause
- <1 ms typical
- Concurrent marking pause reduction
- ~50% on WebGL
- Orinoco parallel scavenger shipped
- V8 6.2 (2017)
- Young gen heap size
- 1–8 MB
- Promotion threshold
- survives 2 minor GCs
- GC pause goal at 60fps
- <16.6 ms per frame
Write barriers
For concurrent marking to be correct while JS simultaneously mutates the heap, V8 uses write barriers: small code emitted at every property write that may cross from a black (already-marked) object to a white (not-yet-visited) object.
V8 uses snapshot-at-the-beginning (Yuasa) semantics: when the main thread overwrites a reference to a white object, the barrier shades the original (overwritten) referent grey so the marker will still visit it. This preserves the invariant “every object reachable at marking start will be visited”, even as the main thread races ahead. The write barrier costs ~3–5 cycles per write; V8 invests heavily in keeping it cheap.
Modern V8 uses tri-color marking (white/grey/black) with snapshot-at-the-beginning semantics and hybrid concurrent + incremental scheduling. Incremental marking amortises GC work across JS execution with no observable single pause.
Order the steps of a parallel minor GC (Scavenger) cycle:
- 1 Allocation triggers young-gen heap full
- 2 Main thread initiates Scavenger, worker threads join
- 3 Walk roots: stack, globals, registers — find live young objects
- 4 Copy live objects from from-space to to-space using work-stealing
- 5 Update pointers across heap to point to new locations
- 6 Objects that survived two GC cycles are promoted to old gen
- 7 From-space is declared empty, to-space becomes the new from-space
A Node.js process consumes 4GB RAM after a week of uptime. Trace the GC pattern.
Orinoco's scavenger reduces young-gen GC pause times via parallel work-stealing. Why is old-gen Mark-Compact still the harder problem?
Why does concurrent marking need write barriers?
Why this works
Why is there a generational split at all? Most allocated objects die quickly — a temporary buffer, a promise, a React element per render. Collecting only the young generation (which is small) is much cheaper than scanning the whole heap. The old generation only needs collection infrequently. This “generational hypothesis” is empirically true for most programs and is why generational GCs became the industry standard in the 1990s.
- 01Describe the minor GC (Scavenger) cycle in V8.
- 02How does concurrent marking in Orinoco avoid corrupting the heap?
- 03What is the most common cause of a slow Node.js memory leak, and how do you find it?
Orinoco is V8’s generational garbage collector. New objects land in the young generation (~1–8 MB); survivors of two minor GC cycles are promoted to old generation. Minor GC uses a copying scavenger that runs in parallel across worker threads — pauses are typically under 1ms. Major GC (old gen) uses mark-compact; concurrent marking moves the object-graph traversal to a background thread while JS runs, reducing main-thread pauses by ~50% on WebGL-heavy workloads. Write barriers at every property write keep the concurrent marker correct when the JS thread modifies references mid-cycle. Incremental marking further amortises work across JS execution. The fundamental tradeoff: 3–5 cycles per write (barrier overhead) in exchange for eliminating hundreds-of-millisecond stop-the-world pauses that were the norm before Orinoco.
appears again in143
- 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
- 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