Queue

The queue delivers a payload to a worker. It is at-least-once and fire-and-forget: there is no result storage and no per-message status. A message is enqueued, then a push-worker's HTTP handler is invoked to process it.

Enqueue

import { createSdk } from "@bext-stack/framework";

const sdk = createSdk("<app-id>");
const { id } = await sdk.queue.push("emails", { to: "a@b.com" }, /* delaySecs */ 30);

Or over HTTP:

curl -s http://127.0.0.1/__bext/sdk/queue/push \
  -H "X-Bext-App-Id: <app-id>" \
  -H "Content-Type: application/json" \
  -d '{"queue":"emails","payload":{"to":"a@b.com"},"delay_seconds":30}'

Endpoints

Scoped by X-Bext-App-Id. POST unless marked GET:

Endpoint Body / notes Returns
/queue/push { queue, payload, delay_seconds? } { id }
/queue/pull pull messages
/queue/ack acknowledge a message
/queue/stats GET queue stats
/queue/list GET queues
/queue/dead/list list dead-letter messages
/queue/dead/retry retry dead-letter messages
/queue/dead/purge purge dead-letter messages
/queue/worker/register { queue, handler_url, concurrency?, visibility_timeout_secs?, max_attempts? }
/queue/worker/unregister remove a worker
/queue/worker/list list registered workers

Push-worker handler contract

Register a push-worker with a handler_url. Register it as the app's public vhost URL — for example https://<app>.inklura.fr/api/queue/<name> — not a loopback address:

curl -s http://127.0.0.1/__bext/sdk/queue/worker/register \
  -H "X-Bext-App-Id: <app-id>" \
  -H "Content-Type: application/json" \
  -d '{
    "queue": "emails",
    "handler_url": "https://<app>.inklura.fr/api/queue/emails",
    "concurrency": 4,
    "visibility_timeout_secs": 60,
    "max_attempts": 5
  }'

The dispatcher POSTs this body to your handler_url:

{ "id": "<message-id>", "queue": "emails", "payload": { }, "attempts": 1 }

Your handler's response status decides the outcome:

Response Meaning
2xx ack — message done
408, 429, 5xx retry — redelivered later
any other 4xx dead-letter — moved to the dead queue

Implement the handler as a route.ts:

export async function POST(request: Request) {
  const { id, payload, attempts } = await request.json();
  try {
    await sendEmail(payload);
    return new Response(null, { status: 200 }); // ack
  } catch (e) {
    return new Response(String(e), { status: 500 }); // retry
  }
}

Semantics to design for

  • At-least-once — a handler may receive the same message more than once. Make handlers idempotent (e.g. dedupe on id).
  • Fire-and-forget — the enqueuer gets an id but nothing else; there is no built-in way to ask "did message X succeed?".
  • Dead-letter — messages that exhaust retries (or return a non-retry 4xx) land in the dead queue; inspect / retry / purge with /queue/dead/*.

Layer a job record when you need status

Because there is no result store, apps that need outcomes keep a durable job record in KV — keys like job:<id> updated by the handler (queuedrunningcomplete / fail).

Queue vs Tasks

If you need long-running work with built-in status and progress, use the warm task-executor instead — see Tasks & Scheduler. The queue is best for high-volume, fire-and-forget delivery to a public handler.

Where to next

Page What it covers
Tasks & Scheduler Long jobs with status/progress; cron
KV Store The durable job-record pattern
Queue Workers Operating and observing workers