Limits
What one Suite instance accepts, and what happens at the boundary. The numbers
below are read from the code that enforces them, so they cannot drift from the
behaviour. Two surfaces enforce them: the relay (apps/collab-server) and the
publish validator (packages/graph).
Workspaces and storage
Section titled “Workspaces and storage”| Quota | Variable | Self-host | Hosted (SUITE_HOSTED=1) |
|---|---|---|---|
| Workspaces per licence or instance | SUITE_MAX_WORKSPACES |
unlimited | unlimited |
| Document bytes per workspace | SUITE_DOCUMENT_BYTES_PER_WORKSPACE |
unlimited | unlimited |
| Projection bytes per workspace | SUITE_PROJECTION_BYTES_PER_WORKSPACE |
unlimited | 32 MiB (33,554,432 bytes) |
| Attachment bytes per workspace | SUITE_ATTACHMENT_BYTES_PER_WORKSPACE |
unlimited | unlimited |
A hosted licence carries its own workspaces, documentBytes and
attachmentBytes; the tighter of the licence and the instance value wins.
Self-hosting has no licence unless SUITE_LICENCE supplies one, so the storage
quotas above are unlimited until then. unlimited is also accepted as a value
for every quota.
What an account actually gets
Section titled “What an account actually gets”The table above is the instance ceiling, not the answer a reader wants. A hosted
account is capped by its licence row, and a new organization is born with no
workspace at all: local and offline use is free and needs no account, so an
account is what buys sync. These are the numbers the seat check enforces, read
from FREE_LICENCE_TIER.
| Cap | Born-with licence | Plan tier |
|---|---|---|
| Workspaces | 0 | from the plan |
| Seats | 1 | from the plan |
| Document bytes per workspace | 0 bytes | from the plan |
| Attachment bytes per workspace | 0 bytes | from the plan |
A plan sells one seat in this build (ADR-0082): there is no per-seat price, so a
plan that declared more would bill one person and admit one person. The plan
figures are in deploy/stripe/fixtures.json and are not repeated here, because
they change with pricing rather than with code.
The projection budget is also a serving ceiling. A projection room whose stored
document is over it refuses a new join with document-limit, because
each peer holds its own copy of the document in memory. Its bytes are kept, the
owner’s workspace is not affected, and the share re-opens once the workspace is
trimmed or the budget is raised.
Links, cards and selection
Section titled “Links, cards and selection”| Cap | Variable | Value | Refusal code | Where it is enforced |
|---|---|---|---|---|
| Active share links per workspace | SUITE_MAX_SHARES_PER_WORKSPACE |
100 | none | Creating a link; existing links keep resolving |
| Cards in one selection share | SUITE_MAX_SELECTION_CARDS / MAX_SELECTION_CARDS |
200 | none | createShareQuotaGuard, shares.ts |
| Cards in one share projection | SUITE_MAX_PROJECTION_CARDS |
2,000 | card-limit |
projectionCardCount, projection/snapshot.ts |
| Cards per workspace | none | bounded by the document budget | none | A workspace over its budget pauses new share links; no card count is refused |
| Single attachment upload | none | 16 MiB (16,777,216 bytes) | none | readBytes, http/attachmentHttp.ts |
Cards per workspace has no number of its own on purpose. What a board may hold is bounded by the document budget, because that is the number that costs disk, and by the card cap above, because that is the number that costs the event loop. Cards in one selection on the canvas stay bounded by what the browser can hold.
Publish caps
Section titled “Publish caps”A draft is written by a browser that any client can run, so the publisher
re-checks these on the projected plan. They are constants in
packages/graph/src/publishValidation.ts; only the two byte caps have an
environment override. A refused publish answers 409 and writes nothing.
| Cap | Variable | Value | Refusal code |
|---|---|---|---|
| Text bytes per card (title and body) | SUITE_PUBLISH_MAX_CARD_TEXT_BYTES |
1 MiB (1,048,576 bytes) | none |
| Bytes in one published draft | SUITE_PUBLISH_MAX_DRAFT_BYTES |
20 MiB (20,971,520 bytes) | draft-too-large |
| Largest absolute coordinate, size or z-order | none | 10,000,000 | none |
| Deepest rich-text nesting | none | 64 | none |
| Section title bytes | none | 1,000 bytes | none |
| Edge label bytes | none | 500 bytes | none |
Only the draft byte cap has a code of its own. It is the one refusal an editor
can act on without reading the plan: the route answers
draft-too-large in the 409 body, so the editor
says the draft is out of room rather than listing an unsupported change beside a
size limit nobody can see.
Transport
Section titled “Transport”A document reaches a peer over the room socket as Loro updates. loro-protocol
refuses to encode a message larger than 256 KiB (262,144 bytes), so the relay
sends a larger payload as a fragment header followed by chunks, and the peer
reassembles the whole batch before it applies a byte. There is no document-size
ceiling on the socket: a 2 MiB workspace arrives the way a 20 KB one does.
| Frame | Source | Value |
|---|---|---|
| Largest message either peer encodes | loro-protocol MAX_MESSAGE_SIZE |
256 KiB (262,144 bytes) |
| Payload carried by one fragment | sync/outboundFragments.ts MAX_FRAGMENT_BYTES |
240 KiB (245,760 bytes) |
| Fragments in one document update | none | as many as its bytes need |
Before that was wired, the relay handed a joining peer its whole document in one message. A board of 240 cards fit and a board of 400 did not, so syncing stopped near 250 KB without a number anyone had chosen. That ceiling is gone: what bounds a workspace here is the document budget above, which is a disk quota.
Concurrency
Section titled “Concurrency”These are per room, not per client address. The rate limits in ../how-to/operate.md answer “may this caller do more of this soon”; these answer “is this room too big to serve”, which is the question one huge board raises.
| Limit | Variable | Value | Refusal code | Behaviour at the limit |
|---|---|---|---|---|
| Peers per room | SUITE_MAX_PEERS_PER_ROOM |
64 | peer-limit |
A join is refused before a session exists, so nothing is written. |
| Outstanding work per room | SUITE_ROOM_OUTSTANDING_MAX |
8 | room-backpressure |
A projection rebuild is refused rather than queued; the next save tries again. |
| Projection build time | SUITE_PROJECTION_BUILD_MS |
5,000 ms | projection-build-limit |
The room keeps its last good projection and stops rebuilding until a reload or a publish. |
| Open projection rooms | SUITE_PROJECTION_ROOMS_MAX |
128 | none | The least-recently-used room with no connected client is closed and re-opens on demand. |
| Live rooms | none | no cap | none | A room lives while a client is connected; the process is single-replica by design. |
Every projection room records the source version vector it was built from. A
save whose source has not moved since that build is skipped rather than
rewritten, and a skipped rebuild costs one version comparison per open room.
suite_projection_skips_total counts them.
A rebuild follows a save, never an edit: the relay coalesces a room’s writes
at saveInterval (2 000 ms), and the worker then waits
200 ms more (DEFAULT_DEBOUNCE_MS). The floor on
share freshness is therefore one save window plus one rebuild, measured at 2.0 s
p50 and 2.1 s p99 for a 200-card board in four rooms with sixteen readers
(apps/collab-server/bench/report.md). Nothing in the projection path can be
fresher than the save it follows.
Refusal codes
Section titled “Refusal codes”Each cap above refuses with one of these, on the wire where the caller can see it and in the log line that records the refusal.
| Code | Refuses | Variable |
|---|---|---|
document-limit |
Serving a projection room whose stored document is over its budget | SUITE_PROJECTION_BYTES_PER_WORKSPACE |
peer-limit |
A join beyond the per-room peer cap | SUITE_MAX_PEERS_PER_ROOM |
card-limit |
Building a projection from an oversized board | SUITE_MAX_PROJECTION_CARDS |
room-backpressure |
A rebuild for a room already at its outstanding bound | SUITE_ROOM_OUTSTANDING_MAX |
projection-build-limit |
Rebuilding a room that already exceeded the build-time cap | SUITE_PROJECTION_BUILD_MS |
draft-too-large |
Publishing a draft over the projected size cap | SUITE_PUBLISH_MAX_DRAFT_BYTES |
attachment-cap |
An upload that would put a workspace over its attachment budget | SUITE_ATTACHMENT_BYTES_PER_WORKSPACE |
Event-loop lag
Section titled “Event-loop lag”The relay samples event-loop lag every 50 ms and exports the samples as the
suite_event_loop_lag_seconds histogram, with
suite_event_loop_lag_max_seconds for the worst sample so far and
suite_event_loop_lag_breaches_total for the samples over the threshold. A
save is judged by how late the loop was, not by how long the write took: the
document write runs off the loop, so a slow disk shows up as throughput and a
stalled loop shows up here.
Five breaching samples in a row log one line at warn, with
code: "event-loop-lag", the threshold, the run length and the document write
counters for the same window. The counter resets when a sample is under the
threshold, so a second episode logs its own line.
| Threshold | Variable | Value |
|---|---|---|
| Lag sample that counts as a breach | SUITE_EVENT_LOOP_LAG_MS |
100 ms |
Disk watermarks
Section titled “Disk watermarks”| Watermark | Variable | Self-host | Hosted (SUITE_HOSTED=1) |
|---|---|---|---|
| Document store size that logs a warning | SUITE_DISK_SOFT_BYTES |
unlimited | 4 GiB (4,294,967,296 bytes) |
| Document store size that refuses new workspaces | SUITE_DISK_HARD_BYTES |
unlimited | 5 GiB (5,368,709,120 bytes) |
A workspace that already exists keeps saving past the hard watermark; only a new workspace is refused. Nothing is deleted to make room.
Restore targets
Section titled “Restore targets”compose.yaml keeps the stores on the named volume suite-data,
mounted at /data in the app container. A backup or restore
target is that volume, not an individual file: owner hashes, share rows and
documents refer to each other.
| Store | Variable | Path in the container | Survives a container recreate |
|---|---|---|---|
| Documents | SUITE_DB |
/data/documents.sqlite |
yes |
| Share links | SUITE_SHARES_DB |
/data/shares.sqlite |
yes |
| Workspaces | SUITE_WORKSPACES_DB |
/data/workspaces.sqlite |
yes |
| Settings | SUITE_SETTINGS_DB |
/data/settings.sqlite |
yes |
| Licence claims | SUITE_LICENCE_DB |
/data/licences.sqlite |
yes |
| Attachments | SUITE_ATTACHMENTS_DB |
/data/attachments.sqlite |
yes |
Backup and restore use deploy/backup.sh and deploy/restore.sh; the
procedure is in ../how-to/operate.md and
../how-to/release.md.
At the boundary
Section titled “At the boundary”- A save is never dropped. A workspace over its document budget keeps loading and saving for its owner; what it loses is the ability to mint new share links.
- Only the places where nothing has been written yet are refused: a new workspace, a new share link, a new session in a room at its peer or byte ceiling, an oversized attachment, an oversized publish. A refused rebuild costs freshness for one share, never a byte of the workspace.
- A workspace room is never refused for being large, because its owner has to be able to open it to trim it, and a document larger than one frame is fragmented rather than refused. The ceiling that refuses a join is on a projection room, which is a copy the relay can always rebuild.
- Every refusal names the limit, the current value and the variable that sets it, so an operator can answer “why” without reading the code.
- The server validates every quota at boot: a value it cannot read is a startup error naming the variable, not a silent fallback.