Scaling
This page describes the performance characteristics of one server process, how to size it for your workload, and how to grow beyond it. Absolute throughput and latency depend on your hardware, document sizes and traffic mix, so this page describes the properties that hold regardless of environment and how to measure your own deployment.
Performance characteristics of one process
| Property | Behaviour |
|---|---|
| Operation cost | Cell operations apply to the live in-memory document; the engine work per operation is small compared to any full-document read or write. |
| Throughput ceiling | Bound by the Node.js main thread: at saturation the process uses one core. Traffic beyond the ceiling increases latency with concurrency; it does not crash the process. |
| Document count | Throughput does not degrade as more documents are opened. The cache has no per-document contention, so capacity is bound by total operations per second, not by how many spreadsheets are cached. |
| First load | Loading a document into the cache is proportional to its serialized size; subsequent operations run against memory. |
| Memory | Each cached document holds memory proportional to its size; the number of cached documents is capped by gc.max, which evicts least-recently-used documents that have no connected sockets. |
Keep the hot path lean
The engine applies a cell update in microseconds; whole-document work dominates throughput cost. Two rules follow from profiling:
- Do not fetch or serialize the full document per operation. An ownership lookup should project only the access-control fields (owner, privacy, invited users). A persistence handler should write only what changed. A single unnecessary full-document fetch on a multi-megabyte document costs more than a hundred engine operations, and in practice can reduce throughput by two orders of magnitude.
- Prefer incremental adapters for large documents. The MongoDB adapter persists per-operation updates. Adapters that re-serialize the whole document on every change are only suitable for small spreadsheets.
Scaling horizontally
The document guid is the atomic unit: all peers of a document must share one process (Architecture). Scale out by partitioning documents across processes with a consistent hash on the guid connection parameter, never by replicating a document across nodes.
upstream sheets {
hash $arg_guid consistent;
server 10.0.0.11:3000;
server 10.0.0.12:3000;
server 10.0.0.13:3000;
}
Every socket connection and REST call carries the guid (in the query string or the URL path), so the same document always lands on the same node, and capacity grows linearly with node count. The only shared component is your database.
Notes:
- Sticky sessions are not sufficient. Session affinity keeps one user on one node; it does not keep one document on one node. Two users of the same document behind plain sticky sessions can land on different processes and diverge without any error.
- Do not add a socket.io Redis adapter to fan out across nodes. Broadcasting an operation to a node that holds no live instance for that document lets peers drift. With guid routing there is nothing to fan out, because all peers are already on the right node.
- Plan capacity per node as
expected ops/s ÷ measured per-process ceiling, and memory ascached documents × the per-document footprint of your documents, bounded bygc.max. Both figures come from measuring your own deployment (below).
Common deployment questions
Common questions about multi-instance deployments.
Does the server officially support horizontal scaling with multiple instances?
Yes. The model is room-based partitioning: each spreadsheet guid is served by exactly one instance, and different guids are distributed across instances. Capacity grows linearly with node count.
Can multiple instances serve collaborative editing for the same guid?
No, by design. Each instance holds the authoritative in-memory state and revision counter for its guids. All connections for a given guid, WebSocket and REST alike, must reach the same instance. This keeps ordering simple and makes cross-instance conflict resolution unnecessary in the normal path.
Are sticky sessions required?
Yes, but route by document, not by user. The client sends the guid in the connection query string, so with Nginx use hash $arg_guid consistent;. Plain cookie/IP affinity is not sufficient: it pins a user to a node, while two users of the same document could still land on different nodes and diverge. The same guid-based routing must apply to REST API requests, which operate on the same in-memory instance (the guid is in the URL path, so hash on that segment for REST traffic).
Is the socket.io Redis adapter supported?
Compatible, since it can be passed through the server config options, but not required in this architecture. With guid-based routing, all traffic for a document terminates on its owning instance, so there is no cross-instance messaging in the normal path. Adding a Redis fan-out does not remove the need for guid routing (see the note below); it adds a dependency without removing a constraint.
How is consistency maintained across instances?
The owning instance keeps the spreadsheet state in memory and applies operations serially, so ordering is guaranteed per document. Persistence is delegated to your adapter through events: change fires for each applied operation, in order, so you can persist incrementally (an operation log or per-operation updates) or snapshot every X seconds, and the in-memory state remains the source of truth between writes. Because only one instance owns a guid, there is a single writer per document against the shared backend, so no cross-instance write coordination is needed.
What is the recommended cluster architecture?
The standard Client → Load Balancer → N instances → shared database model is supported with one change: the load balancer must hash on the guid instead of round-robin.
Client → Nginx (hash $arg_guid consistent) → N server instances → shared MongoDB/PostgreSQL/Redis
One sizing note: a single spreadsheet cannot be split across instances, so size each instance for your largest expected document and its peak per-document traffic, based on measurements of your own workload.
Measuring your own deployment
Because results depend on hardware and workload, size a node by driving the REST API of a staging deployment with documents and a traffic mix representative of your application (for example: mostly small cell writes, some range reads, occasional full-data reads and structural operations). Two test modes answer the two sizing questions:
- Closed-loop, find the ceiling. Fire operations as fast as the server answers, stepping up the number of documents. The throughput where latency starts rising is your per-process ceiling; it should stay flat as the document count grows.
- Open-loop, verify a target. Fire operations on a fixed schedule per document, whether or not the previous ones have returned. A server that cannot keep up shows a growing backlog and rising tail latencies instead of slowing the test down, which makes this the appropriate test for a rate target.
Track latency percentiles per operation type (p50/p95), server CPU and memory. The per-process ceiling and per-document memory footprint you measure are the inputs to the capacity plan above.