Skip to content

Commit d9bc4e3

Browse files
Hossein Niazmandiclaude
andcommitted
Use Upstash Redis for session span storage on Vercel
Replace in-memory Map with Upstash Redis for serverless compatibility. Session spans are now stored with 24h TTL and work across multiple Vercel function instances. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d292fd3 commit d9bc4e3

3 files changed

Lines changed: 33 additions & 30 deletions

File tree

app/api/widget/[slug]/chat/route.ts

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Redis } from "@upstash/redis";
12
import { type CoreMessage } from "ai";
23
import { eq } from "drizzle-orm";
34
import type { NextRequest } from "next/server";
@@ -8,32 +9,20 @@ import { exportSpan, startSessionSpan } from "@/lib/braintrust";
89
import { streamChatTurn } from "@/lib/chat-engine";
910

1011
/**
11-
* In-memory cache for session spans.
12-
* Maps sessionId -> exported span string
12+
* Upstash Redis client for session span storage.
13+
* Uses REST API which works well with Vercel serverless functions.
1314
*
14-
* NOTE: For production with multiple server instances, use Redis or similar:
15-
* const redis = new Redis(process.env.REDIS_URL);
16-
* await redis.set(`session-span:${sessionId}`, exportedSpan, 'EX', 86400);
17-
* const parentSpan = await redis.get(`session-span:${sessionId}`);
15+
* Required env vars (auto-configured by Vercel when you add Upstash):
16+
* - KV_REST_API_URL (or UPSTASH_REDIS_REST_URL)
17+
* - KV_REST_API_TOKEN (or UPSTASH_REDIS_REST_TOKEN)
1818
*/
19-
const sessionSpanCache = new Map<string, string>();
19+
const redis = new Redis({
20+
url: process.env.KV_REST_API_URL!,
21+
token: process.env.KV_REST_API_TOKEN!,
22+
});
2023

21-
// Clean up old sessions periodically (simple TTL implementation)
22-
const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
23-
const sessionTimestamps = new Map<string, number>();
24-
25-
function cleanupOldSessions() {
26-
const now = Date.now();
27-
for (const [sessionId, timestamp] of sessionTimestamps.entries()) {
28-
if (now - timestamp > SESSION_TTL_MS) {
29-
sessionSpanCache.delete(sessionId);
30-
sessionTimestamps.delete(sessionId);
31-
}
32-
}
33-
}
34-
35-
// Run cleanup every hour
36-
setInterval(cleanupOldSessions, 60 * 60 * 1000);
24+
// Session span TTL in seconds (24 hours)
25+
const SESSION_TTL_SECONDS = 24 * 60 * 60;
3726

3827
export async function POST(
3928
request: NextRequest,
@@ -61,8 +50,9 @@ export async function POST(
6150
return new Response("Widget is disabled", { status: 403 });
6251
}
6352

64-
// Get or create session-level parent span
65-
let parentSpan = sessionSpanCache.get(sessionId);
53+
// Get or create session-level parent span from Redis
54+
const cacheKey = `session-span:${sessionId}`;
55+
let parentSpan = await redis.get<string>(cacheKey);
6656

6757
if (!parentSpan) {
6858
// First message in this session - create the root "conversation" span
@@ -77,13 +67,10 @@ export async function POST(
7767
const exported = await exportSpan(rootSpan);
7868
if (exported) {
7969
parentSpan = exported;
80-
sessionSpanCache.set(sessionId, exported);
81-
sessionTimestamps.set(sessionId, Date.now());
70+
// Store in Redis with TTL
71+
await redis.set(cacheKey, exported, { ex: SESSION_TTL_SECONDS });
8272
}
8373
}
84-
} else {
85-
// Update timestamp to keep session alive
86-
sessionTimestamps.set(sessionId, Date.now());
8774
}
8875

8976
const result = await streamChatTurn({

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@radix-ui/react-tabs": "^1.1.1",
4040
"@radix-ui/react-toast": "^1.2.2",
4141
"@radix-ui/react-tooltip": "^1.1.3",
42+
"@upstash/redis": "^1.36.1",
4243
"ai": "^4.0.0",
4344
"autoevals": "^0.0.131",
4445
"braintrust": "0.3.8",

pnpm-lock.yaml

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)