Aurabase Logo
aurabasedocs
docs›Edge Functions›Jobs, cron and the DLQ

Jobs, cron and the dead-letter queue

How to run work outside a request: what a job is, how it is retried, how cron creates jobs, and what to do with the jobs that end up dead.

9 min read·Level intermediate·Revised on Oct 12, 2026
This English text was generated automatically from the French original and has not been reviewed yet.
#
Enqueue

1. Enqueue a job

A job is one background call to a function. Enqueue it from a server with a service_role key.

server/enqueue.tstypescript
import { createClient } from '@aurabase/aurabase-js'

const admin = createClient(process.env.AURA_URL!, process.env.AURA_SERVICE_ROLE_KEY!)

const { data: job, error } = await admin.functions.enqueueJob('send-report', {
  payload: { month: '2026-10' },
  delaySecs: 60,   // wait 60 seconds before the first attempt
  maxAttempts: 3,  // the default
})
OptionDefaultMeaning
payloadnullJSON given to the function as the request body
delaySecs0Seconds to wait before the first attempt. The job stays pending until then.
maxAttempts3Total number of attempts, the first included

You read a job back with getJob(id) or listJobs({ status, limit }) (limit is between 1 and 500, default 50). The job has a status, attempts, max_attempts, error and the dates scheduled_at, started_at, completed_at.

Jobs are a server-side tool

enqueueJob, getJob and listJobs need a service_role key. A public key receives 403.

#
Handler

2. What your function receives

A job calls your function like any other request, with three differences you must handle:

PartValue
MethodJOB (not POST)
Path/jobs/<job id>
BodyThe job's payload, as JSON
Identity headersNone: x-aura-role and x-aura-user-id are absent
aurabase/functions/send-report/index.tstypescript
Deno.serve(async (req) => {
  const jobId = new URL(req.url).pathname.split('/').pop()
  const { month } = await req.json()
  // …build and send the report, using jobId as an idempotency key
  return new Response('ok')
})

A real call logged {"method":"JOB","path":"/jobs/d443d541-…","role":null,"userId":null,"body":"{\"items\":[\"a\",\"b\"],\"order\":42}"}.

A job runs without a user: do not rely on x-aura-user-id. If the work belongs to a user, put the user's id in the payload.

#
Retries

3. Lifecycle and retries

statusestext
pending ──claimed──▶ running ──▶ completed
   ▲                    │
   └── after 5 s ◀── failed (attempts left)
                        └──▶ dead (no attempts left)
  • A job runs at least once. It can run twice if a worker stops while it is running, so make the function idempotent.
  • A failed attempt is retried after a fixed delay of 5 seconds, until maxAttempts is reached. The job is then dead. The delay and the default attempt count are service settings (JOB_RETRY_DELAY_SECS, JOB_MAX_RETRIES).
  • A job that is still running after its lease (450 seconds by default) is reclaimed by the service.
  • Finished jobs are kept for 14 days by default.
An HTTP error from your function is not a failure

A job is completed as soon as your function answers, whatever the status code. In a real run, a function that returned 500 produced a completed job after one attempt and was never retried.

A job fails and is retried only when the call itself fails: the function does not exist or is inactive, the runtime is unreachable, or the call times out. If you want a retry when your own code detects an error, enqueue a new job from the handler (with delaySecs for a back-off) instead of relying on the status code.

In the same real run, a job for a function that does not exist went through this sequence: attempt 1 failed at 3 s with Fonction 'ghost' introuvable, attempt 2 failed at about 9 s, and the job became dead with attempts equal to max_attempts.

#
Recovery

4. The dead-letter queue

Dead jobs stay in the queue and can be listed and retried. These routes have no SDK method yet: use HTTP with a service_role key.

terminalbash
# list dead jobs
curl "$AURA_URL/v1/functions/jobs/dlq" -H "apikey: $AURA_SERVICE_ROLE_KEY"

# retry one job
curl -X POST "$AURA_URL/v1/functions/jobs/$JOB_ID/retry" -H "apikey: $AURA_SERVICE_ROLE_KEY"

# retry every dead job (up to 1000)
curl -X POST "$AURA_URL/v1/functions/jobs/dlq/requeue" -H "apikey: $AURA_SERVICE_ROLE_KEY"
# → {"data":{"requeued":1},"meta":null}

A retry puts the job back to pending without resetting its attempt counter, so it gets exactly one more attempt and returns to dead if it fails again. Fix the cause (deploy the missing function, for example) before you retry.

#
Schedule

5. Schedule with cron

A cron entry does not call the function itself: each time it fires, it enqueues a job.

server/cron.tstypescript
await admin.functions.createCronJob('nightly-cleanup', '0 3 * * *', 'cleanup', {
  payload: { dryRun: false },
})

const { data: crons } = await admin.functions.listCronJobs()
await admin.functions.deleteCronJob('nightly-cleanup') // by NAME, not by id
  • The schedule has 5 fields (min hour day month weekday) or 6 fields (sec min hour day month weekday). A 5-field schedule gets 0 seconds in front: */1 * * * * is stored as 0 */1 * * * *.
  • Aliases such as @daily are refused with a 400 that states the expected format.
  • Times are in UTC.
  • last_run and next_run are null until the first firing. In a real run, a schedule created at 15:40 fired at 15:41:00 and then showed next_run 15:42:00.
  • The payload is frozen when you create the entry.
  • Deleting an unknown name returns 404. Deleting a cron entry also stops it from firing.
  • The SDK has no getCronJob or updateCronJob: delete the entry and create it again.

For purely database work, the Postgres pg_cron extension avoids the round trip to a function: see the Cron page.

Last updated · Oct 12, 2026