QStash is Upstash's HTTP-based message queue — it delivers messages to your endpoints with automatic retries, scheduling, and delivery guarantees, all without a persistent server. For AI pipelines running on serverless infrastructure (Vercel, Cloudflare Workers, AWS Lambda), QStash solves the core problem: you can't run a long-lived worker process, but you still need reliable job execution with retries.

How QStash Works

You publish a message to QStash with a destination URL. QStash delivers it via HTTP POST to that URL, waits for a 2xx response, and retries on failure with exponential backoff. Your endpoint processes the job and returns 200 when done. No persistent workers, no WebSocket connections, no infrastructure to manage.

Feature Description
Delivery guarantee At-least-once delivery with configurable retries
Retry policy Exponential backoff, up to 5 retries by default
Scheduling Publish with a delay or on a cron schedule
Deduplication Content-based or custom deduplication keys
Callbacks POST to a callback URL when the job completes

Installation and Setup

npm install @upstash/qstash

# .env.local QSTASH_TOKEN=your-qstash-token # from console.upstash.com QSTASH_CURRENT_SIGNING_KEY=... # for verifying incoming messages QSTASH_NEXT_SIGNING_KEY=... # for key rotation

Publishing a Job

// app/api/trigger-analysis/route.ts import { Client } from "@upstash/qstash"; import { NextResponse } from "next/server"; const qstash = new Client({ token: process.env.QSTASH_TOKEN! }); export async function POST(req: Request) { const { documentId, userId } = await req.json(); // Publish a message — QStash will POST to /api/analyze with the payload const result = await qstash.publishJSON({ url: "https://yourapp.com/api/analyze", // must be a public HTTPS URL body: { documentId, userId }, retries: 5, delay: 0, // seconds (or "1m", "2h", etc.) headers: { "Content-Type": "application/json", }, }); return NextResponse.json({ messageId: result.messageId }); }

Processing Jobs

Your processing endpoint receives the job payload and must return 200 when done. QStash handles retries automatically if you return 4xx/5xx or the request times out.

// app/api/analyze/route.ts import { verifySignatureAppRouter } from "@upstash/qstash/nextjs"; import { NextResponse } from "next/server"; // verifySignatureAppRouter middleware validates that requests come from QStash async function handler(req: Request) { const { documentId, userId } = await req.json(); try { // Your processing logic const document = await fetchDocument(documentId); const analysis = await analyzeWithOpenAI(document.content); await saveAnalysis(documentId, analysis); // Notify user await sendNotification(userId, "Your document analysis is complete."); return NextResponse.json({ success: true }); } catch (error) { // Return 500 to trigger QStash retry console.error("Analysis failed:", error); return NextResponse.json({ error: "Processing failed" }, { status: 500 }); } } export const POST = verifySignatureAppRouter(handler);

Scheduled Jobs with QStash

// Create a recurring schedule (run every day at 9am UTC) const schedule = await qstash.schedules.create({ destination: "https://yourapp.com/api/daily-summary", cron: "0 9 * * *", body: JSON.stringify({ type: "daily-digest" }), headers: { "Content-Type": "application/json" }, }); console.log("Schedule ID:", schedule.scheduleId); // List all schedules const schedules = await qstash.schedules.list(); // Delete a schedule await qstash.schedules.delete(schedule.scheduleId);

Delayed Publishing

// Send a follow-up email 24 hours after signup await qstash.publishJSON({ url: "https://yourapp.com/api/emails/followup", body: { userId, email }, delay: 60 * 60 * 24, // 24 hours in seconds }); // Or use a natural language duration: await qstash.publishJSON({ url: "https://yourapp.com/api/emails/followup", body: { userId, email }, notBefore: Math.floor(Date.now() / 1000) + 86400, // Unix timestamp });

Chaining Jobs with Callbacks

// Job A — triggers Job B when complete via callback await qstash.publishJSON({ url: "https://yourapp.com/api/process-document", body: { documentId }, callback: "https://yourapp.com/api/on-document-processed", failureCallback: "https://yourapp.com/api/on-document-failed", });

For local development, use the Upstash QStash local emulator or use ngrok to expose your localhost to QStash. Alternatively, use a provider like Railway for your dev environment so you have a real public URL.

QStash vs Inngest vs Trigger.dev

Feature QStash Inngest Trigger.dev
Architecture HTTP queue — stateless HTTP durable steps WebSocket durable steps
Best for Simple job queuing on serverless Complex multi-step workflows Long-running durable jobs
Step orchestration None (single endpoint) Full step.run() API Full io.runTask() API
Self-hostable No Early access Yes — open source
Free tier 500 messages/day 50k runs/month 5k runs/month

QStash is the right choice when you need simple, reliable job delivery on serverless infrastructure and don't need multi-step orchestration. For complex AI pipelines with branching logic, use Inngest or Trigger.dev instead.