Where load goes
What each surface costs at scale, and which part of the cluster absorbs it.
Per surface
| Surface | Scales with | Serializes at |
|---|---|---|
| HTTP request | worker count, stateless | nothing |
| Object call | instance count | the instance's home task |
| Object first touch | one placement-store claim | the placement store; the holder cache absorbs repeats |
Database read, read_one | worker count, via replicas | nothing |
| Directory query | class count; one reader per class | the class's reader node |
| Publish | nodes holding followers, plus one per object follower | the publisher's pump |
| KV | the kv store (Postgres, or ScyllaDB at volume) | nothing |
| Queue message | queue count | the queue's home task, 16 messages per firing |
The unit of serialization
The object instance. One task runs its calls one at a time, which is what makes its transactions and its event log trivial; the price is that one instance is bounded by one core and one file.
A hot instance is therefore a modelling problem, not a tuning
problem. Split hot state across instances: a Channel per room
rather than one router object, a counter per shard rather than one
global counter.
What one node absorbs
- Followers: delivery cost is per node for sockets, so co-located sockets are nearly free. See event delivery.
- Database reads:
readandread_oneanswer from a replica without touching the home. Every object method reaches the home. See placement. - Scripts: pointer and blob caches keep the steady state node-local. See the cluster.
- Misrouted object calls: the holder cache forwards them from memory; the placement store is consulted on first touch and after a home moves, not per call.
Where each queue is bounded
flowchart LR
call([call]) --> mb[object mailbox<br/>128 deep: the caller waits]
mb --> gate[ack gate<br/>10 s: then the answer is unknown]
gate --> ship[ship flights<br/>32 at once, 8 reserved for gated writes]
ev([publish]) --> inbox[connection inbox<br/>256: refuse and sever the edges]
ev --> edge[object edge<br/>8 attempts: drop the edge]
msg([send]) --> q[queue message<br/>5 attempts: dead-letter]
| Bound | At | Behaviour |
|---|---|---|
| object mailbox | 128 calls | backpressure: the caller waits, nothing is dropped |
| ack gate | 10 s (OBJECT_ACK_GATE_MS) for a replica quorum or the store | a written call answers "outcome unknown"; the write keeps trying to ship |
| ship concurrency | 32 flights, 8 reserved (OBJECT_SHIP_CONCURRENCY, OBJECT_SHIP_RESERVED) | flights a caller is waiting on always find a slot |
| connection inbox | 256 items | deliveries: refused and the edges severed; client frames: the wire closes |
| object edge | 8 attempts, 500 ms doubling | the edge is dropped |
| queue message | 5 attempts, 2 s doubling to 1 h | dead-lettered |
The platform sheds at the edges and blocks in the middle: the one bound that waits rather than drops is the mailbox, because a call has a caller to answer.
Related
- Event delivery: the fan-out mechanics.
- Object placement: homes, replicas, and recovery.
- Guarantees and limits: the numbers.