SYSTEM Cited by 3 sources
Figma Multiplayer + QueryGraph¶
What it is¶
Multiplayer is Figma's real-time-collaboration server for design files. It holds, in memory per loaded file, both:
- The full decoded representation of the Figma file (the binary file format decoded into an editable object graph).
- QueryGraph — a bidirectional dependency index over the file's nodes, tracking read dependencies and write dependencies between nodes.
It serves two duties over a client WebSocket:
- Subset computation on load. When a client connects and names an initial page, Multiplayer computes the transitive closure of that page's nodes under read+write deps and ships only that subset.
- Edit fan-out. For each collaborator edit, Multiplayer checks every other session's subscribed subset and streams the edit only if the edited node is reachable from that session's subscription. Mutations to dependency edges can implicitly grow another session's subscription, and those newly-reachable nodes are shipped as part of the edit delivery.
(Source: sources/2024-05-22-figma-dynamic-page-loading)
QueryGraph's two eras¶
Before (viewer / prototype only)¶
QueryGraph existed but encoded only read dependency edges:
explicit foreign keys on node data structures. Example from the post:
an instance node has a componentID field that points to the
component it depends on. Read-only consumers (viewers, prototypes)
never mutate state so write-deps didn't matter.
After (editor-capable, post-2024)¶
Extended to a bidirectional graph with a new edge type for implicit write dependencies — edges that exist between nodes that don't directly reference each other. The canonical example from the post is auto layout: editing a node in an auto-layout frame can automatically change the frame's layout and its neighbors' layout, though those neighbors aren't referenced by the edited node's foreign keys. These implicit edges are encoded as a distinct edge type. Bidirectional because a load needs both the "what does this node depend on" and "what depends on this node" answer in O(graph lookup).
Decoding path (also on Multiplayer)¶
Pre-dynamic-loading, the client could download the raw encoded Figma file directly — the server never had to decode. Dynamic loading made server-side decoding mandatory and critical-path, because the server must understand structure to compute a subset. Two optimizations landed to pay for this:
- Preload hint. The backend fires a hint to Multiplayer on the initial HTTP GET for the page load, so Multiplayer starts downloading + decoding before the client's WebSocket arrives. Saves 300–500 ms at p75. (patterns/preload-on-request-hint)
- Parallel decoding. Raw offsets are persisted into the file format so decoding can be split into chunks processed concurrently by multiple CPU cores. >40% reduction in decoding time on files that were previously >5 s serial to decode.
Subscription semantics¶
session.subscription_set(page P) = closure(P, read_deps ∪ write_deps).
Invariants:
- Load: full subscription closure shipped on connect.
- Edit on node N: ship to session S iff N ∈ S.subscription_set, at the time of the edit.
- Dependency-edge mutation: recompute S.subscription_set for every affected session. Ship newly-reachable nodes as part of the edit delivery. The post's illustrated case is swapping an instance to a different backing component — the new component's descendants become reachable for any session subscribed to the instance, even though that session never touched them.
Correctness bar: editing parity¶
The system's correctness contract (from the post):
"Actions that users take in a dynamically loaded file should produce the exact same set of changes as they would if the file were fully loaded."
A missing write dependency presents as a silent divergence: instances drift from components, layout stale, fonts missing. Validated pre-production via patterns/shadow-validation-dependency-graph — Multiplayer in shadow mode for an extended period, computing write-deps as if dynamic while the old full-load path remained live, and reporting errors whenever an edit landed on a node outside the computed write-dep set. Surfaced at least one complex cross-page recursive write-dep involving frame constraints + instances that would have shipped broken.
Measured impact¶
After six months of group-by-group A/B rollout (patterns/ab-test-rollout):
- 33% speed-up on the slowest, most complex file loads, despite files growing 18% YoY in size.
- 70% reduction in client-memory node count.
- 33% reduction in users hitting client-side out-of-memory errors.
Not disclosed¶
- Server-side RAM cost of QueryGraph + full file for large files.
- Shadow-mode duration and error-rate trajectory.
- Parallel-decoding parallelism factor (how many cores used).
- Preload-hint race-loss rate.
Foundational architecture (2019)¶
The 2019 post (Source: sources/2019-figma-how-figmas-multiplayer-technology-works) documents the original Multiplayer design predating QueryGraph and sets the substrate everything above is built on:
- Client-server, WebSocket. Figma clients are web pages; a cluster of servers handles WebSocket connections.
- One process per multiplayer document. Each active document gets its own server process — the single tie-break authority. This is what lets Figma simplify the CRDT model: the server is the center.
- Download-on-open, replay-offline-on-reconnect. Client downloads a full copy on open; updates flow bidirectionally over WebSocket; on reconnect the client redownloads fresh state and reapplies offline edits. "All of the multiplayer complexity is in dealing with updates to already-connected documents."
- Documents only. Multiplayer syncs Figma documents. Comments, users, teams, projects all live in Postgres behind a separate sync system with different trade-offs for performance / offline / security.
- Document data model is an object tree:
root document → page objects → content hierarchy. Reducible to
Map<ObjectID, Map<Property, Value>>or(ObjectID, Property, Value)triples. "Adding new features to Figma usually just means adding new properties to objects" — the property-valued references in this model (e.g. instance →componentID) are exactly the edges QueryGraph later indexes. - CRDT-inspired, not CRDT-compliant: per-property primitives (grow-only sets, LWW registers) composed into the overall document state, with the decentralization overhead stripped because the server is authoritative. OT rejected as "unnecessarily complex for our problem space" (concepts/operational-transform).
- Design methodology: the whole architecture was prototyped in a three-client simulator playground before any production-code change (patterns/prototype-before-production).
The 2024 QueryGraph extension strictly adds a bidirectional read+write dependency index over the same object-tree state model, upgrades Multiplayer to compute per-session subscription subsets, and makes server-side file decoding critical-path. None of the 2019 substrate was replaced.
Sibling reactive graphs (2026)¶
Figma's client now runs three bidirectional in-memory reactive graphs over the same object- tree document model, each indexing different edges:
- QueryGraph (this system, server-side) — read+write dependencies between document nodes.
- Parameter Runtime (2024–2026, client-side) — parameter-to-bound-property edges.
- Materializer (2026, client-side) — source-of-truth → derived-subtree edges, recorded implicitly by automatic dependency tracking.
The 2026 Materializer post notes that client runtime systems now cooperate under a shared orchestration layer (patterns/runtime-orchestration-unidirectional-flow) with predictable execution order — moving toward unidirectional flow and eliminating back-dirties. Materializer's re-materializations produce document-node mutations that propagate to collaborators via QueryGraph's subscription fan-out (mechanism not disclosed).
Seen in¶
- sources/2019-figma-how-figmas-multiplayer-technology-works —
foundational 2019 post: client-server + WebSocket +
one-process-per-doc architecture, download/replay offline
semantics, CRDT-inspired-centralized design reasoning
(explicit OT rejection), object-tree document data model
(
Map<ObjectID, Map<Property, Value>>), three-client simulator prototype methodology. The substrate everything else builds on. - sources/2024-05-22-figma-dynamic-page-loading — the dynamic-loading-for-editors post; canonical description of QueryGraph's read+write-dep bidirectional structure, Multiplayer's subset computation and edit fan-out, preload + parallel-decoding optimizations, and shadow-mode validation.
Sibling: LiveGraph (non-document real-time data)¶
Multiplayer + QueryGraph handles document-canvas state (vectors, layers, auto-layout, rich text). The non-document real-time data — comments, file lists, team membership, optimistic UI for user actions, FigJam voting — is served by Figma's other real-time system, LiveGraph.
Structurally very different:
| Axis | Multiplayer + QueryGraph | LiveGraph |
|---|---|---|
| Source of truth | In-memory per-document server process | RDS Postgres (via DBProxy) |
| Concurrency model | CRDT-inspired, one process per doc authoritative | Invalidation-based cache + stateless schema-aware invalidator |
| Change source | Collaborator edits (in-memory) | WAL logical replication stream (CDC) |
| Client sub | Subscribe to subset via reachability | Subscribe to query shape + args |
| Scaling axis | One process per active doc | Separate edge / cache / invalidator tiers (patterns/independent-scaling-tiers) |
Both exhibit push-based invalidation but at very different granularities (document node reachability vs DB-row query-shape substitution). LiveGraph's rebuild post (sources/2026-04-21-figma-keeping-it-100x-with-real-time-data-at-scale) names them as the two pillars of Figma's real-time stack.
Cross-system failure mode: connector / autosave cascade¶
The 2026-04-21 game-engine-inspiration post (sources/2026-04-21-figma-how-figma-draws-inspiration-from-the-gaming-world) discloses a production failure where Multiplayer amplified a bug from an unrelated subsystem into a visible autosave outage:
- Reported symptom: FigJam files appearing to suffer autosave failures.
- Actual dynamics: FigJam connectors (the arrows that attach to objects) oscillated — every collaborator's client kept re-computing and re-sending slightly differing connector state, producing "a huge number of multiplayer messages, which overloaded the multiplayer and autosave systems."
- Root cause: a six-month-old PR change in the layout subsystem corrupted connector attachment state. Connectors' state is a function of the layout of the objects they attach to (position/size of a sticky note → where the connector meets it); any layout-system bug can therefore silently corrupt connector state, which then cascades through Multiplayer's fan-out machinery.
- Detection: code audit of the connector subsystem revealed nothing. Debug-message instrumentation across the codebase eventually localized the bug three subsystems away from the reported symptom.
Multiplayer is the amplification path, not the source, but it is where the load manifests. A connector-state corruption that would otherwise be a single-user visual bug becomes a multiplayer message storm as every collaborator's session re-syncs on the oscillating state. This is the canonical wiki instance of concepts/interdependent-systems landing on a real-time collaboration server.
Implications (not all discussed in the post, but consistent with observed practice):
- Rate limiting per-object edit frequency inside Multiplayer would dampen oscillation storms regardless of which upstream subsystem produced them. Post doesn't confirm whether this is in place.
- Cross-subsystem invariant checks — e.g. "connector attachment must be a pure function of the attached object's layout at commit time" — could have surfaced the layout-subsystem bug at CI time rather than six months into production. Consistent with Figma's patterns/consistency-checkers practice (applied in Billing, not confirmed here).
- patterns/bisect-driven-regression-hunt is the debugging discipline the incident eventually required.
Game-engine framing¶
The same 2026-04-21 post frames Multiplayer as one of Figma's "systems" in the game-engine sense. The name was deliberately borrowed — "we called it 'multiplayer'" — and the architectural role is the same as a game engine's multiplayer system: reconcile per-frame edits from N concurrent participants into a coherent shared world state. The collision-like flower-vs-building-block example the post uses ("Player A might lay down a building block on the street, while Player B moves a flower into the same spot") is directly analogous to Figma's edit-conflict cases ("add, move, and edit things at the same time").
Tables-in-FigJam rollout added an observer-side animation layer (patterns/observer-vs-actor-animation) to smooth how remote Multiplayer edits visually arrive — live feedback for the acting user, animated transitions for observers.