Claude Code for QStash: Serverless Message Queue and Scheduler — Claude Skills 360 Blog
Blog / Backend / Claude Code for QStash: Serverless Message Queue and Scheduler
Backend

Claude Code for QStash: Serverless Message Queue and Scheduler

Published: May 21, 2027
Read time: 6 min read
By: Claude Skills 360

QStash is a serverless message queue from Upstash — client.publishJSON({ url, body, retries, delay }) enqueues a job that delivers to an HTTP endpoint. Receiver.verify({ signature, body }) validates the Upstash-Signature header in the handler. delay: "5m" or notBefore: timestamp schedule future delivery. retries: 3 automatically retries on non-2xx responses with exponential backoff. client.schedules.create({ cron: "0 9 * * *", destination: url }) creates a cron-based recurring schedule. deduplicationId prevents duplicate messages in queues. callbackUrl notifies a different endpoint on job success or failure. client.queues.upsert({ queueName, parallelism }) sets concurrency limits. QStash is stateless and HTTP-native — jobs are just POST requests to your Route Handlers. No persistent connections or workers needed. Claude Code generates QStash email job queues, image processing pipelines, scheduled reports, and retry-safe notification delivery.

CLAUDE.md for QStash

## QStash Stack
- Version: @upstash/qstash >= 2.7
- Init: const client = new Client({ token: process.env.QSTASH_TOKEN! })
- Publish: await client.publishJSON({ url: `${APP_URL}/api/jobs/send-email`, body: { userId, template } })
- Delay: await client.publishJSON({ url, body, delay: "10m" })
- Schedule: await client.schedules.create({ destination: url, cron: "0 9 * * 1-5", body: {}, retries: 3 })
- Verify: const receiver = new Receiver({ currentSigningKey: QSTASH_CURRENT_KEY, nextSigningKey: QSTASH_NEXT_KEY })
- Handler: await receiver.verify({ signature: req.headers["upstash-signature"], body: rawBody })

QStash Client Setup

// lib/queue/qstash.ts — QStash client and publisher
import { Client, Receiver, type PublishRequest } from "@upstash/qstash"

export const qstash = new Client({
  token: process.env.QSTASH_TOKEN!,
})

export const qstashReceiver = new Receiver({
  currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
  nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
})

const APP_URL = process.env.VERCEL_URL
  ? `https://${process.env.VERCEL_URL}`
  : process.env.APP_URL ?? "http://localhost:3000"

// ── Job publishers ─────────────────────────────────────────────────────────

export type EmailJobPayload = {
  to: string
  template: "welcome" | "password_reset" | "order_confirmed" | "subscription_ending"
  data: Record<string, string | number>
}

export async function enqueueEmail(payload: EmailJobPayload, delaySeconds?: number) {
  return qstash.publishJSON({
    url: `${APP_URL}/api/jobs/send-email`,
    body: payload,
    retries: 3,
    delay: delaySeconds,
    headers: {
      "x-job-type": "email",
    },
  })
}

export type ImageJobPayload = {
  fileKey: string
  userId: string
  operations: ("resize" | "webp" | "thumbnail")[]
  targetWidth?: number
}

export async function enqueueImageProcessing(payload: ImageJobPayload) {
  return qstash.publishJSON({
    url: `${APP_URL}/api/jobs/process-image`,
    body: payload,
    retries: 2,
    // Image jobs use a queue to limit concurrency
    queue: {
      name: "image-processing",
      parallelism: 3,
    },
  })
}

export type WebhookDeliveryPayload = {
  webhookId: string
  eventType: string
  payload: unknown
  targetUrl: string
  secret: string
}

export async function enqueueWebhookDelivery(
  payload: WebhookDeliveryPayload,
  callbackUrl?: string,
) {
  return qstash.publishJSON({
    url: `${APP_URL}/api/jobs/deliver-webhook`,
    body: payload,
    retries: 5,
    retryDelay: "geometric",  // 1s, 2s, 4s, 8s, 16s
    callbackUrl,
    failureCallbackUrl: callbackUrl ? `${APP_URL}/api/jobs/webhook-failed` : undefined,
  })
}

// Batch: send multiple emails at once
export async function enqueueBatchEmails(payloads: EmailJobPayload[]) {
  return qstash.batchJSON(
    payloads.map(payload => ({
      url: `${APP_URL}/api/jobs/send-email`,
      body: payload,
      retries: 3,
    })),
  )
}

Job Handlers (Next.js Route Handlers)

// app/api/jobs/send-email/route.ts — QStash email job handler
import { NextRequest, NextResponse } from "next/server"
import { qstashReceiver } from "@/lib/queue/qstash"
import type { EmailJobPayload } from "@/lib/queue/qstash"
import { resend } from "@/lib/email/resend"

export async function POST(request: NextRequest) {
  // Verify QStash signature
  const signature = request.headers.get("upstash-signature")
  if (!signature) {
    return NextResponse.json({ error: "Missing signature" }, { status: 401 })
  }

  const rawBody = await request.text()

  try {
    await qstashReceiver.verify({ signature, body: rawBody })
  } catch {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 })
  }

  const payload: EmailJobPayload = JSON.parse(rawBody)

  try {
    await sendEmailByTemplate(payload)
    return NextResponse.json({ ok: true })
  } catch (err) {
    // Non-2xx triggers QStash retry
    console.error("[email job]", err)
    return NextResponse.json({ error: "Email failed" }, { status: 500 })
  }
}

async function sendEmailByTemplate(payload: EmailJobPayload) {
  const { to, template, data } = payload

  const templates = {
    welcome: {
      subject: `Welcome, ${data.name}!`,
      html: `<h1>Welcome to ${data.appName}!</h1><p>Your account is ready.</p>`,
    },
    order_confirmed: {
      subject: `Order #${data.orderId} confirmed`,
      html: `<h1>Order confirmed!</h1><p>Total: ${data.total}</p>`,
    },
    // ... other templates
  }

  const tmpl = templates[template]
  if (!tmpl) throw new Error(`Unknown template: ${template}`)

  await resend.emails.send({
    from: "[email protected]",
    to,
    subject: tmpl.subject,
    html: tmpl.html,
  })
}

Cron Schedules

// lib/queue/schedules.ts — QStash cron schedule management
import { qstash } from "./qstash"

const APP_URL = process.env.APP_URL ?? "http://localhost:3000"

export async function setupCronJobs() {
  const existing = await qstash.schedules.list()
  const existingDests = new Set(existing.map(s => s.destination))

  // Daily 9am digest email
  if (!existingDests.has(`${APP_URL}/api/jobs/daily-digest`)) {
    await qstash.schedules.create({
      destination: `${APP_URL}/api/jobs/daily-digest`,
      cron: "0 9 * * 1-5",  // Weekdays at 9am UTC
      body: JSON.stringify({ type: "digest" }),
      retries: 3,
    })
  }

  // Weekly Sunday report
  if (!existingDests.has(`${APP_URL}/api/jobs/weekly-report`)) {
    await qstash.schedules.create({
      destination: `${APP_URL}/api/jobs/weekly-report`,
      cron: "0 8 * * 0",  // Sundays at 8am UTC
      retries: 2,
    })
  }

  // Hourly subscription expiry check
  if (!existingDests.has(`${APP_URL}/api/jobs/check-expiries`)) {
    await qstash.schedules.create({
      destination: `${APP_URL}/api/jobs/check-expiries`,
      cron: "7 * * * *",  // Every hour at :07
      retries: 1,
    })
  }
}

// Schedule a one-time job at a specific time
export async function scheduleAt(
  url: string,
  body: unknown,
  at: Date,
) {
  const delay = Math.max(0, Math.floor((at.getTime() - Date.now()) / 1000))
  return qstash.publishJSON({
    url: `${APP_URL}${url}`,
    body,
    delay,
    retries: 2,
  })
}

Middleware Helper

// lib/queue/middleware.ts — reusable QStash verification wrapper
import { NextRequest, NextResponse } from "next/server"
import { qstashReceiver } from "./qstash"

type JobHandler<T> = (payload: T, request: NextRequest) => Promise<void | Response>

// Wrap a Next.js Route Handler with QStash signature verification
export function withQStash<T>(handler: JobHandler<T>) {
  return async function POST(request: NextRequest): Promise<NextResponse> {
    const signature = request.headers.get("upstash-signature")

    // Allow direct calls in development without signature
    if (process.env.NODE_ENV === "development" && !signature) {
      const body = await request.json() as T
      await handler(body, request)
      return NextResponse.json({ ok: true })
    }

    if (!signature) {
      return NextResponse.json({ error: "Missing signature" }, { status: 401 })
    }

    const rawBody = await request.text()

    try {
      await qstashReceiver.verify({ signature, body: rawBody })
    } catch {
      return NextResponse.json({ error: "Invalid signature" }, { status: 401 })
    }

    try {
      const payload = JSON.parse(rawBody) as T
      const response = await handler(payload, request)
      if (response) return response as NextResponse
      return NextResponse.json({ ok: true })
    } catch (err) {
      console.error("[QStash job error]", err)
      return NextResponse.json({ error: "Job failed" }, { status: 500 })
    }
  }
}

// Usage:
// export const POST = withQStash<EmailJobPayload>(async (payload) => {
//   await sendEmailByTemplate(payload)
// })

For the Trigger.dev alternative when a code-native job runner with in-process task definitions, TypeScript-native flows, delays, wait-for-event, concurrency control, and a cloud dashboard for monitoring is preferred — Trigger.dev runs jobs as code in your repo rather than HTTP endpoint deliveries, making complex multi-step workflows easier to author and debug, see the Trigger.dev guide. For the Inngest alternative when event-driven background functions with automatic retries, step primitives (step.run, step.waitForEvent), and a local dev server are preferred — Inngest is similar to Trigger.dev but uses a push-based event model while QStash uses URL-based queue delivery, see the Inngest guide. The Claude Skills 360 bundle includes QStash skill sets covering job queues, scheduling, and signature verification. Start with the free tier to try serverless queue generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free