Skip to content
Browse docs

VQueue setup

VQueue is an HTTP webhook queue included with Layerbase Valkey. It supports a documented subset of the @upstash/qstash client API. Check the supported operations below before migrating. No separate signup or queue bill; usage limits apply.

1. Create a Valkey database

On the Create Database page, pick Valkey. VQueue is included on every Valkey database, including the free tier, with nothing extra to turn on.

2. Copy the VQueue environment variables

Open your Valkey database in the dashboard, click Connect, and switch to the Parameters tab. The vqueue section at the bottom has all three values with copy buttons:

.env
VQUEUE_URL=...                 # your VQueue endpoint
VQUEUE_TOKEN=...               # publish/auth token
VQUEUE_CURRENT_SIGNING_KEY=... # verify incoming deliveries

3. Publish a job

Point the qstash Client at your VQueue endpoint:

bash
pnpm add @upstash/qstash
ts
import { Client } from '@upstash/qstash'

const client = new Client({
  baseUrl: process.env.VQUEUE_URL!,
  token: process.env.VQUEUE_TOKEN!,
})

await client.publishJSON({
  url: 'https://your-app.com/api/jobs',
  body: { hello: 'world' },
  retries: 3, // optional: three retries after the first attempt
  // delay: '30s', // optional, up to seven days
})

4. Verify signed deliveries

Every delivery is signed. Verify it in your handler with the Receiver and your current signing key so nobody can spoof your webhook endpoint:

ts
import { Receiver } from '@upstash/qstash'

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

const isValid = await receiver.verify({
  signature: req.headers['upstash-signature'],
  body: rawRequestBody,
  url: 'https://your-app.com/api/jobs',
})
if (!isValid) throw new Error('Invalid signature')

Delivery contract

Every accepted message is stored before the API acknowledges it. By default there is one initial attempt and three retries. Set retries to zero for a single durable attempt. A 2xx response completes a job; other status codes, including redirects, count as failures. Redirects are never followed. Attempts time out after 15 seconds, including DNS. Retry delays are 4, 16, then 64 seconds, capped at 64 seconds for further retries. Dispatch timing is approximate and can lag under load.

Deliveries can happen more than once, especially after a crash or an ambiguous network result. Use Upstash-Message-Id as your receiver idempotency key, backed by durable application state, and commit your side effect and deduplication record together where possible. A DLQ replay keeps the message ID. Set deduplicationId on publishes and reuse it when retrying an ambiguous response. For 24 hours, the same ID and request return the original message ID; changed content returns 409. The ID is scoped to one database. After 24 hours it can create a new job. Receiver idempotency is still required.

Supported operations

Contract tests use @upstash/qstash 2.11.0. Support is for the operations below, not every feature of that package or its future versions.

OperationSupport
publishJSON / batchJSONDurable single or transactional batch acceptance
deduplicationId24-hour durable deduplication; 1-128 non-space ASCII characters
delay / notBeforeMilliseconds, seconds, minutes, hours, days; notBefore is epoch seconds and takes precedence
HTTP methodsGET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS; GET and HEAD cannot have a body
Forwarded headers / Receiver.verifySupported; transport and delivery identity headers are reserved
dlq.listMessages / dlq.retryCursor pagination; replay explicit message IDs
Content-based deduplication, cron schedules, FIFO queues, URL groups, callbacks, workflows, custom retry expressionsNot supported. Unsupported delivery options are rejected; unsupported endpoints are unavailable.

Inspect and recover

Open Connect, Parameters, then VQueue. Load messages and filter by status to see the destination, attempt count, due time, and last error. Fix a failed receiver before replaying a dead-lettered job. Export a record for your own copy; exported bodies use base64. Replay requires confirmation. Removal is blocked until recoverable queue backups and retention are qualified. There is no cancel or pause control yet.

ts
const { messages, cursor } = await client.dlq.listMessages({ count: 25 })
// After fixing the receiver, replay one selected message:
await client.dlq.retry(messages[0].dlqId)
// Use cursor for the next page; inspect a message before replaying it.

Limits and storage

  • 64 KiB body, 8 KiB headers, 8,192-character destination URL; at most 100 messages and 8 MiB per batch request.
  • Schedule up to seven days ahead. Up to 10 retries after the initial attempt.
  • 1,000 local payloads per account on a server, across all databases, and 10,000 per host, including pending and failed jobs. Delivered payloads become eligible for archiving after 24 hours. Only a verified R2 readback releases local payload capacity. Failed backups keep the original payload. History remains bounded at 10,000 records per account and 100,000 per host. Admission stops at either limit or under disk pressure; export records and contact support if capacity stays full.
  • Publish admission: 600 messages per minute and 3,000 per ten minutes per database. These counters are process-local and reset on restart; these are caps, not promised throughput.
  • Public HTTP/HTTPS destinations only. Private, loopback, metadata, and reserved addresses, embedded URL credentials, and fragments are refused.

Jobs live in the cloud server's SQLite state store, not in your Valkey dataset or its database backups. Hibernation does not pause delivery; a stopped database or restricted account defers attempts. Delivered payloads archived to R2 remain readable through message lookup and dashboard export; retrieval never replays the job. Pending and dead-letter payloads do not expire. Queue archives have no automatic deletion. Server moves remain blocked while retained queue records exist. Host-loss recovery and failover are not yet qualified: do not rely on VQueue for irreplaceable jobs or a zero-loss recovery guarantee.

The publisher token is currently your database password. Keep it on your server. The signing key is derived from that password, so rotating the database password changes both credentials with no managed overlap. Update publishers and receivers together. Dedicated queue tokens and signing-key rotation are still pending.

Before switching from QStash

  1. Inventory the SDK operations and options your app uses against the table above.
  2. Test publishing, signature verification, forced failure, and replay with an endpoint you control.
  3. Make the receiver idempotent and check that its work completes within the attempt timeout.
  4. Drain the old queue before cutting over. Monitor the new queue and retain a rollback path for your publisher configuration.