Skip to content

Ack mode

A queue in ack mode acknowledges the webhook delivery immediately and waits for your worker to report the real outcome out of band. Use it for work that might exceed the 15-second webhook ceiling — most LLM calls, anything synchronous that touches a slow downstream — and for work where you want to relay backpressure (defer) or signal a hard failure (nack) cleanly.

Why it exists

SimpleQ enforces a 15-second timeout on every webhook request. A standard-mode webhook that takes longer is aborted and treated as a failure. That's the right default for fast handlers — but it rules out anything that routinely runs longer: a 40-second Claude generation, a video transcode, a slow third-party API.

Ack mode splits the two concerns: the webhook handler returns 2xx the moment it has the job in hand (well under the 15s ceiling), and the worker reports the real outcome later through one of three callbacks.

The three outcomes

CallbackWhen to useEffect
POST /v1/jobs/:id/ackThe work succeeded.Job → completed.
POST /v1/jobs/:id/nack { retryable }The work failed.retryable: true → retry with backoff (counts against maxAttempts). retryable: false → straight to the DLQ.
POST /v1/jobs/:id/defer { retryAfter }A downstream rate-limited you.Job is held and redelivered after retryAfter seconds. No attempt is burned. See backpressure.

All three require a queue with mode: 'ack'. Authenticate with your API key (Authorization: Bearer sq_live_...).

Minimal worker shape

js
app.post('/webhook', async (req, res) => {
  // verifySignature → see https://docs.simpleq.io/concepts/signature-verification
  if (!verifySignature(req.body, req.headers['x-simpleq-signature'])) {
    return res.status(401).end();
  }

  // Ack mode: 200 immediately, then run the work out of band.
  res.status(200).end();

  const job = JSON.parse(req.body.toString('utf8'));
  try {
    await doTheWork(job.payload);
    await callback(job.id, 'ack');                                    // → POST /v1/jobs/:id/ack
  } catch (err) {
    await callback(job.id, 'nack', { retryable: isTransient(err) }); // → POST /v1/jobs/:id/nack
  }
  // For downstream backpressure (429/503/529), see https://docs.simpleq.io/concepts/backpressure
  // → callback(job.id, 'defer', { retryAfter })
});
python
@app.post("/webhook")
async def webhook(
    request: Request,
    background_tasks: BackgroundTasks,
    x_simpleq_signature: str | None = Header(default=None),
):
    # verify_signature → see https://docs.simpleq.io/concepts/signature-verification
    raw = await request.body()  # raw bytes, BEFORE parsing
    if not verify_signature(raw, x_simpleq_signature):
        return Response(status_code=401)

    # Ack mode: 200 immediately, then run the work out of band.
    job = json.loads(raw)
    # BackgroundTasks runs in-process — if this worker crashes mid-job the
    # work is lost, but ackTimeout + ackTimeoutAction: "retry" redelivers it.
    background_tasks.add_task(process_job, job)
    return Response(status_code=200)


async def process_job(job: dict) -> None:
    try:
        await do_the_work(job["payload"])
        await callback(job["id"], "ack")                                 # → POST /v1/jobs/:id/ack
    except Exception as err:
        await callback(job["id"], "nack", {"retryable": is_transient(err)})  # → POST /v1/jobs/:id/nack
    # For downstream backpressure (429/503/529), see https://docs.simpleq.io/concepts/backpressure
    # → callback(job["id"], "defer", {"retryAfter": ...})

A complete handler — including the raw-body access needed to preserve the body for signature verification, plus verifySignature and callback defined — lives in the generic ack worker example.

When to use ack mode vs standard mode

Use standard whenUse ack when
The handler finishes in well under 15s.The handler routinely runs longer than 15s.
You want SimpleQ to mark the job done the moment your webhook returns 2xx.You need to report success / failure / backpressure separately from "I received it."
The work is cheap to repeat on retry.The work is expensive (LLM tokens, downstream quota) and you want explicit control over what gets retried.

If you're unsure, start with standard. Switch to ack the first time you find yourself wishing you could say "I got it, give me a minute" or "the downstream just rate-limited me."

Staying resilient across redeliveries

A worker that succeeds (spends tokens, writes results) then dies before sending /ack will be redelivered after ackTimeout. SimpleQ guarantees durable delivery, and we make it easy to dedupe redeliveries so your worker stays resilient — see idempotency for the pattern.