Fix: Upstash Not Working — Redis Commands Failing, Rate Limiter Not Blocking, or QStash Messages Lost
Quick Answer
How to fix Upstash issues — Redis REST client setup, rate limiting with @upstash/ratelimit, QStash message queues, Kafka topics, Vector search, and edge runtime integration.
The Problem
Upstash Redis commands return unexpected results:
import { Redis } from '@upstash/redis';
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
const value = await redis.get('mykey');
// null — even after setting it moments agoOr the rate limiter doesn’t block requests:
import { Ratelimit } from '@upstash/ratelimit';
const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '10 s'),
});
const result = await ratelimit.limit('user_123');
// result.success is always true — never blocksOr QStash messages aren’t delivered:
import { Client } from '@upstash/qstash';
const qstash = new Client({ token: process.env.QSTASH_TOKEN! });
await qstash.publishJSON({
url: 'https://myapp.com/api/webhook',
body: { event: 'user.created', userId: '123' },
});
// Message published but endpoint never receives itWhy This Happens
Upstash provides serverless Redis, QStash (message queue), and Vector (embeddings) via HTTP REST APIs. They’re designed for edge and serverless runtimes:
- Upstash Redis uses HTTP, not TCP — unlike traditional Redis clients that use TCP connections,
@upstash/redissends each command as an HTTP request. This means no connection management, but also different latency characteristics and potential issues with command pipelining. - Rate limiter state is in Redis —
@upstash/ratelimitstores counters in your Upstash Redis instance. If the Redis URL or token is wrong, the limiter can’t read/write counters and may default to allowing requests instead of blocking them. - QStash is asynchronous — when you publish a message, QStash accepts it immediately and delivers it later (with retries). If your endpoint returns a non-2xx status, QStash retries up to 3 times. If the endpoint URL is wrong or unreachable, messages are retried and eventually dead-lettered.
- Free tier has limits — Upstash Redis free tier allows 10,000 commands/day. QStash free tier has 500 messages/day. Exceeding these limits causes silent failures or rate limiting from Upstash itself.
Fix 1: Redis REST Client Setup
npm install @upstash/redis// Basic setup
import { Redis } from '@upstash/redis';
// Option 1: Explicit configuration
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!, // https://xyz.upstash.io
token: process.env.UPSTASH_REDIS_REST_TOKEN!, // AXxx...
});
// Option 2: Auto-detect from environment variables
// Reads UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN automatically
const redis = Redis.fromEnv();
// String operations
await redis.set('user:123', JSON.stringify({ name: 'Alice', role: 'admin' }));
await redis.set('session:abc', 'user_123', { ex: 3600 }); // Expires in 1 hour
const user = await redis.get<{ name: string; role: string }>('user:123');
// user = { name: 'Alice', role: 'admin' } — automatically parsed from JSON
// Key operations
await redis.del('user:123');
const exists = await redis.exists('user:123'); // 0 or 1
const ttl = await redis.ttl('session:abc'); // Seconds remaining
// Hash operations
await redis.hset('user:456', { name: 'Bob', email: '[email protected]', role: 'user' });
const name = await redis.hget<string>('user:456', 'name'); // 'Bob'
const allFields = await redis.hgetall<Record<string, string>>('user:456');
// List operations (queue-like)
await redis.lpush('tasks', 'task1', 'task2', 'task3');
const task = await redis.rpop<string>('tasks'); // 'task1' (FIFO)
// Sorted set (leaderboard)
await redis.zadd('leaderboard', { score: 100, member: 'alice' });
await redis.zadd('leaderboard', { score: 85, member: 'bob' });
const top = await redis.zrange<string[]>('leaderboard', 0, 9, { rev: true });
// Pipeline — batch multiple commands in one HTTP request
const pipeline = redis.pipeline();
pipeline.set('key1', 'value1');
pipeline.set('key2', 'value2');
pipeline.get('key1');
pipeline.get('key2');
const results = await pipeline.exec();
// results = ['OK', 'OK', 'value1', 'value2']Fix 2: Rate Limiting
npm install @upstash/ratelimit @upstash/redisimport { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const redis = Redis.fromEnv();
// Sliding window — 10 requests per 10 seconds
const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '10 s'),
analytics: true, // Track rate limit metrics in Redis
prefix: '@upstash/ratelimit',
});
// Alternative algorithms
// Fixed window — resets every interval
const fixed = new Ratelimit({
redis,
limiter: Ratelimit.fixedWindow(100, '1 m'), // 100 per minute
});
// Token bucket — smooth rate limiting
const tokenBucket = new Ratelimit({
redis,
limiter: Ratelimit.tokenBucket(5, '10 s', 20),
// 5 tokens added every 10s, max 20 tokens
});
// Usage in an API route
export async function POST(request: Request) {
// Use IP address or user ID as identifier
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous';
const { success, limit, remaining, reset } = await ratelimit.limit(ip);
if (!success) {
return new Response('Too Many Requests', {
status: 429,
headers: {
'X-RateLimit-Limit': String(limit),
'X-RateLimit-Remaining': String(remaining),
'X-RateLimit-Reset': String(reset),
'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)),
},
});
}
// Process request...
return new Response('OK');
}
// Next.js middleware rate limiting
// middleware.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
import { NextResponse, type NextRequest } from 'next/server';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(20, '60 s'),
});
export async function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/')) {
const ip = request.ip ?? request.headers.get('x-forwarded-for') ?? '127.0.0.1';
const { success } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json({ error: 'Rate limit exceeded' }, { status: 429 });
}
}
return NextResponse.next();
}Fix 3: QStash — Message Queues and Scheduled Jobs
npm install @upstash/qstash// Publish a message
import { Client } from '@upstash/qstash';
const qstash = new Client({ token: process.env.QSTASH_TOKEN! });
// Send a message to your API endpoint
await qstash.publishJSON({
url: 'https://myapp.com/api/process-order',
body: { orderId: '123', action: 'fulfill' },
retries: 3,
// Delay delivery by 30 seconds
delay: 30,
// Or schedule with cron
// cron: '0 9 * * *', // Every day at 9 AM
});
// Send to multiple destinations
await qstash.batchJSON([
{
destination: 'https://myapp.com/api/send-email',
body: { to: '[email protected]', template: 'welcome' },
},
{
destination: 'https://myapp.com/api/update-analytics',
body: { event: 'signup', userId: '123' },
},
]);// Receive and verify messages
// app/api/process-order/route.ts
import { verifySignatureAppRouter } from '@upstash/qstash/nextjs';
async function handler(request: Request) {
const body = await request.json();
// Process the message
await fulfillOrder(body.orderId);
// Return 200 to acknowledge — QStash won't retry
return new Response('OK');
}
// Wrap handler with signature verification — prevents unauthorized calls
export const POST = verifySignatureAppRouter(handler);
// For non-Next.js:
import { Receiver } from '@upstash/qstash';
const receiver = new Receiver({
currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
});
async function handleWebhook(request: Request) {
const body = await request.text();
const signature = request.headers.get('upstash-signature')!;
const isValid = await receiver.verify({ signature, body });
if (!isValid) return new Response('Unauthorized', { status: 401 });
// Process message...
}Fix 4: Caching Patterns
import { Redis } from '@upstash/redis';
const redis = Redis.fromEnv();
// Cache-aside pattern
async function getCachedUser(userId: string) {
// Try cache first
const cached = await redis.get<User>(`user:${userId}`);
if (cached) return cached;
// Cache miss — fetch from database
const user = await db.query.users.findFirst({ where: eq(users.id, userId) });
if (!user) return null;
// Store in cache with TTL
await redis.set(`user:${userId}`, JSON.stringify(user), { ex: 300 }); // 5 min
return user;
}
// Invalidate cache on update
async function updateUser(userId: string, data: Partial<User>) {
await db.update(users).set(data).where(eq(users.id, userId));
await redis.del(`user:${userId}`); // Invalidate cache
}
// Stale-while-revalidate pattern
async function getWithSWR<T>(
key: string,
fetcher: () => Promise<T>,
ttl: number = 300,
staleTtl: number = 3600,
) {
const cached = await redis.get<{ data: T; timestamp: number }>(key);
if (cached) {
const age = (Date.now() - cached.timestamp) / 1000;
if (age < ttl) {
// Fresh — return immediately
return cached.data;
}
if (age < staleTtl) {
// Stale — return stale data and revalidate in background
fetcher().then(async (data) => {
await redis.set(key, JSON.stringify({ data, timestamp: Date.now() }), { ex: staleTtl });
});
return cached.data;
}
}
// Expired or missing — fetch fresh data
const data = await fetcher();
await redis.set(key, JSON.stringify({ data, timestamp: Date.now() }), { ex: staleTtl });
return data;
}Fix 5: Session Storage
import { Redis } from '@upstash/redis';
import { nanoid } from 'nanoid';
const redis = Redis.fromEnv();
const SESSION_TTL = 60 * 60 * 24 * 7; // 7 days
// Create session
async function createSession(userId: string) {
const sessionId = nanoid(32);
const session = {
userId,
createdAt: Date.now(),
lastActiveAt: Date.now(),
};
await redis.set(`session:${sessionId}`, JSON.stringify(session), {
ex: SESSION_TTL,
});
return sessionId;
}
// Get session
async function getSession(sessionId: string) {
const session = await redis.get<{
userId: string;
createdAt: number;
lastActiveAt: number;
}>(`session:${sessionId}`);
if (!session) return null;
// Refresh TTL on access
await redis.expire(`session:${sessionId}`, SESSION_TTL);
await redis.set(`session:${sessionId}`, JSON.stringify({
...session,
lastActiveAt: Date.now(),
}), { ex: SESSION_TTL });
return session;
}
// Delete session
async function deleteSession(sessionId: string) {
await redis.del(`session:${sessionId}`);
}
// Delete all sessions for a user (on password change)
async function deleteAllUserSessions(userId: string) {
// This requires tracking session IDs per user
const sessionIds = await redis.smembers<string[]>(`user_sessions:${userId}`);
if (sessionIds.length > 0) {
await redis.del(...sessionIds.map(id => `session:${id}`));
await redis.del(`user_sessions:${userId}`);
}
}Fix 6: Edge Runtime Usage
Upstash works everywhere — no TCP connections needed:
// Cloudflare Workers
export default {
async fetch(request: Request, env: Env) {
const redis = new Redis({
url: env.UPSTASH_REDIS_REST_URL,
token: env.UPSTASH_REDIS_REST_TOKEN,
});
const pageViews = await redis.incr('page-views');
return new Response(`Views: ${pageViews}`);
},
};
// Vercel Edge Middleware
import { Redis } from '@upstash/redis';
import { NextResponse, type NextRequest } from 'next/server';
const redis = Redis.fromEnv();
export async function middleware(request: NextRequest) {
// Feature flag from Redis
const maintenance = await redis.get<boolean>('feature:maintenance');
if (maintenance) {
return NextResponse.rewrite(new URL('/maintenance', request.url));
}
return NextResponse.next();
}
// Deno Deploy
import { Redis } from 'https://esm.sh/@upstash/redis';
const redis = new Redis({
url: Deno.env.get('UPSTASH_REDIS_REST_URL')!,
token: Deno.env.get('UPSTASH_REDIS_REST_TOKEN')!,
});Still Not Working?
redis.get() returns null for a key you just set — check that the set completed without error. If using ex (expiration), a very short TTL could cause the key to expire before the get. Also verify both operations use the same Redis instance — UPSTASH_REDIS_REST_URL might point to different databases in different environments.
Rate limiter always allows requests — the limiter needs a working Redis connection to track counts. If the Redis URL or token is invalid, ratelimit.limit() may not throw but instead fail open (allow the request). Verify the connection with a simple await redis.ping() first. Also check the identifier — if every request uses a different identifier, each gets its own fresh window.
QStash message not delivered — check the QStash dashboard for delivery attempts and errors. Common issues: the endpoint URL must be publicly reachable (not localhost), the endpoint must return a 2xx status within 30 seconds, and the signature verification must pass. For local testing, use ngrok to expose your local server.
“Unauthorized” error from Redis — the REST token is scoped to a specific database. If you created a new database, you need the new token from the Upstash console. Tokens from environment variables might have extra whitespace — trim them.
For related backend issues, see Fix: Wrangler Not Working and Fix: Hono Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Neon Database Not Working — Connection Timeout, Branching Errors, or Serverless Driver Issues
How to fix Neon Postgres issues — connection string setup, serverless HTTP driver vs TCP, database branching, connection pooling, Drizzle and Prisma integration, and cold start optimization.
Fix: Turso Not Working — Connection Refused, Queries Returning Empty, or Embedded Replicas Not Syncing
How to fix Turso database issues — libsql client setup, connection URLs and auth tokens, embedded replicas for local-first apps, schema migrations, Drizzle ORM integration, and edge deployment.
Fix: Convex Not Working — Query Not Updating, Mutation Throwing Validation Error, or Action Timing Out
How to fix Convex backend issues — query/mutation/action patterns, schema validation, real-time reactivity, file storage, auth integration, and common TypeScript type errors.
Fix: Kysely Not Working — Type Errors on Queries, Migration Failing, or Generated Types Not Matching Schema
How to fix Kysely query builder issues — database interface definition, dialect setup, type-safe joins and subqueries, migration runner, kysely-codegen for generated types, and common TypeScript errors.