Video summary
Scaling the Linear Sync Engine
Main summary
Key takeaways
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
.valuebecomes available - components re-render via MobX when cached promises resolve
- the
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