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 three 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).
workflowtarget is a workflow name; the job starts a run of that durable workflow with payload as its input.

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