snoozestackdocs

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 runs a local queue with the managed queue's shape (a send is accepted and delivered on the next tick, retried after a visibility window, and dead-lettered after max_receives) so retries and failure paths are testable on your machine, and snoozestack publish creates the managed queue and trigger hosted.

Why use it

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 functions list can name several, and each gets every message.

Declaring a queue#

snoozestack/project.toml
[[queues]]
name = "jobs"
functions = ["handle-payment"] # the consumer(s) — every message goes to each
max_receives = 3 # attempts before a message dead-letters
terminal
snoozestack dev # queues run locally, in .snoozestack/queues.db
snoozestack queues send jobs '{"kind": "resize", "id": 42}' # smoke test
snoozestack queues list
# jobs messages=1 in_flight=0 delayed=0

snoozestack publish creates the hosted queue and its trigger, and snoozestack preview your preview's. A declaration 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, snoozestack dev keeps the queue in .snoozestack/queues.db and runs it the way the managed queue runs: a POST /_queues/<name> answers 202 at once (add ?delay=<seconds> for a delayed message), a drain tick every 10 seconds delivers what is due to each declared function, a failed delivery becomes visible again after a 30-second visibility window with its receive count one higher, and the max_receivesth failure moves it to the dead-letter list — the same sequence a consumer meets hosted, so one that is not idempotent, or that assumes delivery is synchronous, is found out here. GET /_queues/<name> and the Queues page show the depth (messages · in flight · delayed), recent deliveries and dead letters. A test that must not wait can switch to inline delivery:

snoozestack/dev.toml
[dev]
queues = "immediate" # a POST runs the consumer inline and returns every attempt's outcome (or: snoozestack dev --queues=immediate)
queue_tick_ms = 500 # the drain cadence in async mode (default 10000)
queue_visibility_ms = 500 # how long a failed delivery stays invisible (default 30000)
SurfaceAvailability
Portal UIEach declared queue with the functions it dispatches to, its hosted depth (messages · in flight · delayed), recent deliveries and dead-letter count; a Triggers card with each trigger's last run; the webhooks feeding it; Notifications for push-sending triggers
CLIsnoozestack queues list/push/send/dead-letters/delete, triggers …, webhooks … — each scoped by --dev/--preview
SDK / HTTPsnoozestack.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.

from your own server
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.

snoozestack/project.toml
[[webhooks]]
name = "stripe-events"
queue = "jobs"
terminal
snoozestack publish
snoozestack webhooks list
# stripe-events → https://<ref>.snzzz.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>, enqueueing into the same local queue (and answering 202, as the hosted URL does) so the body retries and dead-letters exactly as hosted.

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.

snoozestack/functions/handle-payment/index.ts
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 queue alike, each redelivery after the visibility window) 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.