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.

Note

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 id and 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