Setup guide

Set up a durable queue with pgmq

Create a Postgres-backed queue for small jobs, retries, and simple event handoff.

The Goal

Use one Postgres queue for one job flow.

Start small. A queue should have a clear job: send emails, process billing events, deliver webhooks, or run imports.

Create The Queue

create extension if not exists pgmq;

select pgmq.create('email_delivery');

Send one message:

select pgmq.send(
  'email_delivery',
  '{"task":"send_welcome_email","user_id":42}'
);

The payload should include enough data for the worker to find the real record. Do not put your whole business object in the message if the source row already lives in Postgres.

Read Messages

select *
from pgmq.read('email_delivery', 30, 5);

The second argument is the visibility timeout in seconds. The third is the number of messages to read.

Pick a visibility timeout longer than normal job time. If most email jobs finish in 3 seconds, 30 seconds is fine. If imports take 10 minutes, use a larger timeout or split the job.

Finish The Job

After the worker succeeds, remove or archive the message.

select pgmq.archive('email_delivery', 123);

Archiving is useful while you are learning. It leaves a record you can inspect. For high-volume queues, you may choose deletion later.

Make The Worker Safe

The worker may see the same message again after a crash or timeout. Plan for that.

For an email job, store a sent record. For a webhook, store attempts. For a payment, use the provider’s idempotency key.

The queue gives you durability. It does not make your side effects safe by itself.

Add Failure Handling Early

Track these from the start:

You do not need a big dashboard. A small table and a few queries are enough for the first version.

When To Stop Here

Stay with pgmq while the job flow is simple and close to Postgres.

Move on when you need many independent consumers, stream replay, long retention, or a platform that many teams share.

References