# Realtime&#x20;

Inngest Realtime lets your functions push updates directly to the browser as
work happens. No polling. No WebSocket boilerplate. No separate pub/sub service.

You publish messages from inside your functions. Clients subscribe and receive
them instantly. Everything is typed end to end.

> **Warning:** Looking for the older @inngest/realtime package docs? See the archived
> v3 docs here.

***

## Four building blocks

Realtime has four concepts. Everything else builds on these.

**Channels** scope messages to a context. A channel might represent a user
session, a document being processed, or a batch job. You define channels with a
name or a function that generates a name from parameters like a user ID or
document ID.

**Topics** organize messages within a channel. A channel for document processing
might have topics for `status`, `review`, `error`, and `completed`. Each topic
carries its own typed payload schema.

**Publishing** sends a message to a topic within a channel. You publish with
`inngest.realtime.publish()` for transient updates or
`step.realtime.publish()` when the message must not duplicate on retry.

**Tokens** authorize a client subscription. You mint them on the server, scoped
to a single channel and an explicit list of topics.

```
Channel: document-processing:{documentId}
  ├── Topic: status    → { progress: 50, message: "Analyzing..." }
  ├── Topic: review    → { items: [...], requiresAction: true }
  ├── Topic: error     → { error: "...", recoverable: true }
  └── Topic: completed → { confidence: 0.95 }
```

On the client, you subscribe to a channel and its topics. Messages arrive as
they're published. You render them however you want.

***

## Define a channel

Channels are defined once and shared between your function code and your client
code. Each topic needs a `schema`, which can be any
[Standard Schema](https://github.com/standard-schema/standard-schema) validator
such as Zod, or `staticSchema<T>()` when you want types without runtime
validation.

```ts {{ filename: "inngest/channels.ts" }}
import { realtime, staticSchema } from "inngest";
import { z } from "zod";

export const pipelineChannel = realtime.channel({
  // The channel name is generated at runtime from the params you pass in.
  // `contentId` is your own identifier, not an Inngest run ID.
  name: ({ contentId }: { contentId: string }) => `pipeline:${contentId}`,
  topics: {
    // Zod schemas are validated at publish time.
    status: {
      schema: z.object({ message: z.string(), progress: z.number() }),
    },
    // `staticSchema` gives you types only, with zero runtime cost.
    tokens: {
      schema: staticSchema<{ token: string }>(),
    },
    result: {
      schema: staticSchema<{ output: string; model: string }>(),
    },
  },
});
```

TypeScript enforces these types when you publish and when you subscribe.

> **Info:** realtime and staticSchema are imported from "inngest". The same helpers
> are also available from the "inngest/realtime" subpath.

***

## Publish from a function

There are two ways to publish. `inngest.realtime.publish()` is non-durable and
fires immediately. `step.realtime.publish()` is a durable step that is memoized,
so it will not re-fire if the function retries.

```ts {{ filename: "inngest/functions/summarize.ts" }}
import { inngest } from "../client";
import { pipelineChannel } from "../channels";

export default inngest.createFunction(
  { id: "summarize-content", triggers: [{ event: "app/content.submitted" }] },
  async ({ event, step }) => {
    // Create a channel instance for this piece of content. The `contentId`
    // comes from the event, and the client uses the same value to subscribe,
    // which is what connects the two sides together.
    const ch = pipelineChannel({ contentId: event.data.contentId });

    // Durable publish (within a step). This caries the same benefit of step.run
    // to handle retries without publishing duplicate messages
    await step.realtime.publish('starting-status', ch.status, {
      message: "Starting...",
      progress: 0,
    });

    const result = await step.run("call-model", async () => {
      const stream = await generateTextStream(event.data.content);
      // Use inngest.realtime.publish for sending messages within steps
      let text = ''
      for await (const chunk of stream) {
        await inngest.realtime.publish(ch.tokens, { token: chunk.delta });
        text += chunk.delta;
      }
      return text;
    });
  }
);
```

Use `inngest.realtime.publish()` for high-frequency updates within steps like streaming
tokens or progress ticks. Use `step.realtime.publish()` for state
transitions that should not duplicate on retry.

> **Info:** inngest.realtime.publish() also works outside a function, in API routes,
> webhooks, or any server-side code. Inside a function run it automatically
> attaches the current run ID.

***

## Mint a subscription token

Clients cannot subscribe directly. Your server mints a token scoped to one
channel and an explicit list of topics, which is also where you check that the
current user is allowed to see that data.

```ts {{ filename: "app/actions.ts" }}
"use server";

import { getClientSubscriptionToken } from "inngest/react";
import { inngest } from "@/inngest/client";
import { pipelineChannel } from "@/inngest/channels";
import { getSession } from "@/lib/session";

export async function fetchToken(contentId: string) {
  // Authorize before minting: the token is a capability, so only hand one out
  // once you know this user may read this channel.
  const { userId } = await getSession();
  await assertUserOwnsContent(userId, contentId);

  return getClientSubscriptionToken(inngest, {
    channel: pipelineChannel({ contentId }),
    topics: ["status", "result"],
  });
}
```

See [Subscription tokens](/docs-markdown/features/realtime/subscription-tokens?ref=docs-realtime-overview)
for authorization patterns, token refresh, and framework examples.

***

## Subscribe from the browser

In React, use the `useRealtime` hook from `inngest/react`.

```tsx {{ filename: "app/page.tsx" }}
"use client";

import { useRealtime } from "inngest/react";
import { pipelineChannel } from "@/inngest/channels";
import { fetchToken } from "./actions";

export default function Progress({ contentId }: { contentId: string }) {
  const { messages, connectionStatus } = useRealtime({
    // Pass the channel instance (not just a name) so message data stays typed.
    channel: pipelineChannel({ contentId }),
    topics: ["status", "result"] as const,
    // A token factory runs on mount and on every reconnect, so the hook
    // always has a fresh token.
    token: () => fetchToken(contentId),
  });

  return (
    <div>
      <p>Connection: {connectionStatus}</p>
      <p>{messages.byTopic.status?.data.message}</p>
      <p>Progress: {messages.byTopic.status?.data.progress}%</p>
      {messages.byTopic.result && (
        <p>Result: {messages.byTopic.result.data.output}</p>
      )}
    </div>
  );
}
```

The hook manages token refresh, reconnection, and buffering. Messages are typed
per topic.

`useRealtime` accepts a number of options beyond the three shown here, including
`enabled`, `bufferInterval`, `historyLimit`, and reconnection controls. See the
[React hooks guide](/docs-markdown/features/realtime/react-hooks?ref=docs-realtime-overview)
for the typical Next.js flow, and the
[`useRealtime` reference](/docs-markdown/reference/typescript/v4/realtime/use-realtime?ref=docs-realtime-overview)
for every parameter and return value.

***

## When to use each publish method

- Prefer `step.realtime.publish()` for important state transitions and final results.
- Use `inngest.realtime.publish()` for high-frequency updates like tokens, logs, or progress ticks where replay on retry is acceptable, and for publishing from routes, webhooks, or other server-side code outside a function run.

## Learn more

**"Stream AI responses to your UI"**: [Walk through token-by-token LLM streaming with a typed channel and the
useRealtime hook.](/docs-markdown/features/realtime/stream-ai-responses?ref=docs-realtime-overview)

**"Use React hooks in Next.js"**: [Learn the v4 useRealtime flow with server-minted tokens and typed
messages.](/docs-markdown/features/realtime/react-hooks?ref=docs-realtime-overview)

**"Secure subscriptions with tokens"**: [Scope tokens to channels and topics, authorize users, and handle refresh.](/docs-markdown/features/realtime/subscription-tokens?ref=docs-realtime-overview)

**"Explore patterns and examples"**: [See single-run subscriptions, multi-channel fanout, and human-in-the-loop
examples.](/docs-markdown/examples/realtime?ref=docs-realtime-overview)

**"Open the v4 reference docs"**: [Dive into channels, publishing, subscribing, and the complete hook API.](/docs-markdown/reference/typescript/v4/realtime?ref=docs-realtime-overview)

**"View archived v3 docs"**: [Reference the deprecated @inngest/realtime concepts if you still maintain
a v3 app.](/docs-markdown/reference/typescript/v3/realtime?ref=docs-realtime-overview)