Queue
The queue delivers a payload to a handler in your app. It is at-least-once and fire-and-forget: it stores no result and exposes no per-message status. You enqueue a message, then the platform calls the HTTP handler registered for that queue.
Enqueue
import { createSdk } from "@bext-stack/framework/sdk";
const sdk = createSdk("<app-id>");
const id = await sdk.queue.push("emails", { to: "a@b.com" });
Call it server-side (loader, action, route.ts or task). push(queue, payload, delaySecs?)
returns the message id and throws if the platform refuses the enqueue. A non-string payload is
serialised with JSON.stringify; a string is sent as is.
The third argument, delaySecs, postpones delivery: the message is handed out only after that
delay, in whole seconds (a non-integer value is ignored and the message goes out at once). For
recurring work, use a schedule instead.
Registering a handler
Each queue is processed by a handler: an HTTP URL in your app that the platform calls for every message. The framework client has no call to register one: HTTP handlers are enabled for your app by the platform, with these settings:
| Setting | Required | Role |
|---|---|---|
queue |
yes | Name of the queue to process |
handler_url |
yes | Handler URL, for example https://<app>.inklura.fr/api/queue/emails |
concurrency |
no | Maximum simultaneous deliveries (default 1, minimum 1) |
visibility_timeout_secs |
no | How long a delivery may take before it is retried (default 30) |
The handler_url must be an http or https URL on your app's domain (a domain name, with no
credentials and no # fragment), among the callback URLs the platform has authorised for your
app. The platform waits for your handler's response for up to
visibility_timeout_secs, with a floor of 30 seconds and a ceiling of 15 minutes: set it above
your handler's longest run, or a slow but successful delivery is retried.
Handler contract
The platform POSTs this body to your handler_url:
{ "id": "<message-id>", "queue": "emails", "payload": { }, "attempts": 0 }
attempts counts the previous deliveries: 0 on the first one. Every delivery counts, including
ones that time out.
With sdk.queue.push, payload arrives as a JSON string (the payload is serialised before it
is sent): decode it with JSON.parse, as in the example below.
Your handler's response status decides what happens next:
| Response | Effect |
|---|---|
2xx |
ack: the message is done |
408, 429, 5xx |
retry: redelivered after the Retry-After delay you send (whole seconds, at least 1), 30 seconds by default, capped at visibility_timeout_secs |
| any other status | dead-letter: the message moves to the dead queue |
| no response (unreachable, timeout) | retry once visibility_timeout_secs has elapsed |
A message is dead-lettered after 5 delivery attempts. A Retry-After given as an HTTP date is
not honoured: the default delay applies.
Write the handler as a route.ts:
export async function POST(request: Request) {
const { id, payload, attempts } = await request.json();
const data = typeof payload === "string" ? JSON.parse(payload) : payload;
try {
await sendEmail(data);
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, for example by deduplicating on
id. - Fire-and-forget: enqueuing returns an
idand nothing else. There is no built-in way to ask whether message X succeeded. - Dead-letter: messages that exhaust their attempts, or whose handler returns a non-retryable status, land in the dead queue. They stay there with their payload and can be listed, requeued (all at once or one by one, with the attempt count reset to zero) or purged.
- Untrusted input: the handler is a route of your app like any other. Treat the body it receives as external input and, before an irreversible action, re-check the state you stored rather than relying on the payload alone.
Add a job record when you need status
Because the queue stores no result, an app that needs outcomes keeps a durable job record in
KV: keys like job:<id> that the handler updates
(queued → running → complete / fail).
Queue or tasks
For long-running work with built-in status and progress, use Tasks instead. The queue suits high-volume, fire-and-forget delivery to an HTTP handler.
Where to next
| Page | What it covers |
|---|---|
| Tasks & Scheduler | Long jobs with status and progress, scheduling |
| KV Store | The durable job-record pattern |
| Queue Workers | Operating and observing handlers |