Skip to main content
Message history allows you to retrieve past events and replay them to clients on connection. This is useful for making sure clients always have the latest state.

Overview

All Upstash Realtime messages are automatically stored in Redis Streams. This way, messages are always delivered correctly, even after reconnects or network interruptions. Clients can fetch past events and optionally subscribe to new events.

Configuration

lib/realtime.ts
import { Realtime } from "@upstash/realtime"
import { redis } from "./redis"
import z from "zod"

const schema = {
  chat: {
    message: z.object({
      text: z.string(),
      sender: z.string(),
    }),
  },
}

export const realtime = new Realtime({
  schema,
  redis,
  // 👇 (optional) per-channel history settings.
  history: {
    maxLength: 100,
    expireAfterSecs: 86400,
  },
})
maxLength
number
default:"Infinite"
Maximum number of messages to retain per channel. Example: maxLength: 100 will keep the last 100 messages in the stream and automatically remove older messages as new ones are added.
expireAfterSecs
number
default:"Infinite"
How long to keep messages per channel before deleting them (in seconds). Resets every time a message is emitted to this channel.

Client Usage

Get history on connection:
page.tsx
"use client"

import { useRealtime } from "@upstash/realtime/client"
import type { RealtimeEvents } from "@/lib/realtime"

export default function Page() {
  useRealtime<RealtimeEvents>({
    event: "chat.message",
    history: true,
    onData(data, channel) {
      console.log("Historical or new message:", data)
    },
  })

  return <>...</>
}

History Options

history
boolean | object
  • true: Fetch all available history
  • { length: number }: Fetch the last N messages
  • { since: number }: Fetch messages after a Unix timestamp (in milliseconds)
page.tsx
useRealtime<RealtimeEvents>({
  event: "chat.message",
  history: {
    length: 50,
  },
  onData(data, channel) {},
})
page.tsx
const ONE_DAY_IN_MS = 60 * 60 * 24 * 1000
const oneDayAgo = Date.now() - ONE_DAY_IN_MS

useRealtime<RealtimeEvents>({
  event: "chat.message",
  history: {
    since: oneDayAgo,
  },
  onData(data, channel) {},
})
You can also use both length and since together.

Server-Side History

Retrieve and process history on the server:
route.ts
import { realtime } from "@/lib/realtime"

export const GET = async () => {
  const channel = realtime.channel("room-123")
  const messages = await channel.history({ length: 50 })

  return new Response(JSON.stringify(messages))
}

Subscribe to new messages with history

You can automatically replay past messages when subscribing to a channel. See the Server-Side Usage documentation for more details.
route.ts
await realtime
  .channel("room-123")
  .history({ length: 50 })
  .on("chat.message", (data) => {
    console.log("Message from room-123:", data)
  })

Use Cases

Load recent messages when a user joins a room:
We recommend keeping long chat histories in a database (e.g. Redis) and only fetching the latest messages from Upstash Realtime.
page.tsx
"use client"

import { useRealtime } from "@upstash/realtime/client"
import type { RealtimeEvents } from "@/lib/realtime"
import { useState } from "react"
import z from "zod/v4"

type Message = z.infer<RealtimeEvents["chat"]["message"]>

export default function ChatRoom({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState<Message[]>([])

  useRealtime<RealtimeEvents>({
    channels: [roomId],
    event: "chat.message",
    history: true,
    onData(data, channel) {
      setMessages((prev) => [...prev, data])
    },
  })

  return (
    <div>
      {messages.map((msg, i) => (
        <div key={i}>
          <strong>{msg.sender}:</strong> {msg.text}
        </div>
      ))}
    </div>
  )
}
Show unread notifications with history:
notifications.tsx
"use client"

import { useRealtime } from "@upstash/realtime/client"
import type { RealtimeEvents } from "@/lib/realtime"
import { useUser } from "@/hooks/auth"
import { useState } from "react"

type Notification = z.infer<RealtimeEvents["notification"]["alert"]>

export default function Notifications() {
  const user = useUser()
  const [notifications, setNotifications] = useState<Notification[]>([])

  useRealtime<RealtimeEvents>({
    channels: [`user-${user.id}`],
    event: "notification.alert",
    history: true,
    onData(notification, channel) {
     if(notification.status === "unread") {
       setNotifications((prev) => [...prev, notification])
     }
    },
  })

  return (
    <div>
      {notifications.map((notif, i) => (
        <div key={i}>{notif}</div>
      ))}
    </div>
  )
}
Replay recent activity when users visit:
activity-feed.tsx
"use client"

import { useRealtime } from "@upstash/realtime/client"
import type { RealtimeEvents } from "@/lib/realtime"
import { useTeam } from "@/hooks/team"
import { useState } from "react"
import z from "zod/v4"

type Activity = z.infer<RealtimeEvents["activity"]["update"]>

export default function ActivityFeed() {
  const team = useTeam()
  const [activities, setActivities] = useState<Activity[]>([])

  useRealtime<RealtimeEvents>({
    channels: [`team-${team.id}`],
    event: "activity.update",
    history: { length: 100 },
    onData(data, channel) {
      setActivities((prev) => [data, ...prev])
    },
  })

  return (
    <div>
      {activities.map((activity, i) => (
        <div key={i}>{activity.message}</div>
      ))}
    </div>
  )
}

How It Works

  1. When you emit an event, it’s stored in a Redis Stream with a unique stream ID
  2. The stream is trimmed to maxLength if configured
  3. The stream expires after expireAfterSecs if configured
  4. On connection, clients request history via query parameters
  5. History is replayed in chronological order (oldest to newest)
  6. New events continue streaming right after history replay, no messages lost

Performance Considerations

Upstash Realtime can handle extremely large histories without problems. The bottleneck is the client who needs to handle all replayed events. At that point you should probably consider using a database like Redis or Postgres to fetch the history once, then stream new events to the client with Upstash Realtime.
For high-volume channels, limit history to prevent large initial payloads.
lib/realtime.ts
export const realtime = new Realtime({
  schema,
  redis,
  history: {
    maxLength: 1000,
  },
})
Expire old messages to reduce storage:
lib/realtime.ts
export const realtime = new Realtime({
  schema,
  redis,
  history: {
    expireAfterSecs: 3600,
  },
})
For less critical features, fetch history only when needed:
page.tsx
const [showHistory, setShowHistory] = useState(false)

useRealtime<RealtimeEvents>({
  event: "chat.message",
  history: showHistory ? { length: 50 } : false,
  onData(data, channel) {},
})

Next Steps

I