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.

FunctionWhat 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}$.

Queues are reachable over SQL today. A client-facing REST/RPC queue API is planned but not shipped yet, so for now enqueue and dequeue from a direct SQL connection or a function.

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:

KindEffect
enqueuetarget is a queue name; the job enqueues its payload onto that queue.
webhooktarget 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:

HeaderValue
X-Kethosbase-TimestampUnix seconds when the request was signed.
X-Kethosbase-SignatureLower-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