Queue Workers

The SDK queue delivers each message at least once. You add messages to a queue (see SDK → Queue) and a worker processes them: an HTTP handler in your app that the platform POSTs each message to. Each app only sees its own queues and its own workers.

Worker settings

A worker is defined for a queue by the settings below. The framework client has no call to register a worker: HTTP workers are enabled for your app by the platform.

Field Required Notes
queue yes Name of the queue to process
handler_url yes The handler URL, on your app's domain
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)

A queue has one worker: defining a new one replaces the previous one. Workers can also be listed and removed.

A message is dead-lettered after 5 deliveries without an ack.

Note

handler_url must be an http or https URL with a domain name (no IP address, no credentials, no # fragment), under one of the callback URLs the platform has allowed for your app. A malformed URL is refused with 400; if HTTP workers are not enabled for the app, or the URL is not allowed, the refusal is a 403. If the authorization is withdrawn later, deliveries are paused without using up attempts.

Example settings:

{
  "queue": "emails",
  "handler_url": "https://<app>.inklura.fr/api/queue/emails",
  "concurrency": 4,
  "visibility_timeout_secs": 60
}

The handler contract

On each delivery, the platform POSTs this JSON envelope to your handler_url:

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

payload carries the value passed to push. With sdk.queue.push it arrives as a JSON string: decode it with JSON.parse, as in the example below. attempts counts previous deliveries: 0 on the first one. Messages in a queue are picked up in arrival order, but with a concurrency above 1 several deliveries run in parallel.

Your response status decides what happens next:

Response status Outcome
2xx Ack: the message is done and removed
408, 429, 5xx Retry: the message is redelivered later (up to 5 deliveries)
any other status Dead-letter: the message moves to the dead-letter queue, no retry
no response (unreachable, timed out) Retry once visibility_timeout_secs has passed

On a retry status, a Retry-After header (a whole number of seconds) sets the delay before redelivery; without it the delay is 30 seconds. Either way the delay is capped at the visibility timeout. Your handler has visibility_timeout_secs to answer (at least 30 seconds, at most 900): set it above your handler's longest run time, otherwise a slow but successful delivery is retried.

// src/app/api/queue/emails/route.ts
export async function POST(req: Request) {
  const { id, payload, attempts } = await req.json();
  const data = typeof payload === "string" ? JSON.parse(payload) : payload;
  try {
    await sendEmail(data);
    return new Response("ok");                 // 2xx → ack
  } catch (e) {
    if (isTransient(e)) {
      return new Response("retry", {           // 5xx → retry in 60 s
        status: 503,
        headers: { "Retry-After": "60" },
      });
    }
    return new Response("bad payload", { status: 400 }); // 4xx → dead-letter
  }
}
Warning

Delivery is at least once. The same message can arrive more than once (for example if your 2xx response was lost, or after a visibility timeout). Make your handlers idempotent by keying side effects on the message id.

Tracking a job: add a record in KV

The queue stores no result. Once a message is acknowledged it is gone: nothing tells you afterwards whether the job succeeded or what it produced. If you need a status or a result, write your own record, ideally in KV under a job:<id> key.

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

const sdk = createSdk("<app-id>");

// when enqueuing (push returns the message id)
const id = await sdk.queue.push("emails", payload);
await sdk.kv.set(`job:${id}`, { status: "queued", createdAt: Date.now() });

// in the handler, id comes from the envelope
await sdk.kv.set(`job:${id}`, { status: "running" });
// …work…
await sdk.kv.set(`job:${id}`, { status: "done", result });

kv.set replaces the whole value: carry over the fields you want to keep on each write.

If you need built-in status and progress, use tasks instead.

Dead letters

Messages that use up their 5 deliveries, or for which the handler returned a non-retryable status, land in the dead-letter queue. They stay there, with their payload, until you retry or purge them:

Operation Purpose
List Inspect dead-lettered messages (50 by default, at most 500) with their payload and attempt count
Retry Put the messages back in the queue with the attempt count reset to zero (a whole queue, or one message by id)
Purge Delete the messages permanently (a whole queue, or one message by id)

Related