Networking & Protocols
WebSocket frame format: opcodes, masking, fragmentation
After the WebSocket handshake the HTTP parser is gone. What flows on the wire is a compact binary format that carries every message — text, binary, keepalive pings, and graceful closes — in as few as 2 bytes of overhead. Understanding that format is what separates “it sometimes works” from “I know exactly what broke.”
The frame header anatomy
A WebSocket frame starts with 2 mandatory bytes, followed by optional length extension and masking key fields, and then the payload:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+-------------------------------+
| Masking-key (if MASK set, 4 bytes) |
+---------------------------------------------------------------+
| Payload data |
+---------------------------------------------------------------+Byte 1 breakdown:
- FIN (bit 7) —
1means this is the last (or only) fragment of a message. - RSV1-3 (bits 6-4) — reserved for extensions (e.g.,
permessage-deflatesets RSV1=1). - Opcode (bits 3-0) — what kind of data the frame carries:
| Opcode | Meaning |
|---|---|
0x0 | Continuation frame |
0x1 | Text data (UTF-8) |
0x2 | Binary data |
0x8 | Close |
0x9 | Ping |
0xA | Pong |
Byte 2 breakdown:
- MASK (bit 7) —
1= payload is XOR-masked (client→server always; server→client never). - Payload length (bits 6-0):
0–125— the actual length.126— next 2 bytes (uint16) hold the real length.127— next 8 bytes (uint64) hold the real length.
Frame overhead totals:
- Small server→client frame: 2 bytes header only.
- Small client→server frame: 2 bytes header + 4 bytes masking key = 6 bytes.
- Minimum frame header (no mask, payload ≤125 bytes)
- 2 bytes
- Client→server overhead (mask required)
- 6 bytes
- Max payload in 7-bit length field
- 125 bytes
- Length extension for 126–65535 byte payloads
- +2 bytes (uint16)
- Length extension for larger payloads
- +8 bytes (uint64)
- Control frames (ping/pong/close) max payload
- 125 bytes
Why client frames must be masked
Masking is not encryption — it is a cache-poisoning defense. Here is the attack it prevents:
A malicious JavaScript on site-a.com opens a WebSocket connection to an intermediate proxy. It then sends bytes that happen to spell out a valid HTTP response. If the proxy is naive and stateless, it treats those bytes as HTTP and reflects them to other clients — poisoning its cache.
With masking, the client XORs every payload byte with a 4-byte random key sent in the frame header:
masked_byte[i] = payload[i] XOR mask_key[i % 4]The receiver XORs back with the same key to recover the original payload. Because the mask key is random per frame, the JavaScript on the malicious site cannot pre-craft bytes that both look like an HTTP response AND decode correctly under XOR. The attack becomes infeasible.
Server frames are not masked because JavaScript on site-a.com cannot read raw bytes from a server response on site-b.com anyway (same-origin policy blocks it).
Fragmentation and continuation frames
A large message can be split across multiple frames. Rules:
- First fragment: real opcode (
0x1or0x2), FIN=0. - Middle fragments: opcode
0x0(continuation), FIN=0. - Last fragment: opcode
0x0, FIN=1.
The receiver reassembles in order. Control frames (ping, pong, close) cannot be fragmented and are limited to 125 bytes; they can arrive interleaved between data fragments.
Control frames: ping, pong, close
Ping (0x9): a keepalive probe. The receiver must reply with a pong carrying the same payload. Proxies often have idle timeouts (60 seconds is common); sending a ping every 25–30 seconds resets the proxy’s timer and keeps the connection alive.
Pong (0xA): the mandatory reply to a ping. Can also be sent unsolicited as a unilateral heartbeat.
Close (0x8): initiates the closing handshake. The body contains an optional 2-byte status code followed by UTF-8 reason text. Standard codes:
| Code | Meaning |
|---|---|
| 1000 | Normal closure |
| 1001 | Going away (server shutdown, tab closed) |
| 1006 | Abnormal closure (no close frame; generated by the implementation) |
| 1008 | Policy violation |
| 1011 | Unexpected condition |
| 1013 | Try again later |
After sending a close frame, each side must wait for the peer’s close frame before closing the TCP connection.
Why this works
Why RSV bits matter for extensions. The permessage-deflate extension (RFC 7692) negotiated during the handshake uses RSV1=1 to signal that the payload is DEFLATE-compressed. A server that did not negotiate the extension and sees RSV1=1 must close the connection with code 1002 (protocol error). This strict checking ensures extensions cannot silently corrupt frames.
Parsing a small WebSocket frame
1/3Why does the client's Sec-WebSocket-Key get transformed into Sec-WebSocket-Accept by adding a fixed GUID, hashing it, and base64-encoding it?
Why must client-to-server WebSocket frames be masked, but server-to-client frames must NOT be?
Order the steps of a WebSocket close handshake:
- 1 One side sends a close frame with status code 1000
- 2 The other side receives it and replies with a close frame
- 3 The sender of the second close frame closes the TCP connection
- 4 Both sides are now in the closed state
- 01Explain why masking defends against cache-poisoning even though the mask key is sent in plain text inside the frame.
- 02A chat server is broadcasting a message to 10,000 connected clients. The broadcast completes in 100 ms. Network RTT is only 5 ms. Where does the other 95 ms come from?
- 03What is the FIN bit for in a WebSocket frame, and how does it interact with the opcode?
Every WebSocket message rides in one or more frames. The 2-byte header encodes the FIN bit (last fragment flag), opcode (text, binary, ping, pong, close), MASK flag, and payload length. Client-to-server frames must XOR their payload with a random 4-byte masking key to prevent cache-poisoning attacks where malicious JavaScript crafts bytes resembling HTTP responses; server-to-client frames are never masked because the same-origin policy already blocks JavaScript from reading cross-origin raw bytes. Large messages may be fragmented across frames using opcode 0x0 (continuation) with FIN=0 on all but the last. Control frames (ping, pong, close) carry at most 125 bytes and cannot be fragmented. Close frames carry a 2-byte status code; 1000 is normal, 1006 is generated when no close frame was received.
appears again in152
- 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
- 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