Queues & Cron
Durable message queues that live in your database, scheduled jobs on a cron expression, and HMAC-signed outbound webhooks — all built in, no extension to install.
Queues
Every project has a pgmq schema of queue functions installed
in its database, owned by the project. You use them over a
direct SQL connection. A queue is created on demand
and backed by ordinary tables, so a message is as durable as any row.
| Function | What it does |
|---|---|
pgmq."create"(queue_name) | Create a queue. |
pgmq.send(queue_name, msg jsonb, delay int = 0) | Enqueue a message; delay seconds before it becomes visible. Returns the message id. |
pgmq.read(queue_name, vt_seconds, qty) | Claim up to qty visible messages and hide them for vt_seconds (the visibility timeout). |
pgmq.pop(queue_name) | Claim and delete one message in a single step. |
pgmq."delete"(queue_name, msg_id) | Delete a message you have finished processing. |
pgmq.archive(queue_name, msg_id) | Move a message to the queue's archive table instead of deleting it. |
-- producer
select pgmq.send('emails', '{"to": "a@example.com", "template": "welcome"}');
-- consumer: claim up to 10 messages, hidden for 30s while you work
select msg_id, message from pgmq.read('emails', 30, 10);
-- on success, remove them
select pgmq."delete"('emails', 42);
read and pop claim rows with
FOR UPDATE SKIP LOCKED, so many workers can drain one queue
without stepping on each other: a claimed message is invisible to other
readers until its visibility timeout lapses, at which point — if you have not
deleted it — it becomes available again for retry. Queue names must match
^[a-z][a-z0-9_]{0,47}$.
Cron
Schedule a job on a standard five-field cron expression (minute, hour, day-of-month, month, day-of-week), interpreted in UTC. Manage jobs on the project's Cron tab in the dashboard, or through the management API (owner or admin session):
GET /v1/projects/{ref}/cron-jobs
POST /v1/projects/{ref}/cron-jobs # { name, schedule, target_kind, target, payload }
DELETE /v1/projects/{ref}/cron-jobs/{name}
A job fires one of two target_kinds:
| Kind | Effect |
|---|---|
enqueue | target is a queue name; the job enqueues its payload onto that queue. |
webhook | target is an https:// URL; the job sends a signed HTTP POST (see below). |
The scheduler claims due jobs with SKIP LOCKED, so running
more than one instance is safe. Scheduling granularity is one minute; a job
with a schedule that fails to parse is disabled rather than run.
Webhooks
A webhook cron job delivers a signed POST to your
URL. Each job has its own signing secret, shown once when the
job is created — copy it then; it cannot be read back. Every delivery carries
two headers:
| Header | Value |
|---|---|
X-Kethosbase-Timestamp | Unix seconds when the request was signed. |
X-Kethosbase-Signature | Lower-case hex HMAC-SHA256 of "<timestamp>.<body>", keyed by the job's secret. |
Verify a delivery by recomputing the HMAC over the timestamp, a literal
., and the exact raw body, then comparing in constant time. Bind
the timestamp into the signature so a captured body cannot be replayed later,
and reject requests whose timestamp is too old for your tolerance:
# pseudocode for the receiver
signed = timestamp + "." + raw_request_body
expected = hmac_sha256(job_secret, signed) # lower-case hex
if not constant_time_equals(expected, header["X-Kethosbase-Signature"]):
reject(401)
if now - int(header["X-Kethosbase-Timestamp"]) > tolerance_seconds:
reject(401)
Deliveries go out through an SSRF-guarded HTTP client: the resolved IP is
checked at dial time, private / loopback / link-local addresses (including
the cloud metadata address) are refused, and redirects are not followed. A
delivery counts as delivered only on a 2xx response.
Limits
- Cron jobs are list / create / delete — there is no edit or manual "run now"; change a schedule by deleting and recreating the job.
- Webhooks are fire-and-forget: no automatic retries or per-attempt history, only the job's last status. Build idempotency and any retry expectation into your receiver.
- A webhook secret cannot be rotated in place — delete and recreate the job to get a new one.
- Schedules resolve to one-minute granularity; there are no sub-minute jobs.