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
import { Realtime } from "@upstash/realtime"
import { redis } from "./redis"
import z from "zod/v4"
const schema = {
chat: {
message: z . object ({
text: z . string (),
sender: z . string (),
}),
},
}
export const realtime = new Realtime ({
schema ,
redis ,
history: {
maxLength: 100 ,
expireAfterSecs: 86400 ,
},
})
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.
How long to keep messages per channel before deleting them (in seconds). Resets every
time a message is emitted to this channel.
Server-Side History
Retrieve and process history on the server:
import { realtime } from "@/lib/realtime"
export const GET = async () => {
const messages = await realtime . channel ( "room-123" ). history ()
return new Response ( JSON . stringify ( messages ))
}
History Options
Maximum number of messages to retrieve (capped at 1000)
Fetch messages after this Unix timestamp (in milliseconds)
Fetch messages before this Unix timestamp (in milliseconds)
const messages = await realtime . channel ( "room-123" ). history ({
limit: 50 ,
start: Date . now () - 86400000 ,
})
History Response
Each history message contains:
type HistoryMessage = {
id : string
event : string
channel : string
data : unknown
}
Subscribe with History
You can automatically replay past messages when subscribing to a channel:
await realtime . channel ( "room-123" ). subscribe ({
events: [ "chat.message" ],
history: true ,
onData ({ event , data , channel }) {
console . log ( "Message from room-123:" , data )
},
})
Pass history options for more control:
await realtime . channel ( "room-123" ). subscribe ({
events: [ "chat.message" ],
history: {
limit: 50 ,
start: Date . now () - 3600000 ,
},
onData ({ data }) {
console . log ( "Message:" , 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.
"use client"
import { useRealtime } from "@/lib/realtime-client"
import { useState , useEffect } from "react"
import z from "zod/v4"
import type { RealtimeEvents } from "@/lib/realtime"
type Message = z . infer < RealtimeEvents [ "chat" ][ "message" ]>
export default function ChatRoom ({ roomId } : { roomId : string }) {
const [ messages , setMessages ] = useState < Message []>([])
useEffect (() => {
fetch ( `/api/history?channel= ${ roomId } ` )
. then (( res ) => res . json ())
. then (( history ) => setMessages ( history . map (( m : any ) => m . data )))
}, [ roomId ])
useRealtime ({
channels: [ roomId ],
events: [ "chat.message" ],
onData ({ data }) {
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: "use client"
import { useRealtime } from "@/lib/realtime-client"
import { useUser } from "@/hooks/auth"
import { useState , useEffect } from "react"
import z from "zod/v4"
import type { RealtimeEvents } from "@/lib/realtime"
type Notification = z . infer < RealtimeEvents [ "notification" ][ "alert" ]>
export default function Notifications () {
const user = useUser ()
const [ notifications , setNotifications ] = useState < Notification []>([])
useEffect (() => {
fetch ( `/api/history?channel=user- ${ user . id } ` )
. then (( res ) => res . json ())
. then (( history ) => {
const unread = history . filter (( m : any ) => m . data . status === "unread" )
setNotifications ( unread . map (( m : any ) => m . data ))
})
}, [ user . id ])
useRealtime ({
channels: [ `user- ${ user . id } ` ],
events: [ "notification.alert" ],
onData ({ data }) {
if ( data . status === "unread" ) {
setNotifications (( prev ) => [ ... prev , data ])
}
},
})
return (
< div >
{ notifications . map (( notif , i ) => (
< div key = { i } > { notif } </ div >
)) }
</ div >
)
}
Replay recent activity when users visit: "use client"
import { useRealtime } from "@/lib/realtime-client"
import { useTeam } from "@/hooks/team"
import { useState , useEffect } from "react"
import z from "zod/v4"
import type { RealtimeEvents } from "@/lib/realtime"
type Activity = z . infer < RealtimeEvents [ "activity" ][ "update" ]>
export default function ActivityFeed () {
const team = useTeam ()
const [ activities , setActivities ] = useState < Activity []>([])
useEffect (() => {
fetch ( `/api/history?channel=team- ${ team . id } &limit=100` )
. then (( res ) => res . json ())
. then (( history ) => setActivities ( history . map (( m : any ) => m . data )))
}, [ team . id ])
useRealtime ({
channels: [ `team- ${ team . id } ` ],
events: [ "activity.update" ],
onData ({ data }) {
setActivities (( prev ) => [ data , ... prev ])
},
})
return (
< div >
{ activities . map (( activity , i ) => (
< div key = { i } > { activity . message } </ div >
)) }
</ div >
)
}
How It Works
When you emit an event, it’s stored in a Redis Stream with a unique stream ID
The stream is trimmed to maxLength if configured
The stream expires after expireAfterSecs if configured
History can be fetched via channel.history() on the server
History is replayed in chronological order (oldest to newest)
New events continue streaming right after history replay, no messages lost
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. export const realtime = new Realtime ({
schema ,
redis ,
history: {
maxLength: 1000 ,
},
})
Expire old messages to reduce storage: export const realtime = new Realtime ({
schema ,
redis ,
history: {
expireAfterSecs: 3600 ,
},
})
Next Steps