Upstash QStash
Integrate Upstash QStash with your application for serverless-first background task processing.
Upstash QStash delivers signed HTTP requests with retries, delays and schedules. It fits a serverless Next.js application because the worker is an ordinary route handler, not a persistent process.
This integration is not installed by default
Add QStash only when its HTTP delivery model fits your job. The starter kit does not include the SDK, credentials, task tables or task routes.
Install the SDK
npm install @upstash/qstashRegister server-only variables
Copy the token and both signing keys from the Upstash console:
QSTASH_TOKEN=replace-me
QSTASH_CURRENT_SIGNING_KEY=replace-me
QSTASH_NEXT_SIGNING_KEY=replace-meAdd the variables to the server schema in lib/env.ts:
server: {
// Existing variables...
QSTASH_TOKEN: z.string().min(1),
QSTASH_CURRENT_SIGNING_KEY: z.string().min(1),
QSTASH_NEXT_SIGNING_KEY: z.string().min(1),
QSTASH_URL: z.string().url().optional()
}Expose them to the server-side validator in the same file:
runtimeEnv: {
// Existing variables...
QSTASH_TOKEN: process.env.QSTASH_TOKEN,
QSTASH_CURRENT_SIGNING_KEY: process.env.QSTASH_CURRENT_SIGNING_KEY,
QSTASH_NEXT_SIGNING_KEY: process.env.QSTASH_NEXT_SIGNING_KEY,
QSTASH_URL: process.env.QSTASH_URL
}Do not prefix these values with NEXT_PUBLIC_. Set a separate credential set
in every deployed environment.
Create the publishing client
QSTASH_URL is optional for the managed service and useful when connecting to a
local QStash server:
import 'server-only';
import { Client } from '@upstash/qstash';
import { env } from '@/lib/env';
export const qstash = new Client({
token: env.QSTASH_TOKEN,
...(env.QSTASH_URL ? { baseUrl: env.QSTASH_URL } : {})
});Add a signed worker route
QStash calls a public HTTP endpoint. Wrap the handler with the official App Router verifier so requests without a valid QStash signature are rejected:
import { verifySignatureAppRouter } from '@upstash/qstash/nextjs';
import * as z from 'zod';
import { processStoredJob } from '@/lib/tasks/process-stored-job';
const payloadSchema = z.object({
jobId: z.string().uuid()
});
async function handler(request: Request): Promise<Response> {
const { jobId } = payloadSchema.parse(await request.json());
await processStoredJob(jobId);
return Response.json({ success: true });
}
export const POST = verifySignatureAppRouter(handler);Create processStoredJob for your product. Load the job and its organization
from the database instead of trusting tenant data carried in the message. Make
the state transition idempotent so delivering the same jobId twice does not
repeat billing, credits, email or another external side effect.
Return a non-success status or throw for transient failures that QStash should retry. Record permanent failures so they can be investigated without retrying forever.
Publish after authorization
Authorize the current user and persist a pending job before publishing its ID.
Use the kit's getBaseUrl() rather than introducing another site URL variable:
import 'server-only';
import { qstash } from '@/lib/qstash';
import { getBaseUrl } from '@/lib/utils';
export async function publishJob(jobId: string): Promise<string> {
const result = await qstash.publishJSON({
url: `${getBaseUrl()}/api/tasks/process`,
body: { jobId },
retries: 3
});
return result.messageId;
}Do not expose this function directly to the browser. Call it from an authenticated server action, route or tRPC procedure after checking access to the organization that owns the stored job.
Test locally
The managed QStash service cannot deliver to an inaccessible localhost URL. Use the official local server or a public development tunnel.
Start local QStash in one terminal:
npx @upstash/qstash-cli devCopy the printed local URL, token and signing keys into the root .env, then
restart npm run dev. Publish a test job and verify all four outcomes:
- The producer returns a QStash message ID.
- The signed worker changes the stored job from pending to successful.
- A failing attempt is retried and remains observable.
- Publishing the same
jobIdagain does not repeat its side effect.
Production checklist
- Set managed QStash credentials in the production environment and remove any
local
QSTASH_URLoverride. - Confirm the destination uses the final HTTPS production origin.
- Keep both signing keys configured so key rotation can complete safely.
- Monitor QStash delivery logs and the application's stored job state.
- Add alerts for exhausted retries and jobs that remain pending too long.
- Avoid logging message bodies when they contain customer or organization data.
For delays, schedules, queues and callbacks, use the QStash TypeScript SDK documentation.