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"
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 ,
},
})
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.
Client Usage
Get history on connection:
"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
true
: Fetch all available history
{ length: number }
: Fetch the last N messages
{ since: number }
: Fetch messages after a Unix timestamp (in milliseconds)
useRealtime < RealtimeEvents >({
event: "chat.message" ,
history: {
length: 50 ,
},
onData ( data , channel ) {},
})
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:
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.
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.
"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: "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: "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
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
On connection, clients request history via query parameters
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 ,
},
})
For less critical features, fetch history only when needed: const [ showHistory , setShowHistory ] = useState ( false )
useRealtime < RealtimeEvents >({
event: "chat.message" ,
history: showHistory ? { length: 50 } : false ,
onData ( data , channel ) {},
})
Next Steps