Skip to content

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).

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.

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.

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.

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.

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.

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.

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

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
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.

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.

  • 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.