Powertools limits are durable Postgres-backed reservations. They are for “this mutation should not even queue unless capacity exists” cases, not just best-effort runtime throttling.
Declare the limiter on the worker
defmodule MyApp.Sync.SyncCustomerWorker do
use ObanPowertools.Worker,
queue: :sync,
args: [account_id: :integer, cost: :integer],
limits: [
name: "customer-sync-api",
scope: :partitioned,
partition_by: {:args, :account_id},
weight_by: {:args, :cost},
bucket_capacity: 10,
bucket_span_ms: 60_000
]
@impl true
def process(_job), do: :ok
endSupported shapes today:
scope: :globalfor one shared bucketscope: :partitionedfor a bucket per resolved partition keypartition_byfrom{:args, :field}or a worker/module resolverweight_byfrom{:args, :field}or a worker/module resolver
What enqueue does when capacity is gone
assert {:ok, _job} = MyApp.Sync.SyncCustomerWorker.enqueue(%{account_id: 42, cost: 7})
assert {:ok, _job} = MyApp.Sync.SyncCustomerWorker.enqueue(%{account_id: 42, cost: 3})
assert {:blocked, [%{code: "limit_reached"}]} =
MyApp.Sync.SyncCustomerWorker.enqueue(%{account_id: 42, cost: 1})A blocked enqueue is not silent:
- the limiter state stays queryable in Postgres
- blocker snapshots are persisted
- normalized audit evidence is written for blocked outcomes
Explain why something is blocked
explanation =
ObanPowertools.Explain.explain(
MyApp.Sync.SyncCustomerWorker,
%{account_id: 42, cost: 1},
repo: MyApp.Repo
)
assert explanation.status == :blocked
assert [%{code: "limit_reached"}] = explanation.blockersExplain.explain/3 gives you:
- one shared operator status such as
Blocked,Waiting, orRunnable live_nowblockers from current limiter statesnapshot_at_block_startfrom the persisted blocker snapshot- one stable shape for rendering diagnosis-first surfaces with venue and evidence
Administrative limiter actions
Use the lower-level ObanPowertools.Limits API when operator flows need explicit capacity
control:
{:ok, reservation} =
ObanPowertools.Limits.reserve(
MyApp.Repo,
MyApp.Sync.SyncCustomerWorker,
%{account_id: 42, cost: 2}
)
:ok = ObanPowertools.Limits.release(MyApp.Repo, reservation) |> then(fn {:ok, _} -> :ok end)You can also put a resource into cooldown:
ObanPowertools.Limits.cooldown(
MyApp.Repo,
"customer-sync-api",
"42",
DateTime.add(DateTime.utc_now(), 30, :second),
"429 backoff"
)When this is the right tool
Use limits when the business rule is “do not queue work beyond this budget.” If the real need is only “run slower once jobs are already in flight,” plain Oban queue controls may be enough.
Rate-Limit Glossary
token_bucket — The rate-limiting algorithm used by ObanPowertools limiters. Each
partition has a bucket of bucket_capacity tokens. Each reservation consumes weight
tokens. The bucket refills (resets to zero tokens used) after bucket_span_ms
milliseconds have elapsed since bucket_started_at.
bucket_capacity — The maximum number of tokens available per bucket window. A
reservation that would bring tokens_used + weight above this value is blocked with
the limit_reached blocker code.
bucket_span_ms — The duration of one bucket window in milliseconds. After this
interval elapses since bucket_started_at, the bucket resets and tokens are available
again. Used to compute retry_at for a limit_reached block.
weight — The per-reservation token cost. Defaults to the resource's
default_weight (usually 1). Each successful reservation consumes weight tokens from
the bucket.
weight_by — A dynamic weight resolver declared on the worker (e.g.
weight_by: {:args, :cost}). At enqueue time the resolved value is bound to the
reservation snapshot as the effective weight.
partition — A named isolation group within a limiter resource. Each partition
maintains its own independent token bucket. For scope: :global limiters there is
one partition (__global__); for scope: :partitioned there is one bucket per
resolved partition_key.
partition_by — A dynamic partition key resolver declared on the worker (e.g.
partition_by: {:args, :user_id}). At enqueue time the resolved value becomes the
partition_key used to look up the correct bucket.
scope — The partitioning strategy for a limiter resource. global means one
shared bucket across all callers. partitioned means one independent bucket per
resolved partition_key, enabling per-user, per-account, or per-tenant limits.
cooldown — An operator-set hold on a partition until a specific DateTime. While
a cooldown is active, all reservations for that partition are blocked with the
cooldown blocker code regardless of remaining bucket capacity. Useful for
propagating backpressure signals (e.g. HTTP 429 responses) into the limiter.
limit_reached — Blocker code returned when tokens_used + weight > bucket_capacity.
The retry_at field indicates when the bucket will reset
(bucket_started_at + bucket_span_ms, clamped to at least now).
cooldown (blocker code) — Blocker code returned when a resource partition is under
an active operator cooldown. The retry_at field is cooldown_until — the
DateTime at which the cooldown expires and reservations are permitted again.