# Queues
Source: https://sovaria.in/docs/queues

Durable message queues: send work, read it with a visibility timeout, archive or delete it, dead-letter what keeps failing, and let a function consume it.

A queue keeps messages until something processes them. Producers **send** JSON messages (optionally delayed); consumers **read** them, which hides each message for a **visibility timeout**, and **archive** or **delete** it when the work is done. If a consumer crashes, the message simply becomes visible again. With **max reads**, a message that keeps failing moves to a **dead-letter queue**. The API mirrors Supabase Queues (pgmq): `send`, `send_batch`, `read`, `pop`, `archive`, `delete`, `set_vt`, `purge`, `metrics`.

(Diagram: A message's life)

## Create a queue and send messages

CLI:
```bash
sov queues create emails-dead
sov queues create emails --vt 60 --max-reads 5 --dlq emails-dead
sov queues send emails '{"to": "ada@example.com", "template": "welcome"}'
sov queues send emails '{"to": "bob@example.com"}' --delay 300
sov queues metrics emails
```

JavaScript:
```js
await sov.api.queues.create({ name: 'emails', visibilityTimeoutSec: 60, maxReads: 5, deadLetterQueue: 'emails-dead' })
const { msg_id } = await sov.api.queues.send({ queueName: 'emails', message: { to: 'ada@example.com' } })
await sov.api.queues.sendBatch({ queueName: 'emails', messages: [{ to: 'a@x.io' }, { to: 'b@x.io' }], delay: 30 })
```

Python:
```python
await client.api.queues.send(queue_name="emails", message={"to": "ada@example.com"})
batch = await client.api.queues.read(queue_name="emails", qty=10, vt=120)
for m in batch["messages"]:
    deliver(m["message"])
    await client.api.queues.archive(queue_name="emails", msg_ids=[m["msg_id"]])
```

HTTP:
```http
POST /api/v1/queues                       {"name": "emails", "visibility_timeout_sec": 60, "max_reads": 5, "dead_letter_queue": "emails-dead"}
POST /api/v1/queues/emails/send           {"message": {...}, "delay": 0}            → {"msg_id": 41}
POST /api/v1/queues/emails/send_batch     {"messages": [...]}                       → {"msg_ids": [42, 43]}
POST /api/v1/queues/emails/read           {"qty": 10, "vt": 120}                    → {"messages": [...]}
POST /api/v1/queues/emails/pop            {"qty": 1}                                  read + delete
POST /api/v1/queues/emails/archive        {"msg_ids": [41]}
POST /api/v1/queues/emails/delete         {"msg_ids": [42]}
POST /api/v1/queues/emails/set_vt         {"msg_id": 43, "vt": 300}
POST /api/v1/queues/emails/purge          {"include_archived": false}
GET  /api/v1/queues/emails/metrics
GET  /api/v1/queues/emails/messages?state=waiting|archived|dead
```

## Let a function consume a queue

Bind a deployed function and Sovaria hands it batches of messages through the background job queue. A run that returns acknowledges its batch (archives it, or deletes it with `ack: "delete"`); a run that throws hands the messages back with a short backoff, and `max_reads` sends a message that keeps failing to the dead-letter queue. Process each message idempotently by `msg_id`.

CLI:
```bash
sov queues update emails --consumer send-email --batch 10 --concurrency 2
```

index.mjs:
```js
export async function handler(event, context) {
  // event = { queue: 'emails', messages: [{ msg_id, read_ct, enqueued_at, message, headers }] }
  // context.caller.role === 'queue'; context.queue = { name, id, message_ids }
  for (const m of event.messages) await sendEmail(m.message, { idempotencyKey: `email-${m.msg_id}` })
  return { sent: event.messages.length }
}
```

## Who can use a queue

- The project team, with the **queues.read** permission (list, metrics, look at messages) or **queues.write** (create, send, read, archive, delete, purge — reading hides messages, so it is a write).
- Service keys and your functions (their own keys).
- App users and the anon key: **send only**, and only to a queue whose owner turned on **app send** (`app_send: true`). Use it when the app enqueues work for the backend; nobody can read a queue from the app.
- Queues belong to the environment they were made in; every change is audited.

## Plans

|  | Free | Pro | Ultra | Team | Enterprise |
|---|---|---|---|---|---|
| Queues per project (`queues.max_queues`) | 3 | 25 | 100 | 250 | 1,000 |
| Messages sent per month (`queues.messages_month`) | 100,000 (then stops) | 2,000,000 | 10,000,000 | 50,000,000 | 200,000,000 |
| Largest message (`queues.max_message_bytes`) | 16 KB | 256 KB | 256 KB | 1 MB | 1 MB |
| Stored per project (`queues.storage_bytes`) | 50 MB | 2 GB | 10 GB | 50 GB | 200 GB |
| Archived / dead messages kept (`queues.retention_days`) | 1 day | 7 days | 14 days | 30 days | 30 days |

Paid plans bill messages beyond the allowance at $0.40 per million. Consumer runs count as function invocations. Large payloads: store them in a document or a file and send the id. See [Plan limits](https://sovaria.in/docs/plan-limits).

## Compared with Supabase Queues

|  | Sovaria | Supabase Queues (pgmq) |
|---|---|---|
| send / read / pop / archive / delete / set_vt / purge / metrics | Yes | Yes |
| Dead-letter queue after N reads | Built in | Do it yourself |
| A function consumes the queue | Built in (batches, concurrency, backoff) | Write a cron + Edge Function poller |
| Delay | Per send | Per send |
| Client-side sends | Opt-in per queue, send only | Through RLS on the queue tables |
| Console | Metrics, browse waiting / archived / dead, send, settings | Browse and send |
