Skip to main content

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

FieldTypeDefaultDescription
namestringrequiredLowercase alphanumerics, hyphens, and underscores; max 128 characters
database_namestringservice defaultTarget database within the PostgreSQL service
visibility_timeout_secondsint30How long a claimed message is hidden from other consumers, 1 to 43200
max_attemptsint5Maximum delivery attempts before a message moves to the DLQ, 1 to 100
dlq_enabledbooltrueWhether 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

FieldTypeDescription
payloadobjectArbitrary JSON document, max 256 KB
delay_secondsintPostpone first visibility by this many seconds (0 to 43200)

A batch may contain up to 100 messages.

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
}
}
FieldMeaning
ready_messagesVisible messages available for claim
inflight_messagesClaimed messages currently hidden from other consumers
dead_messagesMessages that exhausted max_attempts and moved to the DLQ
oldest_age_secondsAge 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_messages in 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

StatusMeaning
PendingCreate request received; agent task queued
ProvisioningAgent is creating schema objects in the database
ActiveQueue is ready for enqueue and claim operations
DeprovisioningDeletion in progress; agent is removing schema objects
FailedProvisioning or deprovisioning failed; see error_message