---
title: From Human Realtime to Agent Realtime
description: "How NoLag's real-time messaging infrastructure maps to agent coordination. The design decisions behind each pattern and performance characteristics for agent workloads."
excerpt: "How NoLag's real-time messaging infrastructure maps to agent coordination. Topics become task queues, rooms become workflow boundaries, and presence becomes health monitoring."
date: '2026-05-19'
readTime: 10 min read
category: AI Agents
---

NoLag was built to power human real-time experiences: chat rooms where people exchange messages, dashboards where operators monitor live data, and tracking systems where dispatchers watch vehicles move on a map. When we started exploring multi-agent AI systems, we realized the infrastructure we built for humans maps almost perfectly to agent coordination.

This post explains how NoLag's real-time primitives translate to agent coordination patterns, the design decisions we made for each, and the performance characteristics that matter for agent workloads.

## The Mapping

Real-time messaging has three core primitives: topics (named channels for routing), rooms (groups of related topics), and presence (who is connected). Agent coordination has the same three needs: task routing, workflow grouping, and agent health tracking.

```text [Topic Mapping]
# Human chat (@nolag/chat): the topics inside a room
messages     chat messages
_typing      typing indicators, ephemeral
_stream      live token chunks of a streamed message, ephemeral

# Agent coordination (@nolag/agents): the topics inside a room
tasks        task envelopes (Handoff)
results      result envelopes, routed to the dispatcher by filter
state        shared key-value state (Blackboard), published retained
events       observability stream (Observe)
inbox        direct agent-to-agent messages (Inbox)
tools        tool requests (Tools)
approval     human-in-the-loop requests and decisions (Approve), published retained
```

The topic names change. The semantics are different. But the underlying infrastructure - message routing, delivery guarantees, retained state, and fan-out - is identical. Both wrappers publish at the client's default QoS (1 on the broker hop); the two agent topics that hold state rather than a stream, `state` and `approval`, are published retained so a late joiner receives the latest value.

## Topics as Task Queues

In a chat app, a topic is a channel where messages flow from publishers to subscribers. In an agent system, a topic is a task queue. The Handoff pattern uses `tasks` as a work queue: the orchestrator publishes task envelopes, workers subscribe and act on the ones whose `capability` they registered for.

The property that turns a topic into a queue is not a QoS level, it is load balancing. By default every subscriber to `tasks` receives every task, which is what you want for a single worker per capability and wrong for a pool of identical workers. Connect the pool with `loadBalance: true` and a shared `loadBalanceGroup` and the broker delivers each task to exactly one member of the group. The agents SDK inherits that setting from the core client for `tasks` and `tools` only, and forces it off for the broadcast topics.

Retained messages serve a different purpose in each context. For chat, retention on the broker is not used: history lives in your own store. For agents, retention is how `state` and `approval` behave like a whiteboard rather than a stream: whoever joins sees the current value without waiting for the next write. Holding a task until a worker is available is a separate feature, replay, and it applies only to load-balanced worker groups with persistent sessions on the hosted platform (see [replay](/docs/concepts/replay)); a lone worker that was offline when a task was dispatched does not get it later.

## Presence as Health Monitoring

Human presence is simple: online, away, or offline. Agent presence needs more metadata: capabilities, model, provider, whatever routing needs. We extended the presence payload to carry this information without changing the underlying protocol.

```json [Human: a chat user's presence]
{
  "actorTokenId": "at_live_7f3a...",
  "presence": {
    "userId": "u_8x2k9",
    "username": "Alice",
    "status": "online"
  },
  "joinedAt": 1745827200000
}
```
```json [Agent: a worker's presence]
{
  "actorTokenId": "at_live_c41d...",
  "presence": {
    "name": "research-worker-3",
    "role": "agent",
    "capabilities": ["research", "finance"],
    "metadata": { "model": "llama3.2", "provider": "local" }
  },
  "joinedAt": 1745827200000,
  "status": "online"
}
```

Both are the same `ActorPresence` shape: an `actorTokenId`, the `presence` data the actor set, and (in fetched snapshots) `joinedAt`. The agents SDK fills `presence` with `AgentPresenceData` (`name`, `role`, optional `capabilities` and `metadata`); the outer `status` is `online`, `offline`, or `waking` and only appears for persistent agents that stay discoverable while disconnected.

The Handoff pattern uses presence as a gate. Before publishing a task, `dispatch()` looks at who is in the room and throws if no connected agent lists the required capability. It does not look at load or capacity, because presence does not carry them: the worker's own `onTask` filter and, for pools, the broker's load-balance group decide who acts on a task.

## QoS Levels for Different Patterns

Not every agent message needs the same delivery guarantee. Just like in human real-time, the right QoS level depends on the consequence of a lost message, and it is worth being precise about what QoS covers on NoLag. The level you pick (0, 1 or 2, default 1) is applied to the broker's internal MQTT hop. The WebSocket leg between your process and the broker has an optional `published` acknowledgement, surfaced as the emit callback, and no resend. There is no exactly-once guarantee end to end, so the envelopes carry the ids you need to tolerate a duplicate.

```typescript [QoS Choices]
import { NoLag } from "@nolag/js-sdk";
import { NoLagAgents, Handoff, createStateEnvelope } from "@nolag/agents";

const client = NoLag(TOKEN);
await client.connect();

// QoS 0 (fire and forget): typing indicators, cursor positions.
// Lost messages are harmless: the next update overwrites anyway.
const doc = client.setApp(COLLAB_APP_SLUG).setRoom("my-doc");
doc.emit("cursors", { x: 120, y: 48 }, { qos: 0 });

// QoS 1 (at least once): the default for everything the wrappers publish.
// Results carry a correlationId, so a pending dispatch resolves once and a
// duplicate result is dropped. Tasks carry a taskId for your worker to do
// the same on its side.
const agents = new NoLagAgents({ client, appName: AGENTS_APP_SLUG, agentId: "orchestrator" });
await agents.ready();
const room = agents.room("research-pipeline");
const handoff = new Handoff(room);
const result = await handoff.dispatch("research", { query }, { waitForResult: true });

// QoS 2 on the broker hop: for a raw publish where you also want the
// broker-side dedup. It adds nothing on the WebSocket leg.
const envelope = createStateEnvelope("run:42", "started", 1, agents.agentId);
room.context.emit("state", envelope, { qos: 2, retain: true });
```

This is one of the advantages of building on real messaging infrastructure rather than a custom agent framework. Delivery levels are a solved problem in messaging. We don't need to reinvent them; we map each pattern to the level it needs and put the idempotency keys (task ids, correlation ids, state versions) in the envelopes so your handlers can be idempotent.

## Rooms as Workflow Boundaries

In chat, rooms group related conversations. In agent coordination, rooms group related workflows. A room called `research-pipeline` contains all the topics for that pipeline: task dispatch, results, shared state, and events. A separate room called `content-review` contains its own isolated set of topics.

This isolation is important. Just as you don't want messages from #general leaking into #engineering, you don't want tasks from one workflow accidentally being picked up by workers in another. Rooms provide that boundary for free. They are created on the control plane, never implicitly: subscribing to a room that does not exist returns `unknown_topic`.

## Performance Characteristics

Agent workloads have different performance profiles than human real-time:

| Metric | Human Realtime | Agent Realtime |
| --- | --- | --- |
| Message size | Small (chat text, GPS coords) | Medium-large (task payloads, LLM outputs) |
| Message rate | Bursty (typing, idle cycles) | Steady (continuous task dispatch) |
| Latency sensitivity | High (users notice 200ms+) | Moderate (agents tolerate seconds) |
| Delivery guarantee | Mixed (QoS 0-1) | QoS 1 with idempotent envelopes |
| Connection count | Many (one per user/tab) | Few (one per agent process) |
| Fan-out ratio | High (1:1000 in large rooms) | Low (1:1 task dispatch, 1:N observe) |

Agent workloads typically involve fewer connections but larger payloads and stricter delivery requirements. NoLag's 900KB message size limit accommodates most LLM outputs. The binary MessagePack protocol keeps overhead low even for large payloads. And for worker pools, load-balanced groups with persistent sessions keep tasks from being lost while a worker restarts.

## Access Control Carries Over

In human systems, access control prevents unauthorized users from reading messages in rooms they haven't joined. In agent systems, the same ACL mechanism prevents agents from accessing workflows they shouldn't participate in.

Each agent connects with an actor token that grants access to specific apps and rooms. A research worker can only subscribe to topics in the research workflow. A monitoring dashboard can observe events but not dispatch tasks. The same per-topic ACL that protects chat messages protects agent coordination.

## The Insight

Agent coordination is a real-time messaging problem wearing a different hat. The primitives are the same: publish, subscribe, presence, retained state, and access control. The patterns change (task handoff instead of chat messages, blackboard instead of typing indicators, approval gates instead of read receipts), but the infrastructure doesn't.

That's why we built `@nolag/agents` as a high-level SDK on top of the same core infrastructure. No new servers, no new protocols, no new persistence layer. Just new patterns mapped onto proven primitives.

[Explore @nolag/agents →](/agents)
