Skip to content

Runtime and services

Durable Objects

A named single-threaded object with private storage; we use them for tenant routing, exact rate limits and WebSocket fan-out.

A Durable Object is a small piece of server that has a name and a location. Ask the platform for the object called tenant-directory and you get exactly one of it in the world, requests to it are handled one at a time, and it has storage that only it can read. That combination, a single instance with serialised execution and local state, is the thing ordinary serverless cannot do. It lets you write coordination code without locks, because the object is the lock.

How we use it

Objectify has two. The tenant directory maps each tenant to one of ten SQLite shards, assigns new tenants to the least loaded shard, and holds the lock during a live migration of a tenant between shards: hold writes, copy, switch the mapping, verify, clean up, release. That sequence is only safe because one object processes one request at a time, and the reasoning behind the whole design is in when many small databases beat one big one. The second object is the realtime hub that fans out WebSocket events. DMARC Engine uses one as a rate limiter, where the count has to be right rather than roughly right. atHeartbeat runs two for presence and for pushing notifications over sockets.

What it costs you

Serialised execution means a popular object is a queue. Objectify's tenant directory sits in the path of every single write, so it is allowed to do one cheap lookup and nothing else; the moment somebody adds an audit write or a metrics call inside it, the whole product slows down at once and it is not obvious why.

An object also lives in one place. Edge compute is everywhere, but the object your Worker is talking to is in a specific region, and a user on the other side of the world pays that round trip on every request that touches it. A directory lookup that costs nothing from London costs a couple of hundred milliseconds from Sydney.

Storage is per object, so there are no cross-object queries. Any question of the form "how many tenants are there" needs either a counter you maintain by hand or a fan-out across objects you have to enumerate some other way. Billing includes the time the object is awake and the duration of every WebSocket held open, so an idle connected browser tab is a line on the invoice, not a free resource.

Class changes go through migrations declared in the deployment config, so renaming or removing an object class is a deliberate operation with data consequences rather than a refactor. And it is easy for one to quietly become dead code: Jonosakti has a notifications object in its tree with no binding declared for it, which means that path has never run in production.

When we would choose something else

Where the value can be slightly stale, key-value storage is cheaper, simpler and readable from everywhere, which is why ClassProfile holds its rate limit counters there and accepts that a determined client gets a few extra requests through. If the system already runs a server, Redis does counters, locks and pub/sub with far more operational tooling around it and people who have run it before. For sockets specifically, a conventional Socket.IO server is the better fit when you need rooms, acknowledgements and a polling fallback rather than a per-entity coordination point.

Where we have used it

Every build below lists this in its stack, so the claim is checkable.