[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-153b0a3e-071d-4ffc-9f5c-3016f77399f4":3,"$fdV657kWRPdEVq5A49wCnX15fDLTi9rGqO4ViL9cRIQ0":43},{"id":4,"title":5,"description":6,"categoryId":7,"moduleId":8,"tags":9,"prompt":10,"icon":11,"source":12,"sourceUrl":13,"authorId":14,"authorName":15,"isPublic":16,"stars":17,"runs":18,"createdAt":19,"updatedAt":19,"module":20,"category":27,"packages":34},"153b0a3e-071d-4ffc-9f5c-3016f77399f4","upstash-qstash","Upstash QStash 服务器端无消息队列专家，定时","cat_life_career","mod_other","sickn33,other","---\nname: upstash-qstash\ndescription: Upstash QStash expert for serverless message queues, scheduled\n  jobs, and reliable HTTP-based task delivery without managing infrastructure.\nrisk: unknown\nsource: vibeship-spawner-skills (Apache 2.0)\ndate_added: 2026-02-27\n---\n\n# Upstash QStash\n\nUpstash QStash expert for serverless message queues, scheduled jobs, and\nreliable HTTP-based task delivery without managing infrastructure.\n\n## Principles\n\n- HTTP is the interface - if it speaks HTTPS, it speaks QStash\n- Endpoints must be public - QStash calls your URLs from the cloud\n- Verify signatures always - never trust unverified webhooks\n- Schedules are fire-and-forget - QStash handles the cron\n- Retries are built-in - but configure them for your use case\n- Delays are free - schedule seconds to days in the future\n- Callbacks complete the loop - know when delivery succeeds or fails\n- Deduplication prevents double-processing - use message IDs\n\n## Capabilities\n\n- qstash-messaging\n- scheduled-http-calls\n- serverless-cron\n- webhook-delivery\n- message-deduplication\n- callback-handling\n- delay-scheduling\n- url-groups\n\n## Scope\n\n- complex-workflows -> inngest\n- redis-queues -> bullmq-specialist\n- event-sourcing -> event-architect\n- workflow-orchestration -> temporal-craftsman\n\n## Tooling\n\n### Core\n\n- qstash-sdk\n- upstash-console\n\n### Frameworks\n\n- nextjs\n- cloudflare-workers\n- vercel-functions\n- aws-lambda\n- netlify-functions\n\n### Patterns\n\n- scheduled-jobs\n- delayed-messages\n- webhook-fanout\n- callback-verification\n\n### Related\n\n- upstash-redis\n- upstash-kafka\n\n## Patterns\n\n### Basic Message Publishing\n\nSending messages to be delivered to endpoints\n\n**When to use**: Need reliable async HTTP calls\n\nimport { Client } from '@upstash\u002Fqstash';\n\nconst qstash = new Client({\n  token: process.env.QSTASH_TOKEN!,\n});\n\n\u002F\u002F Simple message to endpoint\nawait qstash.publishJSON({\n  url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fprocess',\n  body: {\n    userId: '123',\n    action: 'welcome-email',\n  },\n});\n\n\u002F\u002F With delay (process in 1 hour)\nawait qstash.publishJSON({\n  url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Freminder',\n  body: { userId: '123' },\n  delay: 60 * 60,  \u002F\u002F seconds\n});\n\n\u002F\u002F With specific delivery time\nawait qstash.publishJSON({\n  url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fscheduled',\n  body: { report: 'daily' },\n  notBefore: Math.floor(Date.now() \u002F 1000) + 86400,  \u002F\u002F tomorrow\n});\n\n### Scheduled Cron Jobs\n\nSetting up recurring scheduled tasks\n\n**When to use**: Need periodic background jobs without infrastructure\n\nimport { Client } from '@upstash\u002Fqstash';\n\nconst qstash = new Client({\n  token: process.env.QSTASH_TOKEN!,\n});\n\n\u002F\u002F Create a scheduled job\nconst schedule = await qstash.schedules.create({\n  destination: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fcron\u002Fdaily-report',\n  cron: '0 9 * * *',  \u002F\u002F Every day at 9 AM UTC\n  body: JSON.stringify({ type: 'daily' }),\n  headers: {\n    'Content-Type': 'application\u002Fjson',\n  },\n});\n\nconsole.log('Schedule created:', schedule.scheduleId);\n\n\u002F\u002F List all schedules\nconst schedules = await qstash.schedules.list();\n\n\u002F\u002F Delete a schedule\nawait qstash.schedules.delete(schedule.scheduleId);\n\n### Signature Verification\n\nVerifying QStash message signatures in your endpoint\n\n**When to use**: Any endpoint receiving QStash messages (always!)\n\n\u002F\u002F app\u002Fapi\u002Fwebhook\u002Froute.ts (Next.js App Router)\nimport { Receiver } from '@upstash\u002Fqstash';\nimport { NextRequest, NextResponse } from 'next\u002Fserver';\n\nconst receiver = new Receiver({\n  currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,\n  nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,\n});\n\nexport async function POST(req: NextRequest) {\n  const signature = req.headers.get('upstash-signature');\n  const body = await req.text();\n\n  \u002F\u002F ALWAYS verify signature\n  const isValid = await receiver.verify({\n    signature: signature!,\n    body,\n    url: req.url,\n  });\n\n  if (!isValid) {\n    return NextResponse.json(\n      { error: 'Invalid signature' },\n      { status: 401 }\n    );\n  }\n\n  \u002F\u002F Safe to process\n  const data = JSON.parse(body);\n  await processMessage(data);\n\n  return NextResponse.json({ success: true });\n}\n\n### Callback for Delivery Status\n\nGetting notified when messages are delivered or fail\n\n**When to use**: Need to track delivery status for critical messages\n\nimport { Client } from '@upstash\u002Fqstash';\n\nconst qstash = new Client({\n  token: process.env.QSTASH_TOKEN!,\n});\n\n\u002F\u002F Publish with callback\nawait qstash.publishJSON({\n  url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fcritical-task',\n  body: { taskId: '456' },\n  callback: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fqstash-callback',\n  failureCallback: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fqstash-failed',\n});\n\n\u002F\u002F Callback endpoint receives delivery status\n\u002F\u002F app\u002Fapi\u002Fqstash-callback\u002Froute.ts\nexport async function POST(req: NextRequest) {\n  \u002F\u002F Verify signature first!\n  const data = await req.json();\n\n  \u002F\u002F data contains:\n  \u002F\u002F - sourceMessageId: original message ID\n  \u002F\u002F - url: destination URL\n  \u002F\u002F - status: HTTP status code\n  \u002F\u002F - body: response body\n\n  if (data.status >= 200 && data.status \u003C 300) {\n    await markTaskComplete(data.sourceMessageId);\n  }\n\n  return NextResponse.json({ received: true });\n}\n\n### URL Groups (Fan-out)\n\nSending messages to multiple endpoints at once\n\n**When to use**: Need to notify multiple services about an event\n\nimport { Client } from '@upstash\u002Fqstash';\n\nconst qstash = new Client({\n  token: process.env.QSTASH_TOKEN!,\n});\n\n\u002F\u002F Create a URL group\nawait qstash.urlGroups.addEndpoints({\n  name: 'order-processors',\n  endpoints: [\n    { url: 'https:\u002F\u002Finventory.myapp.com\u002Fapi\u002Fprocess' },\n    { url: 'https:\u002F\u002Fshipping.myapp.com\u002Fapi\u002Fprocess' },\n    { url: 'https:\u002F\u002Fanalytics.myapp.com\u002Fapi\u002Ftrack' },\n  ],\n});\n\n\u002F\u002F Publish to the group - all endpoints receive the message\nawait qstash.publishJSON({\n  urlGroup: 'order-processors',\n  body: {\n    orderId: '789',\n    event: 'order.placed',\n  },\n});\n\n### Message Deduplication\n\nPreventing duplicate message processing\n\n**When to use**: Idempotency is critical (payments, notifications)\n\nimport { Client } from '@upstash\u002Fqstash';\n\nconst qstash = new Client({\n  token: process.env.QSTASH_TOKEN!,\n});\n\n\u002F\u002F Deduplicate by custom ID (within deduplication window)\nawait qstash.publishJSON({\n  url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fcharge',\n  body: { orderId: '123', amount: 5000 },\n  deduplicationId: 'charge-order-123',  \u002F\u002F Won't send again within window\n});\n\n\u002F\u002F Content-based deduplication\nawait qstash.publishJSON({\n  url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fnotify',\n  body: { userId: '456', message: 'Hello' },\n  contentBasedDeduplication: true,  \u002F\u002F Hash of body used as ID\n});\n\n## Sharp Edges\n\n### Not verifying QStash webhook signatures\n\nSeverity: CRITICAL\n\nSituation: Endpoint accepts any POST request. Attacker discovers your callback URL.\nFake messages flood your system. Malicious payloads processed as trusted.\n\nSymptoms:\n- No Receiver import in webhook handler\n- Missing upstash-signature header check\n- Processing request before verification\n\nWhy this breaks:\nQStash endpoints are public URLs. Without signature verification, anyone\ncan send requests. This is a direct path to unauthorized message processing\nand potential data manipulation.\n\nRecommended fix:\n\n# Always verify signatures with both keys:\n```typescript\nimport { Receiver } from '@upstash\u002Fqstash';\n\nconst receiver = new Receiver({\n  currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,\n  nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,\n});\n\nexport async function POST(req: NextRequest) {\n  const signature = req.headers.get('upstash-signature');\n  const body = await req.text();  \u002F\u002F Raw body required\n\n  const isValid = await receiver.verify({\n    signature: signature!,\n    body,\n    url: req.url,\n  });\n\n  if (!isValid) {\n    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });\n  }\n\n  \u002F\u002F Safe to process\n}\n```\n\n# Why two keys?\n- QStash rotates signing keys\n- nextSigningKey becomes current during rotation\n- Both must be checked for seamless key rotation\n\n### Callback endpoint taking too long to respond\n\nSeverity: HIGH\n\nSituation: Webhook handler does heavy processing. Takes 30+ seconds. QStash times out.\nMarks message as failed. Retries. Double processing begins.\n\nSymptoms:\n- Webhook timeouts in QStash dashboard\n- Messages marked failed then retried\n- Duplicate processing of same message\n\nWhy this breaks:\nQStash has a 30-second timeout for callbacks. If your endpoint doesn't respond\nin time, QStash considers it failed and retries. Long-running handlers create\nduplicate message processing and wasted retries.\n\nRecommended fix:\n\n# Design for fast acknowledgment:\n```typescript\nexport async function POST(req: NextRequest) {\n  \u002F\u002F 1. Verify signature first (fast)\n  \u002F\u002F 2. Parse and validate message (fast)\n  \u002F\u002F 3. Queue for async processing (fast)\n\n  const message = await parseMessage(req);\n\n  \u002F\u002F Don't do this:\n  \u002F\u002F await processHeavyWork(message);  \u002F\u002F Could timeout!\n\n  \u002F\u002F Do this instead:\n  await db.jobs.create({ data: message, status: 'pending' });\n  \u002F\u002F Or use another QStash message for the heavy work\n\n  return NextResponse.json({ queued: true });  \u002F\u002F Respond fast\n}\n```\n\n# Alternative: Use QStash for the heavy work\n```typescript\n\u002F\u002F Webhook receives trigger\nawait qstash.publishJSON({\n  url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fheavy-process',\n  body: { jobId: message.id },\n});\nreturn NextResponse.json({ delegated: true });\n```\n\n# For Vercel: Consider using Edge runtime for faster cold starts\n\n### Hitting QStash rate limits unexpectedly\n\nSeverity: HIGH\n\nSituation: Burst of events triggers mass message publishing. QStash rate limit hit.\nMessages rejected. Users don't get notifications. Critical tasks delayed.\n\nSymptoms:\n- 429 errors from QStash\n- Messages not being delivered\n- Sudden drop in processing during peak times\n\nWhy this breaks:\nQStash has plan-based rate limits. Free tier: 500 messages\u002Fday. Pro: higher\nbut still limited. Bursts can exhaust limits quickly. Without monitoring,\nyou won't know until users complain.\n\nRecommended fix:\n\n# Check your plan limits:\n- Free: 500 messages\u002Fday\n- Pay as you go: Check dashboard\n- Pro: Higher limits, check dashboard\n\n# Implement rate limit handling:\n```typescript\ntry {\n  await qstash.publishJSON({ url, body });\n} catch (error) {\n  if (error.message?.includes('rate limit')) {\n    \u002F\u002F Queue locally and retry later\n    await localQueue.add('qstash-retry', { url, body });\n  }\n  throw error;\n}\n```\n\n# Batch messages when possible:\n```typescript\n\u002F\u002F Instead of 100 individual publishes\nawait qstash.batchJSON({\n  messages: items.map(item => ({\n    url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fprocess',\n    body: { itemId: item.id },\n  })),\n});\n```\n\n# Monitor in dashboard:\nUpstash Console shows usage and limits\n\n### Not using deduplication for critical operations\n\nSeverity: HIGH\n\nSituation: Network hiccup during publish. SDK retries. Same message sent twice.\nCustomer charged twice. Email sent twice. Data corrupted.\n\nSymptoms:\n- Duplicate charges or emails\n- Double processing of same event\n- User complaints about duplicates\n\nWhy this breaks:\nNetwork failures and retries happen. Without deduplication, the same logical\nmessage can be sent multiple times. QStash provides deduplication, but you\nmust use it for critical operations.\n\nRecommended fix:\n\n# Use deduplication for critical messages:\n```typescript\n\u002F\u002F Custom ID (best for business operations)\nawait qstash.publishJSON({\n  url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fcharge',\n  body: { orderId: '123', amount: 5000 },\n  deduplicationId: `charge-${orderId}`,  \u002F\u002F Same ID = same message\n});\n\n\u002F\u002F Content-based (good for notifications)\nawait qstash.publishJSON({\n  url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fnotify',\n  body: { userId: '456', type: 'welcome' },\n  contentBasedDeduplication: true,  \u002F\u002F Hash of body\n});\n```\n\n# Deduplication window:\n- Default: 60 seconds\n- Messages with same ID in window are deduplicated\n- Plan for this in your retry logic\n\n# Also make endpoints idempotent:\nCheck if operation already completed before processing\n\n### Expecting QStash to reach private\u002Flocalhost endpoints\n\nSeverity: CRITICAL\n\nSituation: Development works with local server. Deploy to production with internal URL.\nQStash can't reach it. All messages fail silently. No processing happens.\n\nSymptoms:\n- Messages show \"failed\" in QStash dashboard\n- Works locally but fails in \"production\"\n- Using http:\u002F\u002F instead of https:\u002F\u002F\n\nWhy this breaks:\nQStash runs in Upstash's cloud. It can only reach public, internet-accessible\nURLs. localhost, internal IPs, and private networks are unreachable. This is\na fundamental architecture requirement, not a configuration issue.\n\nRecommended fix:\n\n# Production requirements:\n- URL must be publicly accessible\n- HTTPS required (HTTP will fail)\n- No localhost, 127.0.0.1, or private IPs\n\n# Local development options:\n\n# Option 1: ngrok\u002Flocaltunnel\n```bash\nngrok http 3000\n# Use the ngrok URL for QStash testing\n```\n\n# Option 2: QStash local development mode\n```typescript\n\u002F\u002F In development, skip QStash and call directly\nif (process.env.NODE_ENV === 'development') {\n  await fetch('http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fprocess', {\n    method: 'POST',\n    body: JSON.stringify(data),\n  });\n} else {\n  await qstash.publishJSON({ url, body: data });\n}\n```\n\n# Option 3: Use Vercel preview URLs\nPreview deploys give you public URLs for testing\n\n### Using default retry behavior for all message types\n\nSeverity: MEDIUM\n\nSituation: Critical payment webhook uses defaults. 3 retries over minutes. Payment\nprocessor is temporarily down for 15 minutes. Message marked as failed.\nPayment reconciliation manual work required.\n\nSymptoms:\n- Critical messages marked failed\n- Manual intervention needed for retries\n- Temporary outages causing permanent failures\n\nWhy this breaks:\nDefault retry behavior (3 attempts, short backoff) works for many cases but\nnot all. Some endpoints need more attempts, longer backoff, or different\nstrategies. One size doesn't fit all.\n\nRecommended fix:\n\n# Configure retries per message:\n```typescript\n\u002F\u002F Critical operations: more retries, longer backoff\nawait qstash.publishJSON({\n  url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fpayment-webhook',\n  body: { paymentId: '123' },\n  retries: 5,\n  \u002F\u002F Backoff: 10s, 30s, 1m, 5m, 30m\n});\n\n\u002F\u002F Non-critical notifications: fewer retries\nawait qstash.publishJSON({\n  url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fanalytics',\n  body: { event: 'pageview' },\n  retries: 1,  \u002F\u002F Fail fast, not critical\n});\n```\n\n# Consider your endpoint's recovery time:\n- Database down: May need 5+ minutes\n- Third-party API: May need hours\n- Internal service: Usually quick\n\n# Use failure callbacks for dead letter handling:\n```typescript\nawait qstash.publishJSON({\n  url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fcritical',\n  body: data,\n  failureCallback: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fdead-letter',\n});\n```\n\n### Sending large payloads instead of references\n\nSeverity: MEDIUM\n\nSituation: Message contains entire document (5MB). QStash rejects - body too large.\nEven if accepted, slow to transmit. Expensive. Wastes bandwidth.\n\nSymptoms:\n- Message publish failures\n- Slow message delivery\n- High bandwidth costs\n\nWhy this breaks:\nQStash has message size limits (around 500KB body). Large payloads slow\ndelivery, increase costs, and can fail entirely. Messages should be\nlightweight triggers, not data carriers.\n\nRecommended fix:\n\n# Send references, not data:\n```typescript\n\u002F\u002F BAD: Large payload\nawait qstash.publishJSON({\n  url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fprocess',\n  body: { document: largeDocumentContent },  \u002F\u002F 5MB!\n});\n\n\u002F\u002F GOOD: Reference only\nawait qstash.publishJSON({\n  url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fprocess',\n  body: { documentId: 'doc_123' },  \u002F\u002F Fetch in handler\n});\n```\n\n# In your handler:\n```typescript\nexport async function POST(req: NextRequest) {\n  const { documentId } = await req.json();\n  const document = await storage.get(documentId);  \u002F\u002F Fetch actual data\n  await processDocument(document);\n}\n```\n\n# Large data storage options:\n- S3\u002FR2\u002FBlob storage for files\n- Database for structured data\n- Redis for temporary data (Upstash Redis pairs well)\n\n### Not using callback\u002FfailureCallback for critical flows\n\nSeverity: MEDIUM\n\nSituation: Important task published. QStash delivers. Endpoint processes. But your\nsystem doesn't know it succeeded. User stuck waiting. No feedback loop.\n\nSymptoms:\n- No visibility into message delivery\n- Users waiting for actions that completed\n- No alerting on failures\n\nWhy this breaks:\nQStash is fire-and-forget by default. Without callbacks, you don't know\nif messages were delivered successfully. For critical flows, you need\nthe feedback loop to update state and handle failures.\n\nRecommended fix:\n\n# Use callbacks for critical operations:\n```typescript\nawait qstash.publishJSON({\n  url: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fsend-email',\n  body: { userId: '123', template: 'welcome' },\n  callback: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Femail-callback',\n  failureCallback: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Femail-failed',\n});\n```\n\n# Handle the callback:\n```typescript\n\u002F\u002F app\u002Fapi\u002Femail-callback\u002Froute.ts\nexport async function POST(req: NextRequest) {\n  \u002F\u002F Verify signature first!\n  const data = await req.json();\n\n  \u002F\u002F data.sourceMessageId - original message\n  \u002F\u002F data.status - HTTP status code\n  \u002F\u002F data.body - response from endpoint\n\n  await db.emailLogs.update({\n    where: { messageId: data.sourceMessageId },\n    data: { status: 'delivered' },\n  });\n\n  return NextResponse.json({ received: true });\n}\n```\n\n# Failure callback for alerting:\n```typescript\n\u002F\u002F app\u002Fapi\u002Femail-failed\u002Froute.ts\nexport async function POST(req: NextRequest) {\n  const data = await req.json();\n  await alerting.notify(`Email failed: ${data.sourceMessageId}`);\n  await db.emailLogs.update({\n    where: { messageId: data.sourceMessageId },\n    data: { status: 'failed', error: data.body },\n  });\n}\n```\n\n### Cron schedules using wrong timezone\n\nSeverity: MEDIUM\n\nSituation: Scheduled daily report at \"9am\". But 9am in which timezone? QStash uses UTC.\nReport runs at 4am local time. Users confused. Support tickets filed.\n\nSymptoms:\n- Schedules running at unexpected times\n- Off-by-one-hour issues during DST\n- User complaints about report timing\n\nWhy this breaks:\nQStash cron schedules run in UTC. If you think in local time but configure\nin UTC, schedules will run at unexpected times. This is especially tricky\nwith daylight saving time changes.\n\nRecommended fix:\n\n# QStash uses UTC:\n```typescript\n\u002F\u002F This runs at 9am UTC, not local time\nawait qstash.schedules.create({\n  destination: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fdaily-report',\n  cron: '0 9 * * *',  \u002F\u002F 9am UTC\n});\n```\n\n# Convert to UTC:\n- 9am EST = 2pm UTC (winter) \u002F 1pm UTC (summer)\n- 9am PST = 5pm UTC (winter) \u002F 4pm UTC (summer)\n\n# Document timezone in schedule name:\n```typescript\nawait qstash.schedules.create({\n  destination: 'https:\u002F\u002Fmyapp.com\u002Fapi\u002Fdaily-report',\n  cron: '0 14 * * *',  \u002F\u002F 9am EST (14:00 UTC)\n  body: JSON.stringify({\n    timezone: 'America\u002FNew_York',\n    localTime: '9:00 AM',\n  }),\n});\n```\n\n# Handle DST programmatically if needed:\nUpdate schedules when DST changes, or accept UTC timing\n\n### URL groups with dead or outdated endpoints\n\nSeverity: MEDIUM\n\nSituation: URL group has 5 endpoints. One service deprecated months ago. Messages\nstill fan out to it. Failures in dashboard. Wasted attempts. Slower delivery.\n\nSymptoms:\n- Failed deliveries in URL groups\n- Messages to deprecated services\n- Slow fan-out due to timeouts\n\nWhy this breaks:\nURL groups persist until explicitly updated. When services change, endpoints\nbecome stale. QStash tries to deliver to dead URLs, wastes retries, and\nthe failure noise obscures real issues.\n\nRecommended fix:\n\n# Audit URL groups regularly:\n```typescript\nconst groups = await qstash.urlGroups.list();\nfor (const group of groups) {\n  console.log(`Group: ${group.name}`);\n  for (const endpoint of group.endpoints) {\n    \u002F\u002F Check if endpoint is still valid\n    try {\n      await fetch(endpoint.url, { method: 'HEAD' });\n      console.log(`  OK: ${endpoint.url}`);\n    } catch {\n      console.log(`  DEAD: ${endpoint.url}`);\n    }\n  }\n}\n```\n\n# Update groups when services change:\n```typescript\n\u002F\u002F Remove dead endpoint\nawait qstash.urlGroups.removeEndpoints({\n  name: 'order-processors',\n  endpoints: [{ url: 'https:\u002F\u002Fold-service.myapp.com\u002Fapi\u002Fprocess' }],\n});\n```\n\n# Automate in CI\u002FCD:\nCheck URL group health as part of deployment\n\n## Validation Checks\n\n### Webhook signature verification\n\nSeverity: CRITICAL\n\nMessage: QStash webhook handlers must verify signatures using Receiver\n\nFix action: Add signature verification: const receiver = new Receiver({ currentSigningKey, nextSigningKey }); await receiver.verify({ signature, body, url })\n\n### Both signing keys configured\n\nSeverity: CRITICAL\n\nMessage: QStash Receiver must have both currentSigningKey and nextSigningKey for key rotation\n\nFix action: Configure both keys: new Receiver({ currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY, nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY })\n\n### QStash token hardcoded\n\nSeverity: CRITICAL\n\nMessage: QStash token must not be hardcoded - use environment variables\n\nFix action: Use process.env.QSTASH_TOKEN\n\n### QStash signing keys hardcoded\n\nSeverity: CRITICAL\n\nMessage: QStash signing keys must not be hardcoded\n\nFix action: Use process.env.QSTASH_CURRENT_SIGNING_KEY and process.env.QSTASH_NEXT_SIGNING_KEY\n\n### Localhost URL in QStash publish\n\nSeverity: CRITICAL\n\nMessage: QStash cannot reach localhost - endpoints must be publicly accessible\n\nFix action: Use a public URL (e.g., your deployed domain or ngrok for testing)\n\n### HTTP URL instead of HTTPS\n\nSeverity: ERROR\n\nMessage: QStash requires HTTPS URLs for security\n\nFix action: Change http:\u002F\u002F to https:\u002F\u002F\n\n### QStash publish without error handling\n\nSeverity: ERROR\n\nMessage: QStash publish calls should have error handling for rate limits and failures\n\nFix action: Wrap in try\u002Fcatch and handle errors appropriately\n\n### Using parsed JSON for signature verification\n\nSeverity: CRITICAL\n\nMessage: Signature verification requires raw body (req.text()), not parsed JSON\n\nFix action: Use await req.text() to get raw body for verification\n\n### Callback endpoint without signature verification\n\nSeverity: CRITICAL\n\nMessage: Callback endpoints must also verify signatures - they receive QStash requests too\n\nFix action: Add Receiver signature verification to callback handlers\n\n### Schedule without destination URL\n\nSeverity: ERROR\n\nMessage: QStash schedules require a destination URL\n\nFix action: Add destination: 'https:\u002F\u002Fyour-app.com\u002Fapi\u002Fendpoint' to schedule options\n\n## Collaboration\n\n### Delegation Triggers\n\n- complex workflow|multi-step|state machine -> inngest (Need durable step functions with checkpointing)\n- redis queue|worker process|job priority -> bullmq-specialist (Need traditional queue with workers)\n- ai background|long running ai|model inference -> trigger-dev (Need AI-specific background processing)\n- deploy|vercel|production|environment -> vercel-deployment (Need deployment configuration for QStash)\n- database|persistence|state|sync -> supabase-backend (Need database for job state)\n- auth|user context|session -> nextjs-supabase-auth (Need user context in message handlers)\n\n### Serverless Background Jobs\n\nSkills: upstash-qstash, nextjs-app-router, vercel-deployment\n\nWorkflow:\n\n```\n1. Define API route handlers (nextjs-app-router)\n2. Configure QStash integration (upstash-qstash)\n3. Deploy with environment vars (vercel-deployment)\n```\n\n### Reliable Webhooks\n\nSkills: upstash-qstash, stripe-integration, supabase-backend\n\nWorkflow:\n\n```\n1. Receive webhooks from Stripe (stripe-integration)\n2. Queue for reliable processing (upstash-qstash)\n3. Persist state to database (supabase-backend)\n```\n\n### Scheduled Reports\n\nSkills: upstash-qstash, email-systems, supabase-backend\n\nWorkflow:\n\n```\n1. Configure cron schedule (upstash-qstash)\n2. Query data for report (supabase-backend)\n3. Send via email system (email-systems)\n```\n\n### Fan-out Notifications\n\nSkills: upstash-qstash, email-systems, slack-bot-builder\n\nWorkflow:\n\n```\n1. Publish to URL group (upstash-qstash)\n2. Email handler receives (email-systems)\n3. Slack handler receives (slack-bot-builder)\n```\n\n### Gradual Migration to Workflows\n\nSkills: upstash-qstash, inngest\n\nWorkflow:\n\n```\n1. Start with simple QStash messages (upstash-qstash)\n2. Identify multi-step patterns\n3. Migrate complex flows to Inngest (inngest)\n4. Keep simple schedules in QStash\n```\n\n## Related Skills\n\nWorks well with: `vercel-deployment`, `nextjs-app-router`, `redis-specialist`, `email-systems`, `supabase-backend`, `cloudflare-workers`\n\n## When to Use\n- User mentions or implies: qstash\n- User mentions or implies: upstash queue\n- User mentions or implies: serverless cron\n- User mentions or implies: scheduled http\n- User mentions or implies: message queue serverless\n- User mentions or implies: vercel cron\n- User mentions or implies: delayed message\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.\n","","imported","https:\u002F\u002Fgithub.com\u002Fsickn33\u002Fantigravity-awesome-skills","user_system_seed","SkillOPIC",true,89,211,"2026-05-16 13:45:39",{"id":8,"name":21,"slug":22,"icon":23,"description":24,"sort":25,"createdAt":26},"其他","other","mdi-page-next-outline","其他类型Skill",5,"2026-05-16 12:53:40",{"id":7,"name":28,"slug":29,"icon":30,"description":31,"moduleId":8,"sort":32,"skillCount":33,"createdAt":26},"职场发展","career","mdi-briefcase-outline","面试准备、简历优化、职业规划",4,575,[35],{"id":36,"skillId":4,"version":37,"fileName":38,"fileSize":39,"filePath":40,"fileHash":41,"manifest":42,"createdAt":19},"01b0ee30-fa8b-49b0-abe4-3075f1a2824d","1.0.0","upstash-qstash.zip",8180,"uploads\u002Fskills\u002F153b0a3e-071d-4ffc-9f5c-3016f77399f4\u002Fupstash-qstash.zip","acac4f8fed8065cb55bfc534b1f10b2fff604972be0f3265ebf6998c8c06eb1d","[{\"path\":\"SKILL.md\",\"isDirectory\":false,\"size\":24972}]",{"code":44,"message":45,"data":46},200,"success",{"items":47,"stats":48,"page":51},[],{"averageRating":49,"totalRatings":49,"ratingCounts":50},0,[49,49,49,49,49],{"limit":52,"offset":49,"hasMore":53,"nextOffset":52,"ratedOnly":16},15,false]