Why Figma's multiplayer needed a journal

Figma's multiplayer service keeps the state of every open file in memory so it can validate, order, and resolve conflicts as changes arrive from connected clients. Because in-memory state is volatile, the service periodically encodes the entire file into a binary format, compresses it, and uploads it to S3. This "checkpointing" happens every 30 to 60 seconds and is fundamental to product features like version history.

That checkpoint-centric design had two serious weaknesses. First, if the multiplayer service crashed, up to 60 seconds of server-side work could be lost. Second, deployments caused problematic write spikes: before closing a file, multiplayer had to ensure all changes were checkpointed, so redeploying the service meant checkpointing every open file at once and hammering the database.

To address both problems, Figma introduced a write-ahead log. The journal is a durable datastore that multiplayer writes to asynchronously as it accepts changes. Every change gets a sequence number, an incrementing integer per file, and every checkpoint records the current sequence number at write time. After a crash, multiplayer loads the latest checkpoint (at most 60 seconds old) and then replays all journal entries with higher sequence numbers to recover the file's latest state. Keeping the journal write latency very low reduces data loss in rare failures to under one second.

The key difference between journal entries and checkpoints is scale. Journal entries are incremental user changes — moving a layer, tweaking text. Checkpoints serialize the entire file, whose complexity grows over a file's lifetime. Journal writes are thus orders of magnitude smaller and far more frequent. They're complementary mechanisms: checkpoints bound recovery time, while the journal bounds data loss.

Deployments also became cheaper. Instead of checkpointing every file before shutdown, multiplayer just closes connections and waits for any unsaved changes to hit the journal. For the 99th percentile this takes less than a second, and because this is normal write behavior, the load on storage stays steady and predictable.

Designing for correctness

The journal concept is simple, but the details mattered. Figma evaluated several datastores before settling on DynamoDB. Postgres was already used heavily in Figma's architecture, but the write volume required horizontal scalability, which ruled it out. DynamoDB was chosen as the backing store.

Clients send updates every 33ms (30 FPS), far more granular than the journal needs. Batching multiple changes into single journal entries improved performance, but required the data model to track both a start_sequence_number and an end_sequence_number per entry. This complicates recovery — loading a checkpoint at sequence number 7 might encounter a journal entry covering sequences 5 through 9 — but it works thanks to Figma's last-writer-wins conflict resolution.

Keeping the file in memory means all clients for a file must connect to the same multiplayer instance, otherwise clients would see divergent state ("split brain"). The journal introduced a subtler hazard: contending instances could write conflicting histories and corrupt the journal. Figma added a lock mechanism via a new DynamoDB table. Multiplayer writes a (lock UUID, file key) pair to take ownership, and journal writes are conditional on that UUID matching. Journal entries are only read after ownership is acquired, and those reads are strongly consistent. This guarantees a single writer per file and handles handoff race conditions cleanly.

Recovery and validation

Figma wanted file data cross-region replicated within 30 minutes for disaster recovery. DynamoDB's global tables were considered but would have increased feature cost by 6x, and AWS confirmed this wasn't the intended usage. Scheduled table backups were too slow at Figma's write volume and table size. Point-in-time incremental backups with cross-region replication weren't available at the time.

The journal itself therefore isn't cross-region replicated. Instead, because checkpoints live in S3 with replication already configured, Figma guarantees all journal changes are checkpointed within 30 minutes. When multiplayer closes a file without checkpointing, it enqueues an async job to trigger a checkpoint after a jittered delay, avoiding deployment write spikes.

Auditing every code path that modifies the file was critical — journal entries depend on the file's context at write time, and replaying them without that context can produce wrong results (e.g., deleting a parent node before a child node was moved out of it). Figma's multiplayer service is written in Rust, and refactoring file ownership into an encapsulated type made it easy to audit every mutable access point and ensure it wrote to the journal and incremented the sequence number. The type system grants read access broadly but makes mutable access without journal writes require deliberately bypassing well-documented guards and code owner review.

Tests covered the refactoring and logic, but Figma also validated against real-world data. Recreating a checkpoint B from checkpoint A plus all intervening journal entries should produce a byte-for-byte identical file blob. During a dark-launch period, changes were written to the journal but not read from it. After roughly 400,000 consecutive successful validations, the feature rolled out progressively to 100% of files.

Today the journal processes over 2.2 billion received changes per day, persists 95% of changes within about 600ms, and has prevented data loss in several incidents. It's also become a standard reference in on-call handbooks for investigating edge-case errors.

What the journal enables next

Adding the journal to multiplayer sets up several improvements, mostly centered on reducing redundant work — whether that work is done by the client, the server, or other Figma services that need to read document state.

Faster file loading

Today when a client opens a file, it can skip the download if its local copy carries the same sequence number as the version multiplayer has loaded. If the server-side file has changed, however, the full file needs to come down again. The journal removes that requirement. A client can instead fetch a small number of journal entries from before the latest checkpoint and apply them locally, catching itself up to the current state without transferring the entire document.

No more checkpoint-on-demand for read services

Figma exposes a public API endpoint that renders a file to an image. If that endpoint is called while the file is being actively edited, multiplayer must currently force a checkpoint so the rendering service can read a consistent snapshot of the document. Checkpoint creation is not cheap: for the worst 5% of files, it can take roughly four seconds or more. With the journal, this on-demand checkpointing becomes unnecessary, which should cut both latency for API callers and the load on multiplayer itself.

Journal data as a platform

There is already an internal service, backed by the Postgres replication log, that lets other Figma teams subscribe to metadata changes. The engineering team wants to build a similar self-service layer on top of the multiplayer journal. That would allow other groups to consume document-change streams and power features like file webhooks, activity graphs, and time-lapse visualizations without putting extra pressure on the multiplayer system.

The journal is part of a broader effort to keep multiplayer stable and reliable — the foundation that makes real-time collaboration practical. Improving load times, removing forced checkpoints, and opening the journal to the rest of the platform are all steps in that direction. Figma is recruiting engineers to continue this work.