# Can I use Postgres as a message queue?

> Yes. Every SupaHost project can enable Queues (pgmq), a Postgres-native message queue with visibility timeouts and archiving, managed from the dashboard's Queues tab.

Source: https://supahost.app/kb/postgres-as-queue · Updated 2026-11-11

Yes. SupaHost projects include Queues, built on the `pgmq` extension. Messages are rows in Postgres tables, so sending a message can be part of the same transaction as the data change that caused it, and nothing extra has to run. Turn it on in Dashboard → your project → **Queues** tab → **Enable Queues**.

## How pgmq works

You `send` a JSON message to a named queue. A consumer `read`s one or more messages, which hides them for a *visibility timeout* (for example 30 seconds). If the consumer finishes, it `delete`s or `archive`s the message. If it crashes, the timeout runs out and the message becomes visible again for another try. That gives at-least-once delivery, so make your handlers safe to run twice.

## Example: sending receipts for an invoice tool

Create a queue (or use **Queues** tab → new queue):

```sql
select pgmq.create('receipts');
```

Enqueue a message in the same transaction that marks an invoice paid:

```sql
begin;
update public.invoices set status = 'paid', paid_at = now() where id = 4211;
select pgmq.send('receipts', jsonb_build_object('invoice_id', 4211));
commit;
```

Consume from a server or Edge Function with the `service_role` key, through the `pgmq_public` wrappers that the Data API exposes:

```ts
const { data: msgs, error } = await admin.schema('pgmq_public').rpc('read', {
  queue_name: 'receipts',
  sleep_seconds: 30, // visibility timeout
  n: 10,
})

for (const m of msgs ?? []) {
  await sendReceipt(m.message.invoice_id)
  await admin.schema('pgmq_public').rpc('archive', { queue_name: 'receipts', message_id: m.msg_id })
}
```

To process the queue regularly, call that function from a pg_cron job (see [How do I run background jobs?](/kb/background-jobs)).

## SupaHost specifics

- `pgmq_public` has `send`, `send_batch`, `read`, `pop`, `archive` and `delete`. Execute is granted to `service_role` only. If you want signed-in users to send messages from the browser, add the grants and RLS on the queue table yourself.
- The Queues tab shows Waiting, Total sent and Oldest per queue, the newest 25 messages, and Purge and Delete actions.
- "Unlogged" queues are faster but aren't crash-safe: messages can be lost if Postgres restarts unexpectedly.

## FAQ

### Is Postgres fast enough for a queue?

For most apps, yes: emails, webhooks, image jobs and similar background work come nowhere near Postgres's limits, and the queue shares your instance's resources with everything else. If you need millions per second or fan-out to many independent consumers, a dedicated broker is a better fit.

### Why do my messages come back after I read them?

The visibility timeout expired before you deleted or archived them. Use a longer timeout or finish faster. See [Queue messages reappearing](/kb/queue-messages-reappearing).

### Can I build my own queue with SKIP LOCKED instead?

Yes. A `jobs` table read with `select ... for update skip locked limit 10` works well, and you'd call it through a database function. pgmq saves you writing that yourself.

## Related

- [Queues with pgmq](/docs/cron-queues/queues)
- [Processing queue messages](/docs/cron-queues/queue-consumers)
- [Scheduled jobs with pg_cron](/docs/cron-queues/cron)
