Video summary

Scaling the Linear Sync Engine

Main summary

Key takeaways

Technology

Summary of the Talk: “Scaling the Linear Sync Engine”

Purpose and Scope

  • The speaker presents a real-time sync architecture used in the Linear application.
  • This talk follows up on an earlier “sync engine internals” discussion, focusing on:
    • scaling challenges
    • how the API and architecture evolve in response to those challenges

Core Idea: Real-Time Sync via Local State + Reconciliation

  • The sync engine maintains an in-memory object graph representing application data on the client.
  • Developers interact with the object graph as though it were local, while the engine handles:
    • persistence
    • rollback when server validation rejects optimistic changes
    • offline support via a queued transaction log
    • conflict resolution through rebasing/handling concurrent edits
  • A key emphasis is engineering productivity: teams shouldn’t need to reimplement networking, error handling, offline queues, and reconciliation for every feature.

System Benefits (Product + Infrastructure)

User-Side Benefits

  • Real-time UI updates when other users change data
  • Reduced need for repeated page reloads / network refetching
  • Potential offline capabilities via queued mutations

Server-Side Benefits

  • Clients load most state locally, sending the network mainly for mutations/changes
  • Potentially fewer servers than request-per-page architectures

Client-Side Data Model and UI Reactivity

Object Pool → Object Graph

  • The client stores many raw records in an object pool (loaded from local storage and/or network).
  • The engine hoists and reconstructs a usable object graph (e.g., Workspace/Organization → Teams → Issues).

MobX Integration

  • The UI uses MobX observability:
    • model objects become observable
    • when observable properties change, dependent UI re-renders automatically
  • Local edits and network-received updates go through the same pathway, keeping UI logic consistent.

Transaction Queue and Mutation Lifecycle

Optimistic Updates

When a user edits a model object:

  • The change is applied optimistically in memory for responsiveness
  • A transaction is created and sent to a GraphQL API
  • The transaction is stored locally so it can be replayed after refresh/offline
  • If the server rejects the change:
    • the engine rolls back in-memory state
    • the UI may display the server error (e.g., toast/dialog)

Batching and Broadcasting

  • Multiple object changes are batched into GraphQL mutations.
  • Server workflow:
    • writes to Postgres
    • records a sync action in a table
  • A dedicated sync server:
    • reads sync actions
    • distributes them to connected clients via web sockets

Ordering + Refresh Correctness (Deduplication)

The architecture accounts for scenarios like “refresh during in-flight transactions”:

  • Clients keep their pending transaction queue
  • A deduplicator checks transaction IDs against socket updates to avoid reapplying
  • Deletion conflicts may still occur, but are handled (often via error toasts)

Bootstrap vs. Delta Sync (Initial State, Then Staying Current)

Initial Approach: Full Bootstrap

  • On first load, the client fetches all workspace-visible data via a GraphQL API.
  • The client stores results in IndexedDB and constructs model objects + the object graph.

Delta Sync

  • After bootstrap, the client uses web sockets for incremental updates.
  • Delta sync was added to handle:
    • slow bootstrap
    • offline gaps
  • Mechanism:
    • client sends last-seen sync ID
    • server returns a “fast-forward” dataset since that sync ID

Early Scaling Problem

  • Delta sync became heavy:
    • large numbers of changes caused expensive serialization
  • The sync server sometimes paused other updates to serve delta bootstrap for one client, creating complexity and bottlenecks.

Key Architectural Change: Move Delta Sync to GraphQL

  • Delta sync was moved from the sync server to the GraphQL API (seen as more reliable/scalable).
  • A client-side queue was added to prevent race conditions:
    • while waiting for delta data, incoming websocket updates are queued in memory
    • after delta is applied, queued updates are flushed to local storage
    • the client then resumes normal real-time behavior

Performance Scaling: Fixing Slow Bootstrap and API Bottlenecks

1) Client Bootstrap Too Slow (MobX Object Construction Cost)

Large workspaces (tens/hundreds of thousands of objects) were slow due to:

  • constructing model objects
  • making them observable via MobX

Solutions:

  • Partial bootstrap: load only immediately needed models first
  • Lazy collections: delay hydration of heavy models (e.g., comments/history) until accessed

Lazy Collections + Suspense UX

  • Lazy collections use the same API shape, minimizing engineer-facing changes.
  • When data isn’t loaded, hydration occurs from:
    • IndexedDB first (then later network)
  • UI may temporarily show an empty state → hydrated state, mitigated with:
    • React Suspense boundaries
    • fade-in behavior
  • Pre-hydration tooling reduces perceived latency.

2) GraphQL Bootstrap Crashes (Memory Pressure)

GraphQL built the full response in memory before sending, causing:

  • large memory blobs
  • duplicated in-memory copies during serialization

Fix:

  • introduce a streaming REST endpoint (internal)
    • performs DB streaming
    • sends results incrementally with lower memory usage
    • can stream to disk if the client is slow

Backend Scaling: Keeping Postgres Stable

Postgres Load for Frequent Full Bootstraps

  • Full bootstrap requests increasingly stressed Postgres, especially with:
    • large organizations
    • growing traffic

Cache/Dump Database Strategy

  • Introduced a cache-like database (mentions of GCP Bigtable trials).
  • Periodic dumps:
    • serialized model objects stored in a fast-read format
    • include last sync ID so the client can catch up via delta sync
  • The streaming REST endpoint chooses the best source:
    • use cached dumps if valid/recent
    • otherwise read from Postgres and potentially generate a new dump

Note: the talk notes that delta sync ultimately targets Postgres in the described architecture, and some “partial/real-time reading paths” details are “complicated in code.”


Final Scaling Direction: Network / On-Demand Data with Batch Loading

New Approach: Batch Loader Between Client and Network

Even with caching and streaming, largest orgs can still be too slow for full bootstraps. So the next system adds a batch loader to stream missing data on demand.

Batch Loader Goal

Replace “load everything up front” with:

  • partial bootstrap for first-screen essentials
  • defer heavy collections (e.g., issues, attachments) until needed

Example: Issues/Attachments Lazy + Batching

Previously:

  • requesting attachments per issue could trigger dozens of network requests
    • e.g., 50 issue rows → 50+ attachment queries returning mostly empty results

With batch loading:

  • aggregate requests over ~tens of milliseconds:
    • deduplicate
    • group into an optimized network query set
    • ideally turn many small requests into one request from the streaming endpoint
  • backend returns combined results; multiple pending collection requests resolve together.

Engineering Impact: Minimal API Change

  • Collection access patterns remain the same:
    • engineers keep using lazy collections
    • hydration source can switch from disk → network transparently
  • UI uses Suspense strategically to target loading under noticeable thresholds (e.g., < ~500ms or under ~1s; mention of a longer loader after ~4 seconds fallback).

Technical Details: Typed Lazy References (“Cached Promises”)

TypeScript-facing behavior described in the talk:

  • when an entity property (e.g., parent issue) is lazy:
    • it becomes a cached promise
  • once resolved:
    • the .value becomes available
    • components re-render via MobX when cached promises resolve
  • hydrate():
    • returns a type-safe “hydrated object”
    • converts cached promise fields into concrete non-optional values
    • hydrated objects include hydrated collections as needed while preserving consistent collection behavior

Review/Guide/Tutorial Elements Mentioned

  • No explicit developer “review” format or step-by-step tutorial was presented, but there were implementation-oriented mechanics:
    • how lazy collections work
    • how Suspense boundaries prevent empty flashes
    • how queued websocket updates avoid race conditions during delta sync

Main Speakers / Sources

  • Main speaker: Linear engineer referred to as Alex in the dialogue portion.
  • Technical references included:
    • Linear sync engine architecture
    • MobX
    • IndexedDB
    • GraphQL API
    • web sockets
    • Postgres
    • internal streaming REST endpoint

Original video