Limits and metering
Service limits, resource budgets, and usage metering. See pricing for plan allowances.
Room settings
See Room configuration for the complete option table, defaults, and units. This page lists service limits and what happens when they are reached. Plan allowances are on pricing.
Schema limits
defineSchema throws on a schema past one of these, so it fails at build time and never reaches a
server. The complete list, including string, list and id bounds, is in schema limits.
| Limit | Value |
|---|---|
| Fields per entity or singleton | 255 |
Message shapes declared under messages | 256 per schema |
RPCs declared under rpc, both directions together | 60,000 per schema |
Where a room runs
Your project gets its own server, sized from the size class your room types declare. A bigger class gets you a bigger server. Declaring nothing gets you Standard.
Every project starts on one server and gets another when its rooms need one. The room budget below applies to each server on its own, so a project on three servers has three servers’ worth of budget rather than one budget divided three ways.
Room budget
One server holds a fixed number of awake rooms. irtio sets the number, per size class:
| Size class | Room memory | Rooms per server |
|---|---|---|
| Standard | 64 MB | 20 |
| Large | 256 MB | 8 |
| Extra large | 1 GB | 3 |
A room counts against the budget while it is awake. Asleep rooms cost nothing, so a project with thousands of rooms and twenty awake ones fits on one server.
A project with room types in more than one class shares one budget. Each awake room takes a slot of its own class: five awake Standard rooms use a quarter of the budget, one awake Extra large room uses a third. The server is sized for the largest class the project declares.
When a project fills a server’s budget, irtio adds a server and the next new room starts there. See when a project gets another server.
Players per room
maxClients defaults to 64. Measure your game’s capacity with load tests, then set a count that stays within your tick-time and bandwidth budgets.
Relay rooms
Relay rooms provide messaging and, with a deployed schema, shared state. See relay rooms for setup and ownership policies.
| Limit | Value | At the limit |
|---|---|---|
| State per room | 512 KB | Write refused with E_STATE_CAP |
| Records per collection | 1024 | Add refused with E_RECORD_CAP |
| Write operations per client per second | 35 | Whole frame refused with E_WRITE_RATE |
| Ownership request response | 5 seconds | Unanswered request refused |
Persisted player fields (persist) | 4 KB per player | Schema refused at compile |
| Players with persisted fields | 10,000 per project | Bumpable on request to support@irt.io |
| Persisted fields kept | 90 days from the last write | The player starts at schema defaults |
Changing a deployed relay schema resets the room’s stored state and reconnects clients against the new schema. Deploying the same schema preserves state.
Ports a client connects to
| Traffic | Port range |
|---|---|
| Room traffic | 443, TCP (a secure WebSocket to <region>.irt.io) |
| Voice media | 40000-40999, UDP and TCP, and only where voice is switched on. TCP on the same ports is the fallback for networks that block UDP |
Room traffic goes through the regional router, so a player’s network only needs outbound 443. The game servers accept room connections from the router alone. What protects a room is the join credential your project issues.
Memory
Declare a room class to set its memory ceiling: Standard 64 MB, Large 256 MB, or Extra large 1 GB. Server sizing changes take effect on a restart. Keep collections bounded and measure memory during load tests.
The ceiling covers everything the room holds, not only its JavaScript objects. Typed arrays, Buffers, and WebAssembly memory count against it the same way, so a room that keeps decoded assets, audio buffers, or a WebAssembly heap can reach its ceiling while its object graph stays small.
If a room exceeds its ceiling, it restarts from its saved state and the log line names what it was holding and how much of that was off the JavaScript heap. Three restarts in a minute close the room.
When the server itself nears its memory limit, it refuses new rooms with E_TENANT_WAKING, which clients retry, and puts rooms with no players in them to sleep early. Rooms with players keep running. If there is no such room left to sleep, the server restarts its largest room from that room’s saved state, and changes since that room’s last snapshot are lost.
Connection limits
Enforced by the server, per connection or per project. Exceeding one closes the socket or refuses the frame rather than degrading quietly.
| Limit | Default |
|---|---|
| Frames per second per connection | 240, with a burst allowance of twice that |
| Joins per client address per minute | 600, per server. The whole project is also capped at 6,000 a minute, per server, which one address cannot reach |
| New connections per client address per minute | 120, per server. An IPv6 address is counted by its /64 |
| Chat line | 512 bytes of UTF-8. Refused by name and never trimmed. See chat |
| Chat lines per second per connection | 1, with a burst allowance of 5. The line that does not fit is refused by name, told once, and the connection stays open |
| Outbound buffer per socket before it is closed | 4 MiB for a player, 256 KB for a viewer, plus the size of a WELCOME still being sent. See slow clients |
| Time to send the join handshake after connecting | 5 seconds |
| Declared room class | Standard: 64 MB; Large: 256 MB; Extra large: 1 GB |
| Room worker heap | Derived from your server’s memory in production, so a room reaches its own limit before the server reaches its. 256 MB in local dev |
| Awake rooms per server | 20 Standard, 8 Large or 3 Extra large, per server. A project gets another server when one is full, except on the free plan, which stays on one |
| Rooms awake at once, free plan | 10, per server |
| New rooms per client address per minute | 60, per server. A join to a room the server already holds in memory is not counted; waking a hibernated room counts as a start. Refused with E_RATE_LIMITED |
| Players in one relay room | 64, or 16 on the free plan |
| Relay message size | 64 KB, counted with the message envelope. Refused with E_WRITE_REJECTED and the connection stays open. See relay rooms |
| Relay message bytes per second per connection | 256 KB, with a burst allowance of 1 MB. The message that does not fit is dropped, the sender is told once with E_RATE_LIMITED, and the connection stays open. Rooms with deployed room code are not counted |
| Viewers in one relay room’s audience | 256, or 64 on the free plan. Counted separately from the players, so a full audience never costs a player their seat. Refused by name with E_AUDIENCE_FULL. See audience rooms |
| Messages per second from one audience peer | 2, with a burst allowance of 10. The message that does not fit is dropped, the sender is told once, and the connection stays open |
| Audience credentials minted per room | 512, with a refill of 600 an hour |
| Audience credentials minted per IP address | 30, with a refill of 60 an hour |
| Audience credential lifetime | 3 hours. Scoped to the project and room it was minted for |
| Encoded state in one relay room | 512 KB. The write that would cross it is refused by name and the room keeps serving. See relay rooms |
| Records in one collection of a relay room | 1024. The add is refused by name |
| Write operations per client per second in a relay room | 35, counted per operation and not per frame. The frame that does not fit is refused whole by name and the connection stays open |
Ticks of body poses kept for room.rewind | Whatever physics.history says, 0 to 240 (one second at the maximum tick rate). Off unless declared. Costs about 120 bytes per body per tick of room heap, so 100 bodies at depth 30 is about 350 KB. See lag compensation |
| Declared history channels per physics collection | 8. Off unless declared. Costs channels x depth x bodies x 4 bytes of room heap, so 4 channels at depth 30 over 100 bodies is about 48 KB on top of the pose cost. See lag compensation |
| Room worker restarts per minute before the room closes | 3 |
Slow clients
When a socket’s outbound buffer exceeds its budget, irtio closes it with code 1008 and reason E_SLOW_CONSUMER.
| Seat | Buffer budget |
|---|---|
| Player | 4 MiB |
| Viewer | 256 KB |
The budget is per socket. While a join or resync WELCOME is still leaving the server, the budget
is raised by that welcome’s own size and returns to normal once it is sent, so a large state
snapshot does not by itself cost a player the connection. There is no throttling. Reconnect to
receive a fresh snapshot in a room with state. These limits also apply to platform connections
forwarding player traffic through relay rooms and are not configurable per project. Refused bytes
are not billed as egress.
Platform error text
A message irtio writes for one of its own errors is at most 96 bytes of UTF-8, so a str(96) field
can always carry one. That covers every code in the error catalogue and
every refusal a room API rejects with, including the replay walls. A message that would run longer
is cut on a character boundary and ends with a single …, so it is always valid UTF-8 and you can
tell a cut message from a short one.
The bound is on irtio’s text, not yours. Your own data still has to fit the field you declared for
it, and @irtio/schema throws on a value that does not rather than trimming it. See schema limits.
The rows marked “per server” are abuse limits, not per-project budgets. They stop one server being flooded, and a project on more than one server has each of them enforcing its own copy. See limits per server.
Storage limits
| Limit | Value |
|---|---|
| Save generations kept per room, per deployment version | 10 |
| Player KV value | 16 KiB |
| Player KV key | 256 bytes |
| Player id | 256 bytes |
| Distinct keys one player may hold in a project | 128 |
| KV rows one project may hold | 1,000,000 |
| Player KV reads per project | 12,000/min, burst 3,000 |
| Player KV writes and deletes per project | 6,000/min, burst 2,000 |
| Uploaded room bundle | 32 MiB |
Going over a KV limit rejects the write with a named error code rather than truncating the value. See saves and restores and the error catalogue.
Public API limits
Quick match, player identity and leaderboard reads are open endpoints. A stranger holding your project key can call them, because that is what makes them work from a browser with no login. So they are rate limited on two dimensions at once, per calling address and per project.
The address limit stops one machine flooding. The project limit stops a botnet making one customer everyone else’s problem. The address allowance is drawn from first, so a single flooding client runs out before it can spend the allowance everyone else is sharing. An IPv6 address is counted by its /64, the block one home connection is given.
The project allowance is only spent by requests about a project that exists. On the identity exchange it is only spent by a valid identity, so a flood of junk credentials cannot use up the allowance your real players need. Link code redemption is the exception by design: every guess against your project counts, because that count is what keeps a short code safe to type.
| Endpoint | Per address | Per project |
|---|---|---|
POST /match (join a queue) | 30/min, burst 10 | 600/min, burst 120 |
POST /match/party (mint a party code) | 20/min, burst 5 | 300/min, burst 60 |
GET /match/status (queue depth) | 300/min, burst 30 | 3,000/min, burst 300 |
POST /v1/identity (mint an identity) | 60/hour, burst 30 | 600/min, burst 600 |
POST /v1/identity/assertion (exchange) | 60/min, burst 20 | 3,000/min, burst 300 |
GET /v1/leaderboard/... (read a board) | 120/min, burst 30 | 3,000/min, burst 300 |
POST /v1/account/link-code (mint a link code) | 6/min, burst 3 | 300/min, burst 60 |
POST /v1/account/link (redeem one) | 10/min, burst 5 | 300/min, burst 60 |
/v1/account, /v1/account/devices (list, revoke, delete) | 30/min, burst 10 | 600/min, burst 120 |
GET /v1/presence (where are these players) | 120/min, burst 30 | 3,000/min, burst 300 |
GET /v1/presence?wait= (watch for a change) | 20/min, burst 5 | 600/min, burst 120 |
POST /v1/audience (mint an audience credential) | 60/hour, burst 30 | 600/hour, burst 512 per room |
Audience credential minting is limited per IP and per room. See audience rooms.
Leaderboard limits
| Limit | Value |
|---|---|
| Board rows one project may hold, across all boards | 1,000,000 |
| Board name | 1-64 characters of a-z 0-9 . _ - |
| Score | a whole number, within the safe-integer range |
Rows in one top read | 10 by default, 100 maximum |
Neighbours on each side of an around read | 5 by default, 25 maximum |
| Device credentials one account may hold | 10 |
| Link code lifetime | 10 minutes, single use |
Scores are integers by decision, not by omission. See leaderboards.
Quick-match limits
| Limit | Value |
|---|---|
| Party size a queue may declare | 2 to 64 |
| Queues one project may declare | 32 |
| Queue name | 1-32 characters of a-z 0-9 . _ - |
| Default wait before “no match” | 30 seconds |
| Longest wait a client may ask for | 2 minutes |
| Live tickets one identity may hold in one queue | 1 (queueing again retires the earlier request) |
| Party size | 2 to 64, and never more than the queue seats |
| Party code lifetime | 10 minutes (expiry stops new members joining, not members already queued) |
| Room occupancy freshness for a backfill offer | 10 seconds |
Skill rating limits
| Limit | Value |
|---|---|
| Players named in one result report | 2 to 64 |
| Finishing place | a whole number, 1 to 64 (ties share a place) |
| Rating rows one project may hold, across all queues | 1,000,000 |
| Starting rating for a player who has never played | 1500, deviation 350 |
| Rating band at 0 seconds waited | 100, plus 100 every 5 seconds, unbounded after 60 |
A rating is written only by room.ratings.report from your own room code. There is no
client-originated path and no CLI submit, for the same reason there is none for leaderboards.
Client-side prediction
| Budget | Default | Beyond the budget |
|---|---|---|
| Non-owned predicted bodies | 64 | Bodies use kinematic proxies |
| Proxies | 256 | Bodies have no local collider |
Configure maxPredictedBodies and maxProxyBodies in the client’s physics options. Monitor stats.absent and stats.lastResimMicros. See prediction.
Room log limits
| Limit | Value |
|---|---|
| One log line | 8,192 characters (UTF-16 units). The rest is cut. In irtio logs and the dashboard a long line may be cut without a marker; in local irtio dev it ends with … [truncated N chars] |
| Log ingest per project | 256 KiB per minute, counted after the per-line cut |
Lines over the per-minute budget are dropped, not queued, and nothing in irtio logs marks the gap.
If verbose lines go missing while you debug, log less or log a summary instead of a whole object.
See logs.
Retention
| Data | Kept for |
|---|---|
| Log lines | 7 days |
| Log lines, buffered inside your project’s server | the last 500 per room |
| Rows in the room list | pruned 30 days after the room was last reported. This removes the listing entry only; the room’s stored state is not affected |
| A room’s stored state (snapshot, saves, alarms) | forever, unless its type declares a retention window |
| Save generations | the newest 10 per room per deployment version |
| Deployments | forever, because old snapshots need their schema to decode |
Room state is kept forever by default. A room type can opt into having its rooms removed a set time
after they last went to sleep by declaring retention: '10m' (see room types). The window is a floor: the sweep runs every 15 minutes, so
a room is deleted on the first sweep after its window lapses.
Usage and metrics
The dashboard and irtio usage show room hours, voice participant-minutes, storage, and data out. See pricing for rates and allowances.
Operational metrics include sockets, roomsByState.*, ingressBytes, egressBytes, and rss. Use them to monitor connections, room lifecycle, traffic, and memory. Hibernated rooms do not accrue room hours.
Usage caps
Each billed meter has a cap, and what happens at the cap depends on whether the account has a card on file.
| Tier | At the cap |
|---|---|
| Free, no card | A hard wall. New joins, new rooms, new voice calls and new writes are refused. Nothing is deleted and nothing already running is killed |
| Card on file | An alert. The billing page shows it, one email goes out, and the project keeps being served |
| Owner-set ceiling | A hard wall, whether or not there is a card. Setting a ceiling is asking to be stopped, so you are stopped |
Set a ceiling per project and per meter with PATCH /v1/projects/<id>/caps, and clear one by setting
it to null. GET /v1/projects/<id>/caps shows where a project stands.
The voice wall refuses new calls, not calls already running. A participant who joined before the wall keeps talking until they leave.
Nothing already playing is cut off
A cap refuses new work, never work in flight:
- A player connected before the cap tripped keeps playing until they leave. Their session is not closed, their room is not stopped, and their writes keep being accepted.
- A room that already exists keeps running, and other players can still join it. Only a new room is refused by the room-hours cap.
- A storage cap refuses writes. Reads keep serving and deletes keep working, which is how a project gets back under the line.
- Nothing is ever deleted by a cap.
The cost of that grace is that data out keeps accruing under sessions that were already open. In practice session churn bounds it, and the overage is preferred to disconnecting somebody mid-game.
Enforcement timing
Usage caps update asynchronously. Room-hour enforcement can lag by about 95 seconds; storage enforcement can lag by about 17 minutes. A server temporarily unable to refresh caps keeps its last received state. Use headroom when choosing a ceiling.
Data-out enforcement
At the data-out allowance, sockets close with code 4290 and rooms hibernate with saved state. The dashboard warns at 80% and at the limit. Allowances reset with the billing period; see pricing.
Enforcement works in budget chunks, so usage can exceed the allowance by up to roughly 5 GB before forwarding stops. A separate sustained-rate limit drops messages if a project exceeds it for more than ten seconds, with a room log warning.
Replays
A room type that sets replay keeps recent state in memory so room.replay.clip(seconds) can save
it. Nothing is stored until a clip is taken. With replay: { record: true } the room stores the
whole match instead, in the background, as the oldest seconds fall out of memory. See replays.
| Limit | Value | When you reach it |
|---|---|---|
| Recorded footage | seconds, 60 by default, 300 at most | The oldest seconds are dropped |
| Memory per recording room | 8 MB | The oldest seconds are dropped early, so a busy room holds fewer than seconds |
| One recorded moment | must fit in that 8 MB | A room whose whole state is larger than the ring cannot record at all: recording stops, a line says so in the room’s logs, and room.replay.clip() is refused with E_CLIP_DISARMED |
| Clips per room | one per second | The call is refused with E_CLIP_COOLDOWN |
| Clip size | 16 MB | The clip is refused with E_CLIP_TOO_LARGE |
| How long a clip is kept | ttl, 30 days by default, '1m' to '3650d' | The clip is deleted and its link stops working |
| One recording | 256 MB of stored bytes | Recording stops, a line says so in the room’s logs, and clips keep working |
| Recording chunks waiting on the store | 2 | Recording stops rather than leaving a hole in the footage, a line says so in the room’s logs, and clips keep working |
| Footage lost if the room stops without warning | up to seconds | Everything already stored still plays |
| How long a recording is kept | ttl, 30 days by default, counted from its first stored chunk | The recording is deleted and its link stops working. A ttl shorter than the match deletes the recording while it is still being made |
| Excerpt window | 30 seconds | The call is refused with E_EXCERPT_TOO_LONG, never shortened silently |
| Excerpts per room | 4 at once, one more per second | The call is refused with E_EXCERPT_COOLDOWN |
| Excerpt output | 4 MB of frames | The call is refused with E_EXCERPT_TOO_LARGE. Ask for fewer seconds, or name the collections and ids you need |
room.replay.excerpt() reads the same in-memory footage back into the room as plain frames. It is
experimental, it stores nothing, and its limits are counted separately from clips, so a killcam that
reads footage does not use up the room’s clips. Excerpt output is not filtered by interest
management: it carries what the recorder recorded (public collections unless replay.view or replay.omniscient says otherwise), and the room decides who may see it.
Set ttl in the room config to change how long clips and recordings from that room type are kept.
The lifetime is fixed when the clip is saved, so changing ttl does not re-time clips you already
saved.
A link can keep working for up to a minute after the clip expires, so the '1m' floor is honest
to about a minute.
A clip or a recording counts as stored bytes while it is kept, and as bandwidth each time it is watched.
Clips and recordings are stored and served compressed. The recording cap counts compressed stored
bytes; replay() handles decoding.