Changelog#
This release focuses on controlling jobs with bounded shutdowns, per-job timeouts, and safer acks when jobs are rescued and run again on another node.
Job Timeouts#
Jobs could previously run indefinitely, so a hung network call or runaway loop could hold a queue
slot forever. Workers can now limit how long a job runs by defining a timeout method that
returns the limit in seconds. It receives the job, so the limit can vary by args or attempt:
@worker(queue="default")
class ExportWorker:
async def process(self, job):
await export_report(job.args["report_id"])
def timeout(self, job):
# Allow more time on each attempt: 60s, 120s, 180s...
return 60 * job.attempt
Jobs that exceed their timeout fail with a JobTimeoutError and retry with the usual backoff.
Cancellation is cooperative, so a timed out job stops at its next await.
Shutdown Grace Period#
Stopping Oban or a queue previously waited indefinitely for running jobs, so a slow or hung job could stall a deploy until the process was killed. Running jobs now have a shutdown grace period, 15 seconds by default. Jobs still running afterwards are cancelled and left for the lifeline to rescue.
[oban]
queues = {default = 10}
shutdown_grace_period = 30
Each queue emits an oban.queue.shutdown telemetry event as it stops, including the ids of any
orphaned jobs, which the telemetry logger reports as a warning. Set shutdown_grace_period to
None to restore the previous behavior of waiting indefinitely.
Current Job Context#
Function workers defined with @job are concise, but they only receive the job’s args, not the
job itself. The new oban.current_job() function returns the job executing in the current
context, so function workers can read details like the attempt, id, or meta without converting to
a class worker:
from oban import current_job, job
@job(queue="billing", max_attempts=5)
async def charge_invoice(invoice_id: int):
job = current_job()
# Use the job id as an idempotency key so retries never double charge
await payments.charge(invoice_id, idempotency_key=f"oban-{job.id}")
if job.attempt > 1:
logger.warning("Retried invoice %s, attempt %s", invoice_id, job.attempt)
Because it’s backed by a context variable, current_job() also works in any helper called from
the job, without threading the job through as an argument. The job is set for the entire
execution, so it’s available within oban.job.start, oban.job.stop, and oban.job.exception
telemetry handlers and result wrapping extensions as well. Outside of job execution it returns
None.
v0.7.0 — 2026-10-22#
Enhancements#
[Config] Extend to provide the notifier from config
A new
notifiersetting, from a[notifier]table inoban.tomlorOBAN_NOTIFIER, is passed to thenotifier.buildextension hook along with the prefix and connection pool. Extensions can use it to replace the default PostgreSQL notifier without application code changes. Without an extension it’s ignored, and the default notifier is used.Fixes dict settings from the environment or CLI being dropped when
oban.tomldidn’t also set them.[Job] Rename
schedule_intoscheduled_infor relative schedulingUsing
scheduled_inis now the correct way to schedule a job relatively, matching thescheduled_atfield it sets. Theschedule_inoption is still accepted as an alias, and will be removed in 1.0.Testing helpers also accept
scheduled_in, matching jobs scheduled that far from now within a second, so assertions don’t need to compute an exactscheduled_at.[Notifier] Build notifier from a config dict
Oban now accepts
notifieras a dict, like the other component options, and builds the notifier itself with the instance’s connection pool and prefix.[Oban] Build background components through extensions
All looping services are constructed through
<name>.buildextensions, matching the existingnotifier.build.[Oban] Expose
current_joband retain through telemetryAdd the
oban.current_job()function to fetch the job executing in the current context. This is primarily for function workers that don’t receive the job as an argument.The current job is also set for the entire execution, rather than only while the worker runs, so it’s available within
oban.job.start,oban.job.stop, andoban.job.exceptiontelemetry handlers, as well as result wrapping extensions.[Producer] Cancel jobs running after shutdown grace period
Stopping Oban or a queue previously waited indefinitely for running jobs, so a slow or hung job could stall shutdown until the process was killed.
Running jobs now have a
shutdown_grace_periodto finish, 15 seconds by default. Jobs still running afterwards are cancelled at their next await and left for the lifeline to rescue. Setshutdown_grace_periodtoNoneto restore the previous behavior of waiting indefinitely.Each queue emits an
oban.queue.shutdowntelemetry event as it stops, including the ids of any orphaned jobs, which the telemetry logger reports as a warning.[Telemetry] Publish canonical telemetry event names
Add
oban.telemetry.EVENTS, listing every emitted event, so integrations can validate the names they attach to. Attaching to an event that’s never emitted otherwise fails silently.The telemetry logger now correctly logs the actual
oban.producer.get.*andoban.producer.ack.*events as well.[Testing] Include suspended jobs in testing assertions
The
all_enqueued,assert_enqueued, andrefute_enqueuedhelpers now include thesuspendedstate. This makes it possible to assert on jobs that are enqueued but waiting on something else, e.g. workflow deps.[Worker] Add job timeouts through a worker method
Workers may define
timeout(self, job)to limit how long a job runs, in seconds. Jobs exceeding their timeout are cancelled at the next await and fail with aJobTimeoutError, then retry with the usual backoff. Timeouts raised by the job itself are left untouched.
Bug Fixes#
[Cron] Validate cron range bounds before expanding them
Cron expressions with ranges far outside a field’s bounds, such as
0-99999999 * * * *, are now rejected immediately. Previously the full range was built in memory before validation, which could stall startup or exhaust memory.[Leader] Release leadership before notifying on shutdown
Peers could react to the resign notification before the lease was released, lose the election, and wait a full interval before taking over.
[Oban] Broadcast
start_queue/stop_queueacross all nodesCalling
start_queueorstop_queuewithout a node only changed the local node, despite being documented to act on all nodes. Both now act locally and broadcast, matchingscale_queue.[Producer] Prevent stale acks from overwriting newer executions
Acking a job now also requires its
attempted_atto match the execution being acked. Previously, a job that was rescued and fetched again by another node could have its newer execution completed, retried, or snoozed by a late ack from the original run.[Producer] Fix shutdown crash when jobs finish while draining
Stopping a queue could raise “Set of Tasks/Futures is empty” if the last running jobs completed while the producer was recording its shutdown state.
[Refresher] Restore producers missing after a database reset
Producer records live in an unlogged table, which Postgres empties after a crash restart or failover. Previously the refresher only updated existing rows, so producers stayed missing until their queue restarted, and the Pro lifeline repeatedly rescued jobs that were still running. The refresher now re-inserts any missing producer records and reports a
restored_countin its telemetry.[Stager] Keep notifying queues when staging jobs fails
The stager notifies every local queue when there is a staging error such as a unique conflict. Previously, queues weren’t notified after a failed stage, and a queue with no running jobs could leave available jobs unprocessed.