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.
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.
- Ordered by ID within a claim batch. No global FIFO guarantee across concurrent consumers.
- 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. - 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 |