Queues
A queue is a durable, PostgreSQL-backed message queue provisioned inside one of your PostgreSQL managed services. Message data lives in a dedicated mdb_queue schema, transactional with your application data. Consumers claim messages using SELECT ... FOR UPDATE SKIP LOCKED, so multiple workers can drain a queue concurrently without contention or double-processing.
Queues use at-least-once delivery. A message that is claimed but never acknowledged reappears automatically when the visibility timeout expires.
There is no separate broker process. The queue is rows in a PostgreSQL table, so the same database engine that stores your application data also coordinates delivery. Producers INSERT rows; competing workers each claim a disjoint slice of the visible rows and process it independently. The sections below cover the lifecycle in detail; Delivery semantics summarizes the guarantees.
Prerequisites
- A running PostgreSQL managed service. Queues are only supported on PostgreSQL.
- The PostgreSQL service ID.
Create a queue
curl -u admin:password -X POST \
https://api.foundrydb.com/managed-services/{service-id}/queues \
-H "Content-Type: application/json" \
-d '{
"name": "order-events",
"database_name": "defaultdb",
"visibility_timeout_seconds": 60,
"max_attempts": 5,
"dlq_enabled": true
}'
Returns 201 Created:
{
"id": "q-uuid",
"service_id": "pg-service-uuid",
"name": "order-events",
"database_name": "defaultdb",
"visibility_timeout_seconds": 60,
"max_attempts": 5,
"dlq_enabled": true,
"status": "Pending",
"created_at": "2026-06-22T14:00:00Z",
"updated_at": "2026-06-22T14:00:00Z"
}
Provisioning is asynchronous. The queue transitions from Pending through Provisioning to Active as the agent creates the schema objects inside your database. Only Active queues accept messages.
Request fields
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Lowercase alphanumerics, hyphens, and underscores; max 128 characters |
database_name | string | service default | Target database within the PostgreSQL service |
visibility_timeout_seconds | int | 30 | How long a claimed message is hidden from other consumers, 1 to 43200 |
max_attempts | int | 5 | Maximum delivery attempts before a message moves to the DLQ, 1 to 100 |
dlq_enabled | bool | true | Whether exhausted messages are saved in mdb_queue.dead_messages |
List queues
curl -u admin:password \
https://api.foundrydb.com/managed-services/{service-id}/queues
{
"queues": [
{
"id": "q-uuid",
"name": "order-events",
"database_name": "defaultdb",
"status": "Active",
"visibility_timeout_seconds": 60,
"max_attempts": 5,
"dlq_enabled": true
}
]
}
Get a queue
curl -u admin:password \
https://api.foundrydb.com/managed-services/{service-id}/queues/order-events
The {queue-name} path parameter is the queue name, not an ID.
Enqueue messages
The HTTP enqueue path is brokered: the controller creates an agent task, the agent writes the batch in a single transaction on the primary VM, and returns the assigned message IDs. The endpoint returns 202 Accepted with a task ID to poll.
This path is useful for external producers, CI scripts, or any caller without a direct database connection.
curl -u admin:password -X POST \
https://api.foundrydb.com/managed-services/{service-id}/queues/order-events/messages \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"payload": {"order_id": 1001, "event": "created"}},
{"payload": {"order_id": 1002, "event": "paid"}, "delay_seconds": 30}
]
}'
Returns 202 Accepted:
{"task_id": "task-uuid"}
Poll for enqueue result
curl -u admin:password \
"https://api.foundrydb.com/managed-services/{service-id}/queues/order-events/messages?task_id=task-uuid"
When the task completes, the response includes the assigned message IDs in request order:
{
"task_id": "task-uuid",
"status": "completed",
"result": {
"message_ids": [4217, 4218]
}
}
While the task is pending or running, the response returns 202 with "status": "pending" or "status": "running".
Message fields
| Field | Type | Description |
|---|---|---|
payload | object | Arbitrary JSON document, max 256 KB |
delay_seconds | int | Postpone first visibility by this many seconds (0 to 43200) |
A batch may contain up to 100 messages.
Direct enqueue (recommended for application code)
For application code running with access to the injected database connection, enqueue directly via SQL. This is faster and allows atomic enqueue within your own transaction:
-- Enqueue one message
INSERT INTO mdb_queue.messages (queue_name, payload, max_attempts, visible_at)
SELECT q.name, $2, q.max_attempts, now() + ($3 * interval '1 second')
FROM mdb_queue.queues q WHERE q.name = $1
RETURNING id;
The mdb_queue schema and the SQL contract are documented in the Queue SQL reference section below.
Queue stats
Stats are also brokered via the agent. First request a snapshot, then poll for the result.
Request stats
curl -u admin:password -X POST \
https://api.foundrydb.com/managed-services/{service-id}/queues/order-events/stats
Returns 202 Accepted:
{"task_id": "task-uuid"}
Poll for stats
curl -u admin:password \
"https://api.foundrydb.com/managed-services/{service-id}/queues/order-events/stats?task_id=task-uuid"
{
"task_id": "task-uuid",
"status": "completed",
"result": {
"queue_name": "order-events",
"ready_messages": 142,
"inflight_messages": 8,
"dead_messages": 3,
"oldest_age_seconds": 47.2
}
}
| Field | Meaning |
|---|---|
ready_messages | Visible messages available for claim |
inflight_messages | Claimed messages currently hidden from other consumers |
dead_messages | Messages that exhausted max_attempts and moved to the DLQ |
oldest_age_seconds | Age of the oldest ready message; useful for consumer lag alerts |
Delete a queue
Deletion is asynchronous. The queue transitions to Deprovisioning while the agent removes the schema objects, then the registry row is soft-deleted.
curl -u admin:password -X DELETE \
https://api.foundrydb.com/managed-services/{service-id}/queues/order-events
Returns 202 Accepted with the queue row in Deprovisioning status.
Queue SQL reference
Your application interacts with the queue directly over its injected PostgreSQL connection. The mdb_queue schema is owned by the platform but your primary user has full data access.
Claim messages
-- Claim up to 10 messages with a 60-second visibility window.
-- Run in autocommit, not inside a long-running transaction.
UPDATE mdb_queue.messages m
SET visible_at = now() + (60 * interval '1 second'),
attempt_count = m.attempt_count + 1,
claimed_by = 'worker-1'
WHERE m.id IN (
SELECT id FROM mdb_queue.messages
WHERE queue_name = 'order-events'
AND visible_at <= now()
AND attempt_count < max_attempts
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 10
)
RETURNING m.id, m.payload, m.attempt_count, m.enqueued_at;
SKIP LOCKED ensures that concurrent consumers each claim a disjoint set of rows without blocking each other.
Acknowledge a message
-- Ack: delete the message once processing succeeds.
-- Token-gated: an expired claim that was re-claimed by another consumer sees zero rows.
DELETE FROM mdb_queue.messages WHERE id = $1 AND claimed_by = $2;
Return a message early (nack)
-- Nack: reschedule the message for redelivery after a backoff, with an error note.
UPDATE mdb_queue.messages
SET visible_at = now() + ($3 * interval '1 second'),
last_error = $4
WHERE id = $1 AND claimed_by = $2;
Extend a claim
-- If processing takes longer than expected, extend the claim to prevent redelivery.
UPDATE mdb_queue.messages
SET visible_at = now() + ($3 * interval '1 second')
WHERE id = $1 AND claimed_by = $2;
Delivery semantics
- At-least-once. A message is redelivered if the consumer crashes between claiming and acknowledging it, or if the visibility timeout expires before ack. Design consumers to be idempotent: the same message may be processed more than once.
- Competing consumers via
SKIP LOCKED. The claim statement selects ready rowsFOR UPDATE SKIP LOCKED, so each concurrent consumer locks and claims a disjoint set of rows and skips rows another consumer already holds. Workers never block on each other and never claim the same row, which is what prevents double-processing while letting you scale out by simply running more workers against the same queue. - Claim and visibility timeout. Claiming a message sets
visible_at = now() + visibility_timeout, which hides the row from other consumers for that window, and incrementsattempt_count. If the worker acknowledges within the window the row is gone; if not, the row becomes visible again oncevisible_at <= now()and is eligible for another claim. Use extend a claim to pushvisible_atfurther out when processing legitimately runs long. - Ack is a token-gated delete. Acknowledging deletes the row only when
claimed_bystill matches the acknowledging worker (DELETE ... WHERE id = $1 AND claimed_by = $2). If the visibility window already expired and another consumer re-claimed the row, the stale worker's ack matches zero rows and cannot delete work that now belongs to someone else. - Nack reschedules with backoff. Returning a message early sets
visible_at = now() + backoffand recordslast_error, so the row reappears for redelivery after your chosen delay instead of being retried immediately.attempt_countwas already incremented at claim time, so each redelivery counts against the budget. - Dead-letter after
max_attempts. A message keeps being redelivered untilattempt_countreaches the queue'smax_attempts. Rows that exhaust the budget move tomdb_queue.dead_messages(whendlq_enabledis true) instead of cycling forever; thedead_messagescount surfaces in queue stats. - DLQ sweep is inline. When a consumer claims messages, the claim statement first sweeps any over-budget messages to
mdb_queue.dead_messagesin the same atomic operation. No background daemon is required. - Ordered by ID within a claim batch. Rows in one claim are ordered by
id, but there is no global FIFO guarantee across concurrent consumers, and a redelivered message returns to the queue behind newer rows. - SKIP LOCKED is safe in autocommit. Claim in autocommit or a short transaction. Holding claimed rows inside a long transaction blocks the visibility index and degrades throughput.
Queue statuses
| Status | Meaning |
|---|---|
Pending | Create request received; agent task queued |
Provisioning | Agent is creating schema objects in the database |
Active | Queue is ready for enqueue and claim operations |
Deprovisioning | Deletion in progress; agent is removing schema objects |
Failed | Provisioning or deprovisioning failed; see error_message |