Queues
A queue is a durable to-do list your system writes to now and processes later. Declare one in project.toml — with the functions that should process its messages — and the same declaration runs in both places: snoozestack dev dispatches deterministically in-process so retries and failure paths are testable on your machine, and snoozestack publish creates the managed queue and trigger hosted.
Queues exist to break the assumption that work has to finish while someone is waiting. Without one, every slow or failure-prone step — charging a card, transcoding a video, calling a flaky third-party API — sits inside the user's request. The request is only as fast as its slowest dependency, and if that dependency is down, the user sees the error.
Putting a message on a queue instead splits one risky operation into two reliable ones: an enqueue that is fast and almost never fails, and a consume that can take as long as it needs and retry on its own. That buys you three things — a responsive request path, a buffer that absorbs traffic spikes instead of collapsing under them, and automatic retries, since a message stays on the queue until something successfully handles it.
The cost is eventual consistency: the work is promised, not done. If the caller needs the answer in the response, don't use a queue — call the function directly.
- Receiving third-party webhooks
- Stripe, GitHub, and Twilio all retry — and eventually give up — if your endpoint is slow. The inbound webhook URL below writes straight to the queue with no project compute in the path, so you accept in milliseconds and process afterwards.
- Slow work triggered by a user action
- Image resizing, PDF generation, sending a welcome email. Insert the row, enqueue the job, return immediately — the user isn't waiting on an email provider.
- Smoothing out spikes
- A launch or a cron-driven stampede produces more work per second than your downstream can take. The queue holds the backlog and the consumer drains it at a steady rate, instead of every request timing out at once.
- Fanning one event out to several handlers
- One “order placed” message can run a receipt email, a warehouse sync, and an analytics roll-up — a queue's
functionslist can name several, and each gets every message.
Declaring a queue#
[[queues]]name = "jobs"functions = ["handle-payment"] # the consumer(s) — every message goes to eachmax_receives = 3 # attempts before a message dead-letterssnoozestack dev # queues run locally, in-processsnoozestack queues send jobs '{"kind": "resize", "id": 42}' # smoke testsnoozestack queues list# jobs messages=1 in_flight=0 delayed=0snoozestack publish (or queues push alone) creates the hosted queue and its trigger. Push only ever creates or updates — removing a [[queues]] entry never silently destroys a hosted queue; queues delete is the only removal, and it prompts. Each namespace has its own queues, so a preview's backlog never leaks into live. Locally, a queue is inspectable at GET /_queues/<name> on the dev server.
| Surface | Availability |
|---|---|
| Portal UI | Per-queue gauges (messages, in flight, delayed) and the dead-letter view; Notifications for push-sending triggers |
| CLI | snoozestack queues list/push/send/dead-letters/delete, triggers …, webhooks … — each scoped by --dev/--preview |
| SDK / HTTP | snoozestack.queues.send(queue, message) from trusted server code, or POST /api/queues/v1/<queue> |
Sending a message#
Enqueueing is a trusted operation — a queued message can become work, or a push notification, so browsers and app binaries don't get to do it directly. Send from your own functions or server-side code, or let a webhook write inbound traffic straight onto the queue.
import { createClient } from "snoozestack-js"; const snoozestack = createClient(process.env.SNOOZESTACK_URL, process.env.SNOOZESTACK_API_KEY);await snoozestack.queues.send("jobs", { kind: "booking-confirmed", id: 42 });Triggers#
A trigger is the link between a queue and what happens to its messages. Declaring functions on the [[queues]] entry is the usual way to get one — publish creates it alongside the queue. The standalone snoozestack triggers commands cover the rest: linking a queue to a push notification service (--notify), pausing without deleting (disable), and one-off wiring outside the file. A trigger fires on any message the queue receives — webhook, SDK send, or CLI — and a message is only removed once every destination succeeded.
Webhooks#
A webhook gives a queue a public inbound URL so outside systems can put messages on it — nothing more. It runs no functions on its own; the queue's trigger processes what it delivers.
[[webhooks]]name = "stripe-events"queue = "jobs"snoozestack publishsnoozestack webhooks list# stripe-events → https://<ref>.snoozestack.com/api/webhooks/stripe-events/<token>The URL is on your project's own host, and the write path runs on managed queue infrastructure with no project compute in it — it keeps accepting even while you redeploy. The unguessable <token> in the URL is the credential; point Stripe or GitHub at it directly. Locally, snoozestack dev serves the same webhook at POST /_webhooks/<name>, landing messages in the same local queue.
Writing the consumer#
A triggered function receives the message's raw body as its request body — the same (req, capabilities) contract as every function. The response is the contract that matters: 2xx means “done, delete it”, anything else means “retry later.” Since a retry can redeliver a message you already partly handled, make the handler idempotent — key the work on something stable from the payload so processing it twice is harmless.
export default async function handler(req: Request, capabilities) { const event = await req.json(); // Idempotency: the provider's own event id is the natural key. A unique // index on event_id plus "on conflict do nothing" turns a redelivery into // a no-op instead of a double charge. try { capabilities.db.query( "insert into payments (event_id, amount) values (?, ?) on conflict (event_id) do nothing", [event.id, event.amount], ); } catch (err) { // Non-2xx leaves the message on the queue for redelivery, so a // transient blip fixes itself. console.error("payment upsert failed", err); return new Response("retry", { status: 500 }); } return new Response("ok"); // 2xx → message deleted}Dead letters#
A message that keeps failing is redelivered up to max_receives times (default 3 — hosted and in snoozestack dev's local simulation alike) and then moves to the queue's dead-letter queue instead of retrying forever. snoozestack queues dead-letters <name> lists what's sitting there — the same view as the console and, locally, GET /_queues/<name> — so a permanently bad payload is something you inspect and fix, not an invisible retry loop.