Claude
Skills
Sign in
Back

upstash-qstash

Included with Lifetime
$97 forever

Upstash QStash expert for serverless message queues, scheduled jobs, and reliable HTTP-based task delivery without managing infrastructure.

Productivity

What this skill does


# Upstash QStash

Upstash QStash expert for serverless message queues, scheduled jobs, and
reliable HTTP-based task delivery without managing infrastructure.

## Principles

- HTTP is the interface - if it speaks HTTPS, it speaks QStash
- Endpoints must be public - QStash calls your URLs from the cloud
- Verify signatures always - never trust unverified webhooks
- Schedules are fire-and-forget - QStash handles the cron
- Retries are built-in - but configure them for your use case
- Delays are free - schedule seconds to days in the future
- Callbacks complete the loop - know when delivery succeeds or fails
- Deduplication prevents double-processing - use message IDs

## Capabilities

- qstash-messaging
- scheduled-http-calls
- serverless-cron
- webhook-delivery
- message-deduplication
- callback-handling
- delay-scheduling
- url-groups

## Scope

- complex-workflows -> inngest
- redis-queues -> bullmq-specialist
- event-sourcing -> event-architect
- workflow-orchestration -> temporal-craftsman

## Tooling

### Core

- qstash-sdk
- upstash-console

### Frameworks

- nextjs
- cloudflare-workers
- vercel-functions
- aws-lambda
- netlify-functions

### Patterns

- scheduled-jobs
- delayed-messages
- webhook-fanout
- callback-verification

### Related

- upstash-redis
- upstash-kafka

## Patterns

### Basic Message Publishing

Sending messages to be delivered to endpoints

**When to use**: Need reliable async HTTP calls

import { Client } from '@upstash/qstash';

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

// Simple message to endpoint
await qstash.publishJSON({
  url: 'https://myapp.com/api/process',
  body: {
    userId: '123',
    action: 'welcome-email',
  },
});

// With delay (process in 1 hour)
await qstash.publishJSON({
  url: 'https://myapp.com/api/reminder',
  body: { userId: '123' },
  delay: 60 * 60,  // seconds
});

// With specific delivery time
await qstash.publishJSON({
  url: 'https://myapp.com/api/scheduled',
  body: { report: 'daily' },
  notBefore: Math.floor(Date.now() / 1000) + 86400,  // tomorrow
});

### Scheduled Cron Jobs

Setting up recurring scheduled tasks

**When to use**: Need periodic background jobs without infrastructure

import { Client } from '@upstash/qstash';

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

// Create a scheduled job
const schedule = await qstash.schedules.create({
  destination: 'https://myapp.com/api/cron/daily-report',
  cron: '0 9 * * *',  // Every day at 9 AM UTC
  body: JSON.stringify({ type: 'daily' }),
  headers: {
    'Content-Type': 'application/json',
  },
});

console.log('Schedule created:', schedule.scheduleId);

// List all schedules
const schedules = await qstash.schedules.list();

// Delete a schedule
await qstash.schedules.delete(schedule.scheduleId);

### Signature Verification

Verifying QStash message signatures in your endpoint

**When to use**: Any endpoint receiving QStash messages (always!)

// app/api/webhook/route.ts (Next.js App Router)
import { Receiver } from '@upstash/qstash';
import { NextRequest, NextResponse } from 'next/server';

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

export async function POST(req: NextRequest) {
  const signature = req.headers.get('upstash-signature');
  const body = await req.text();

  // ALWAYS verify signature
  const isValid = await receiver.verify({
    signature: signature!,
    body,
    url: req.url,
  });

  if (!isValid) {
    return NextResponse.json(
      { error: 'Invalid signature' },
      { status: 401 }
    );
  }

  // Safe to process
  const data = JSON.parse(body);
  await processMessage(data);

  return NextResponse.json({ success: true });
}

### Callback for Delivery Status

Getting notified when messages are delivered or fail

**When to use**: Need to track delivery status for critical messages

import { Client } from '@upstash/qstash';

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

// Publish with callback
await qstash.publishJSON({
  url: 'https://myapp.com/api/critical-task',
  body: { taskId: '456' },
  callback: 'https://myapp.com/api/qstash-callback',
  failureCallback: 'https://myapp.com/api/qstash-failed',
});

// Callback endpoint receives delivery status
// app/api/qstash-callback/route.ts
export async function POST(req: NextRequest) {
  // Verify signature first!
  const data = await req.json();

  // data contains:
  // - sourceMessageId: original message ID
  // - url: destination URL
  // - status: HTTP status code
  // - body: response body

  if (data.status >= 200 && data.status < 300) {
    await markTaskComplete(data.sourceMessageId);
  }

  return NextResponse.json({ received: true });
}

### URL Groups (Fan-out)

Sending messages to multiple endpoints at once

**When to use**: Need to notify multiple services about an event

import { Client } from '@upstash/qstash';

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

// Create a URL group
await qstash.urlGroups.addEndpoints({
  name: 'order-processors',
  endpoints: [
    { url: 'https://inventory.myapp.com/api/process' },
    { url: 'https://shipping.myapp.com/api/process' },
    { url: 'https://analytics.myapp.com/api/track' },
  ],
});

// Publish to the group - all endpoints receive the message
await qstash.publishJSON({
  urlGroup: 'order-processors',
  body: {
    orderId: '789',
    event: 'order.placed',
  },
});

### Message Deduplication

Preventing duplicate message processing

**When to use**: Idempotency is critical (payments, notifications)

import { Client } from '@upstash/qstash';

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

// Deduplicate by custom ID (within deduplication window)
await qstash.publishJSON({
  url: 'https://myapp.com/api/charge',
  body: { orderId: '123', amount: 5000 },
  deduplicationId: 'charge-order-123',  // Won't send again within window
});

// Content-based deduplication
await qstash.publishJSON({
  url: 'https://myapp.com/api/notify',
  body: { userId: '456', message: 'Hello' },
  contentBasedDeduplication: true,  // Hash of body used as ID
});

## Sharp Edges

### Not verifying QStash webhook signatures

Severity: CRITICAL

Situation: Endpoint accepts any POST request. Attacker discovers your callback URL.
Fake messages flood your system. Malicious payloads processed as trusted.

Symptoms:
- No Receiver import in webhook handler
- Missing upstash-signature header check
- Processing request before verification

Why this breaks:
QStash endpoints are public URLs. Without signature verification, anyone
can send requests. This is a direct path to unauthorized message processing
and potential data manipulation.

Recommended fix:

# Always verify signatures with both keys:
```typescript
import { Receiver } from '@upstash/qstash';

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

export async function POST(req: NextRequest) {
  const signature = req.headers.get('upstash-signature');
  const body = await req.text();  // Raw body required

  const isValid = await receiver.verify({
    signature: signature!,
    body,
    url: req.url,
  });

  if (!isValid) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  // Safe to process
}
```

# Why two keys?
- QStash rotates signing keys
- nextSigningKey becomes current during rotation
- Both must be checked for seamless key rotation

### Callback endpoint taking too long to respond

Severity: HIGH

Situation: Webhook handler does heavy processing. Takes 30+ seconds. QStash times out.
Marks message as failed. Retries. Double processing begins.

Symptoms:
- Webhook timeouts in QStash dashboard
- Messages marked failed then retried
- Duplicate processing of same message

Why this breaks:
QStash has a 30-second timeou

Related in Productivity