Real-time
How subscriptions, deltas, and hibernation fit together.
Last updated:
Every Lunora query doubles as a subscription. Calling useQuery(api.x.y, args)
in React opens a multiplexed WebSocket to your Worker, registers the query
with the resolving Durable Object, and re-renders the component whenever a
matching mutation broadcasts a delta.
import { api } from "@/lunora/_generated/api";
import { useMutation, useQuery } from "@lunora/react";
export const Chat = ({ channelId }: { channelId: string }) => {
const messages = useQuery(api.messages.list, { channelId });
const send = useMutation(api.messages.send);
if (!messages) return <p>Loading…</p>;
return (
<ul>
{messages.map((m) => (
<li key={m._id}>{m.text}</li>
))}
</ul>
);
};Hibernation
Subscriptions are stored on the WebSocket via state.serializeAttachment(...).
When the Durable Object hibernates (no traffic for ~10s) the WebSocket is
suspended without losing the subscription registry; the next message wakes
it up and re-routes deltas exactly where they were going.
This is why your DO bill drops to near-zero between bursts: idle subscribers pay for storage, not compute.
Delta routing
A mutation that writes to messages calls this.broadcastDelta({ table: "messages", ... }).
Each socket carries a SocketAttachment mapping subId → SubscriptionQuery.
Only sockets with a registered subscription whose query.table matches
receive the delta, scoped further by index predicate.
Reconnect
The client (@lunora/client) uses exponential backoff with jitter and
resumes by bookmark: it sends the last delta sequence it acknowledged so
the server can replay anything that was missed during the disconnect.
Why no MQTT / Pub/Sub
Real-time fan-out in Lunora is entirely Durable-Object-based: hibernated
WebSocket subscriptions on ShardDO, type-safe and integrated end-to-end with
your queries, with no external broker to run. Cloudflare Pub/Sub (an MQTT
broker) is therefore a non-goal. It is in beta with gated onboarding and no
Worker binding, and the only capability it adds over the DO path is native-MQTT
device ingest (IoT clients speaking MQTT directly), a narrow niche. We'll
revisit only if Pub/Sub reaches GA and a concrete need to ingest from
native-MQTT devices appears; until then there is nothing to configure.
See also
- Sharding
- API: @lunora/react
- Deployment — platform config & non-goals