# Queues

> Declared queues, triggers that run functions for their messages, inbound webhooks, and dead letters.

[HTML version](https://snoozestack.com/docs/queues/) · [Docs index](https://snoozestack.com/llms.txt)

A queue is a durable to-do list your system writes to now and processes later. Declare one in `project.toml` — with the [functions](https://snoozestack.com/docs/functions.md) 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

```bash title="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
```

```bash title="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_receives`th 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:

```bash title="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)
```

| Surface | Availability |
| --- | --- |
| Portal UI | Each 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 |
| 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>` |

**Diagram:** External sender · Stripe, GitHub, … · POST · <ref>.snzzz.com · /api/webhooks/<name> · write only · Your producers · SDK, CLI, functions · send · Queue · [[queues]] · trigger · Linked function(s) · all must succeed to ack · max_receives exceeded · Dead letters · inspect · redrive

### Sending a message

Enqueueing is a trusted operation — a queued message can become work, or a [push notification](https://snoozestack.com/docs/push-notifications.md), 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.

```bash title="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.

```bash title="snoozestack/project.toml"
[[webhooks]]
name = "stripe-events"
queue = "jobs"
```

```bash title="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.

```ts title="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.
