Concurrency & Rate Limiting
Note
This feature requires a Pro license on the server.
Applications enqueue jobs to offload expensive work to a background worker. Some of those jobs may put pressure on upstream systems if running with high throughput. For example, you may have jobs that resize images through an image service, and that service may have a maximum throughput of 10,000 requests per hour. Or you may send push notifications through your queue worker, but at times bursts of notifications dominate your queue, starving other jobs of processing time. Both of these problems can be solved with a feature Zizq calls budgets.
Budgets are Zizq’s approach to limiting throughput, both from a pure concurrency
control perspective (no more than N jobs in-flight at any given time), and also
from a rate limiting perspective (no more than N jobs dispatched over a period
of time). The server stores named budgets, which are pools of available tokens
managed under a specified strategy. Currently two strategies exist:
while_in_flight, and time_based. Jobs are enqueued referencing one or more
of these budgets, along with optional costs to run those jobs, where that cost
defaults to 1 token. Before a job can be dispatched to a worker, it needs to
successfully debit its cost from each of its budgets’ shared token pools.
Unlike with some other job queues, throughput is controlled entirely by the server, so workers do not need to receive a job and then wait or retry because it exceeded some rate limit. Instead, workers remain naive to the dispatching logic and just process every job they receive in the same way. If the budget does not have enough tokens available for a job to run, Zizq does not dispatch that job to a worker in the first place. It parks that job and dispatches it the moment its budget allows. Other jobs — those without budget restrictions and those on other budgets that have tokens available — continue to be dispatched to workers without stalling.
Note
Budgets are a shared resource. The Zizq server currently enforces an upper limit on the number of distinct budgets that can be created. The default is
8192different budgets, which should be far in excess of what typical applications would require, but this limit can be configured on the server via--max-budgets($ZIZQ_MAX_BUDGETS) when runningzizq serve. A future release will introduce a sub-bucket concept for dynamically allocated budget scenarios.
Budget Strategies
There are currently two available strategies for budgeting: :while_in_flight
implements pure concurrency control, and :time_based implements a dispatch
rate limit over time. Both take a total allocation value, which is the number
of tokens made available in the budget’s pool. Jobs can be bound to more than
one budget, mixing and matching across different strategies. In this case,
all budgets must be satisified before the job can run.
Budgets must have an allocation greater than or equal to the cost of each
job that references that budget. That is, the Zizq server will reject any
attempt to enqueue a job that costs more than its budget will ever allow, and
it will reject any attempt to update a budget to allocate less tokens than some
job referencing that budget would cost.
:while_in_flight
For pure concurrency control, where you need to ensure at most N workers are
processing a given job at once, use the :while_in_flight strategy. In this
strategy, the budget has a given token allocation of say 20 and no other
configuration.
Ruby:
Zizq.define_budget( "cpu-intensive", allocation: 20, strategy: { type: :while_in_flight } )
The above budget would allow at most 20 concurrent jobs with the default cost
of 1, or 10 concurrent jobs with a cost of 2, or any valid combination of
costs that is less than or equal to 20.
20 x cost=110 x cost=2(5 x cost=2) + (10 x cost=1)(3 x cost=5) + (2 x cost=2)
When a job is dispatched that uses a :while_in_flight strategy, it must debit
its cost in full from the budget’s token pool. If the pool is too depleted to
do that, the job remains parked until more tokens are available in the pool.
Once dispatched, tokens remain debited from the pool for as long as that job is
"in_flight". As soon as the worker acknowledges the job with a successful
completion, or reports a failure, the job is no longer "in_flight" and its
tokens are released back to the pool, allowing other jobs to run within that
same budget.
To illustrate how this works, take this example job that takes a consistent 2
seconds to run, bound to a "while_in_flight" budget with an allocation of 5.
2 second jobs with
:while_in_flight5:D, [2026-09-10T14:21:20.926911 #3954337] DEBUG -- : Received example (03guad5408l32mar6g23tnuql), dispatch queue: 0 D, [2026-09-10T14:21:20.927188 #3954337] DEBUG -- : Received example (03guad540ozoaxl3fazxngdpv), dispatch queue: 1 D, [2026-09-10T14:21:20.927403 #3954337] DEBUG -- : Received example (03guad540zxegggplf64q2td9), dispatch queue: 2 D, [2026-09-10T14:21:20.927601 #3954337] DEBUG -- : Received example (03guad5415e9j83oqlgggdu3i), dispatch queue: 3 D, [2026-09-10T14:21:20.927802 #3954337] DEBUG -- : Received example (03guad541gbzorp4uvufmeez8), dispatch queue: 4 D, [2026-09-10T14:21:22.930215 #3954337] DEBUG -- : Job example (03guad5408l32mar6g23tnuql) completed in 2.0021s D, [2026-09-10T14:21:22.930456 #3954337] DEBUG -- : Job example (03guad540ozoaxl3fazxngdpv) completed in 2.0024s D, [2026-09-10T14:21:22.930512 #3954337] DEBUG -- : Job example (03guad540zxegggplf64q2td9) completed in 2.0024s D, [2026-09-10T14:21:22.930630 #3954337] DEBUG -- : Job example (03guad5415e9j83oqlgggdu3i) completed in 2.0025s D, [2026-09-10T14:21:22.930673 #3954337] DEBUG -- : Job example (03guad541gbzorp4uvufmeez8) completed in 2.0026s D, [2026-09-10T14:21:22.933266 #3954337] DEBUG -- : Received example (03guad541lsurizqiqrs1swx5), dispatch queue: 0 D, [2026-09-10T14:21:22.933619 #3954337] DEBUG -- : Received example (03guad541r9puax48rtp1pghx), dispatch queue: 1 D, [2026-09-10T14:21:22.933837 #3954337] DEBUG -- : Received example (03guad541wqkx29mjfw85w94h), dispatch queue: 2 D, [2026-09-10T14:21:22.933969 #3954337] DEBUG -- : Received example (03guad54227fzu07za3g0truo), dispatch queue: 3 D, [2026-09-10T14:21:22.934199 #3954337] DEBUG -- : Received example (03guad542d565dgd2794uhl9b), dispatch queue: 4 D, [2026-09-10T14:21:24.936925 #3954337] DEBUG -- : Job example (03guad541lsurizqiqrs1swx5) completed in 2.0021s D, [2026-09-10T14:21:24.937260 #3954337] DEBUG -- : Job example (03guad541r9puax48rtp1pghx) completed in 2.0024s D, [2026-09-10T14:21:24.937317 #3954337] DEBUG -- : Job example (03guad541wqkx29mjfw85w94h) completed in 2.0025s D, [2026-09-10T14:21:24.937483 #3954337] DEBUG -- : Job example (03guad54227fzu07za3g0truo) completed in 2.0026s D, [2026-09-10T14:21:24.937524 #3954337] DEBUG -- : Job example (03guad542d565dgd2794uhl9b) completed in 2.0027s D, [2026-09-10T14:21:24.939414 #3954337] DEBUG -- : Received example (03guad542im184k4yuvstxmkh), dispatch queue: 0 D, [2026-09-10T14:21:24.939531 #3954337] DEBUG -- : Received example (03guad542o2waw80tumu5kwes), dispatch queue: 1 D, [2026-09-10T14:21:24.939584 #3954337] DEBUG -- : Received example (03guad542tjrdnxu6xdfosu1s), dispatch queue: 2 D, [2026-09-10T14:21:24.939623 #3954337] DEBUG -- : Received example (03guad542z0mgfx5vmj1vhfnq), dispatch queue: 3 D, [2026-09-10T14:21:24.939658 #3954337] DEBUG -- : Received example (03guad5434hhj72czyv8jhnc9), dispatch queue: 4 D, [2026-09-10T14:21:26.941879 #3954337] DEBUG -- : Job example (03guad542im184k4yuvstxmkh) completed in 2.0021s D, [2026-09-10T14:21:26.942010 #3954337] DEBUG -- : Job example (03guad542o2waw80tumu5kwes) completed in 2.0022s D, [2026-09-10T14:21:26.942067 #3954337] DEBUG -- : Job example (03guad542tjrdnxu6xdfosu1s) completed in 2.0023s D, [2026-09-10T14:21:26.942116 #3954337] DEBUG -- : Job example (03guad542z0mgfx5vmj1vhfnq) completed in 2.0023s D, [2026-09-10T14:21:26.942170 #3954337] DEBUG -- : Job example (03guad5434hhj72czyv8jhnc9) completed in 2.0024s D, [2026-09-10T14:21:26.944265 #3954337] DEBUG -- : Received example (03guad5439yclyzloygnro4w1), dispatch queue: 0 D, [2026-09-10T14:21:26.944686 #3954337] DEBUG -- : Received example (03guad543ff7oqjtumn1ix8kn), dispatch queue: 1 D, [2026-09-10T14:21:26.945099 #3954337] DEBUG -- : Received example (03guad543qcxu9jmvg5gxpr32), dispatch queue: 2 D, [2026-09-10T14:21:26.945199 #3954337] DEBUG -- : Received example (03guad543vtsx13snzic6s7x0), dispatch queue: 3 D, [2026-09-10T14:21:26.945392 #3954337] DEBUG -- : Received example (03guad5441anzt166s2yn805o), dispatch queue: 4 D, [2026-09-10T14:21:28.947714 #3954337] DEBUG -- : Job example (03guad5439yclyzloygnro4w1) completed in 2.0021s D, [2026-09-10T14:21:28.947848 #3954337] DEBUG -- : Job example (03guad543ff7oqjtumn1ix8kn) completed in 2.0022s D, [2026-09-10T14:21:28.947904 #3954337] DEBUG -- : Job example (03guad543qcxu9jmvg5gxpr32) completed in 2.0023s D, [2026-09-10T14:21:28.947962 #3954337] DEBUG -- : Job example (03guad543vtsx13snzic6s7x0) completed in 2.0023s D, [2026-09-10T14:21:28.948016 #3954337] DEBUG -- : Job example (03guad5441anzt166s2yn805o) completed in 2.0024s D, [2026-09-10T14:21:28.950214 #3954337] DEBUG -- : Received example (03guad5446rj2kqag18apd9vd), dispatch queue: 0 D, [2026-09-10T14:21:28.950899 #3954337] DEBUG -- : Received example (03guad544c8e5c5rqlw2jjpnw), dispatch queue: 1 D, [2026-09-10T14:21:28.951521 #3954337] DEBUG -- : Received example (03guad544hp983tq2svu2wz8b), dispatch queue: 2 D, [2026-09-10T14:21:28.951895 #3954337] DEBUG -- : Received example (03guad544smzdmtscfpzsfms7), dispatch queue: 3 D, [2026-09-10T14:21:28.952252 #3954337] DEBUG -- : Received example (03guad544y3ugexkkord5fy4d), dispatch queue: 4
Jobs are running 5 at once, taking 2 seconds to complete and then another 5 jobs run at once.
:time_based
Where concurrency is not the concern, but overall throughput is, the
:time_based strategy can be used to enforce a rate limit. Just like a
:while_in_flight budget, a :time_based one has a token allocation, which
represents the number of tokens that can be spent over some period of time.
Unlike the :while_in_flight strategy, a :time_based strategy is specified
along with a duration, specifying the number of seconds (or if you have
ActiveSupport, an ActiveSupport::Duration like 5.minutes) over which
its allocation can be spent.
Ruby:
Zizq.define_budget( "image-service", allocation: 10000, strategy: { type: :time_based, duration: 3600 } )
The above budget states that at most 10,000 tokens can be spent over a
1 hour period, hence it is a rate limit of 10000/hour.
When a job is dispatched that uses a :time_based budget, it must debit its
cost in full, otherwise it remains parked until enough tokens become available.
Unlike :while_in_flight, tokens are not released back to the pool when the
job completes, but rather are released back to the pool on the cadence
specified by the duration. This means a :time_based budget controls how many
jobs are dispatched over time, but it cares not how many of those jobs finish
up running at once (i.e. jobs that take longer than duration to run may
overlap). The server implements this lazily. There is no constant scanning of
the database to look for jobs that can now be dispatched. The server is smart
enough to know when tokens will next become available and sleeps until that
time, or until some other event wakes it.
The :time_based strategy implements a continuous drip rate limiter. Also
known as a leaky bucket rate
limit. Unlike some rate limiters which bucket tokens into fixed time intervals
— e.g. for a 5 minute limit, 00:00 - 00:05, 00:05 - 00:10, … — a
continuous drip rate limiter sets a pace. For example if 100 tokens are
available over 5 minutes, and the pool is empty, after 1 minute the pool has 20
tokens available, after 4 minutes it has 80 tokens available, and after the
full 5 minutes it has all 100 tokens available. This naturally spreads work
over time, rather than sending sharp bursts of jobs across fixed bucket
boundaries, then stalling until the next bucket etc. When the pool is full
however, it has 100 tokens available and therefore a sudden burst of 100 jobs
with a cost of 1 could all go at once, followed by a steady pace of around 1
job every 3 seconds. This is generally desirable in order to accommodate
short-lived spikes, but not always, and the behaviour is configurable through
the burst parameter on on the strategy.
To illustrate how this works, take this example which runs a job that takes a consistent 2 seconds per execution, using a budget configured at 6/minute.
time_basedat 6/minute:D, [2026-09-10T14:32:22.804431 #3955340] DEBUG -- : Received example2 (03guaf86qr82o2avowmz7yy1x), dispatch queue: 0 D, [2026-09-10T14:32:24.807137 #3955340] DEBUG -- : Job example2 (03guaf86qr82o2avowmz7yy1x) completed in 2.0021s D, [2026-09-10T14:32:32.803674 #3955340] DEBUG -- : Received example2 (03guaf86qwoxqtiauglqa24jv), dispatch queue: 0 D, [2026-09-10T14:32:34.806273 #3955340] DEBUG -- : Job example2 (03guaf86qwoxqtiauglqa24jv) completed in 2.0021s D, [2026-09-10T14:32:42.802740 #3955340] DEBUG -- : Received example2 (03guaf86r7mnwd0g5ms3ssciz), dispatch queue: 0 D, [2026-09-10T14:32:44.805806 #3955340] DEBUG -- : Job example2 (03guaf86r7mnwd0g5ms3ssciz) completed in 2.0021s D, [2026-09-10T14:32:52.803952 #3955340] DEBUG -- : Received example2 (03guaf86rd3iz4kqg39tqvl8o), dispatch queue: 0 D, [2026-09-10T14:32:54.807137 #3955340] DEBUG -- : Job example2 (03guaf86rd3iz4kqg39tqvl8o) completed in 2.0028s D, [2026-09-10T14:33:02.805181 #3955340] DEBUG -- : Received example2 (03guaf86rike1w42oka0tpw5w), dispatch queue: 0 D, [2026-09-10T14:33:04.807812 #3955340] DEBUG -- : Job example2 (03guaf86rike1w42oka0tpw5w) completed in 2.0021s D, [2026-09-10T14:33:12.803981 #3955340] DEBUG -- : Received example2 (03guaf86rti47fcur3fmwounf), dispatch queue: 0 D, [2026-09-10T14:33:14.806749 #3955340] DEBUG -- : Job example2 (03guaf86rti47fcur3fmwounf) completed in 2.0021s D, [2026-09-10T14:33:22.802674 #3955340] DEBUG -- : Received example2 (03guaf86ryyza7bcc64o67ov3), dispatch queue: 0 D, [2026-09-10T14:33:24.805673 #3955340] DEBUG -- : Job example2 (03guaf86ryyza7bcc64o67ov3) completed in 2.0021s
As you can see, each job takes its 2 seconds to complete, but the worker continues receiving and processing these jobs at a rate of 6 per second.
The burst is how full the token pool can be at any single point in time. When
not specified, the allocation is used, so for our 100 jobs/5 minute example
the default burst is 100, as descibed above. Budgets specifying a different
burst look like so:
Ruby:
Zizq.define_budget( "image-service", allocation: 10000, strategy: { type: :time_based, duration: 3600, burst: 500 } )
In this example, no more than 500 jobs can be dispatched at any moment, then
10,000/hour at a steady pace thereafter. Setting a burst of just 1 is
equivalent to enforcing the 10,000/hour always. It is also possible to set the
burst higher than the total allocation — say 20,000 tokens — which allows
for brief spikes of high throughput that exceed the rate limit by design, if
and only if the budget was otherwise unused for an equivalent period of time.
Again, to illustrate how this works, here’s that 6/minute job with its upfront default burst of 6 jobs in one go.
time_basedat 6/minute, with its burst:D, [2026-09-10T14:32:12.802443 #3955340] DEBUG -- : Received example2 (03guaf86oxlpqukoqrsaop7i1), dispatch queue: 0 D, [2026-09-10T14:32:12.802553 #3955340] DEBUG -- : Received example2 (03guaf86pjh61x0ge1m3vot7i), dispatch queue: 1 D, [2026-09-10T14:32:12.802623 #3955340] DEBUG -- : Received example2 (03guaf86puew7g5327hfv4txc), dispatch queue: 2 D, [2026-09-10T14:32:12.802694 #3955340] DEBUG -- : Received example2 (03guaf86pzvra89d4k7rr4wgz), dispatch queue: 3 D, [2026-09-10T14:32:12.802753 #3955340] DEBUG -- : Received example2 (03guaf86qathfre87u46ro62s), dispatch queue: 4 D, [2026-09-10T14:32:12.802809 #3955340] DEBUG -- : Received example2 (03guaf86qgaciim321320xx9v), dispatch queue: 5 D, [2026-09-10T14:32:14.805240 #3955340] DEBUG -- : Job example2 (03guaf86oxlpqukoqrsaop7i1) completed in 2.0021s D, [2026-09-10T14:32:14.805384 #3955340] DEBUG -- : Job example2 (03guaf86pjh61x0ge1m3vot7i) completed in 2.0023s D, [2026-09-10T14:32:14.805446 #3955340] DEBUG -- : Job example2 (03guaf86puew7g5327hfv4txc) completed in 2.0024s D, [2026-09-10T14:32:14.805496 #3955340] DEBUG -- : Job example2 (03guaf86pzvra89d4k7rr4wgz) completed in 2.0024s D, [2026-09-10T14:32:14.805545 #3955340] DEBUG -- : Job example2 (03guaf86qathfre87u46ro62s) completed in 2.0024s D, [2026-09-10T14:32:14.805589 #3955340] DEBUG -- : Job example2 (03guaf86qgaciim321320xx9v) completed in 2.0025s D, [2026-09-10T14:32:22.804431 #3955340] DEBUG -- : Received example2 (03guaf86qr82o2avowmz7yy1x), dispatch queue: 0 D, [2026-09-10T14:32:24.807137 #3955340] DEBUG -- : Job example2 (03guaf86qr82o2avowmz7yy1x) completed in 2.0021s D, [2026-09-10T14:32:32.803674 #3955340] DEBUG -- : Received example2 (03guaf86qwoxqtiauglqa24jv), dispatch queue: 0 D, [2026-09-10T14:32:34.806273 #3955340] DEBUG -- : Job example2 (03guaf86qwoxqtiauglqa24jv) completed in 2.0021s D, [2026-09-10T14:32:42.802740 #3955340] DEBUG -- : Received example2 (03guaf86r7mnwd0g5ms3ssciz), dispatch queue: 0 D, [2026-09-10T14:32:44.805806 #3955340] DEBUG -- : Job example2 (03guaf86r7mnwd0g5ms3ssciz) completed in 2.0021s D, [2026-09-10T14:32:52.803952 #3955340] DEBUG -- : Received example2 (03guaf86rd3iz4kqg39tqvl8o), dispatch queue: 0 D, [2026-09-10T14:32:54.807137 #3955340] DEBUG -- : Job example2 (03guaf86rd3iz4kqg39tqvl8o) completed in 2.0028s D, [2026-09-10T14:33:02.805181 #3955340] DEBUG -- : Received example2 (03guaf86rike1w42oka0tpw5w), dispatch queue: 0 D, [2026-09-10T14:33:04.807812 #3955340] DEBUG -- : Job example2 (03guaf86rike1w42oka0tpw5w) completed in 2.0021s D, [2026-09-10T14:33:12.803981 #3955340] DEBUG -- : Received example2 (03guaf86rti47fcur3fmwounf), dispatch queue: 0 D, [2026-09-10T14:33:14.806749 #3955340] DEBUG -- : Job example2 (03guaf86rti47fcur3fmwounf) completed in 2.0021s D, [2026-09-10T14:33:22.802674 #3955340] DEBUG -- : Received example2 (03guaf86ryyza7bcc64o67ov3), dispatch queue: 0 D, [2026-09-10T14:33:24.805673 #3955340] DEBUG -- : Job example2 (03guaf86ryyza7bcc64o67ov3) completed in 2.0021s
Here it is visible that before the worker settles into receiving these jobs at a rate of 6 per second, it receives an upfront burst of 6 jobs in one go. This only happens:
- If no jobs have been dispatched for the configured duration (i.e. the token pool is full); or
- The budget is freshly allocated (newly created, or the Zizq server was restarted).
When using burst, all jobs that reference the budget must have a cost less
than or equal to the configured burst, and any attempts to update the budget
such that this condition is violated are rejected.
Binding jobs to budgets
A job class using Zizq::Job declares which budgets it is bound to using
zizq_budget, and every enqueue carries that information:
Ruby:
class ProcessImageJob include Zizq::Job zizq_budget "image-service", cost: 2 def perform(attachment_id) end end
Note
The default
costis1and can be omitted.
Jobs can also bind to more than one budget at once, in which case all budgets must be satisfied before the job can be dispatched:
Ruby:
class ProcessImageJob include Zizq::Job zizq_budget "image-service", cost: 2 zizq_budget "cpu-intensive" def perform(attachment_id) end end
A single enqueue can override the job class’ default:
Ruby:
Zizq.enqueue_with( budgets: [{ key: "storage" }] ).enqueue(ProcessImageJob, 42)
With no budgets a job is unthrottled and dispatches as soon as it reaches the
front of the queue. With several, it must satisfy of them. A job bound to
a :while_in_flight limit of 10 and a :time_based limit of 1000/hour honours
both: never more than 10 at once, never more than 1000 an hour.
Use cost to make jobs weigh differently against the same pool. A bulk
operation costing 10 against an allocation of 100 leaves room for 90 more
single operations.
Creating budgets lazily
A budget normally exists before anything binds to it. create_with lets one
enqueue do both atomically:
Ruby:
class ProcessImageJob include Zizq::Job zizq_budget "image-service", cost: 2, create_with: { allocation: 10000, strategy: { type: :time_based, duration: 60 } } def perform(attachment_id) end end
If the budget already exists the policy is ignored and the stored one stays
authoritative — an enqueue with a create_with will never clobber an existing
tuned budget.
Managing budgets
Ruby:
# List all budgets on the server Zizq.budgets # Fetch a single budget Zizq.budget("emails") # Create a :while_in_flight budget raises Zizq::ConflictError if exists Zizq.define_budget( "controlled-fan-out", allocation: 10, strategy: { type: :while_in_flight } ) # Create a :time_based budget raises Zizq::ConflictError if exists Zizq.define_budget( "controlled-throughput", allocation: 100, strategy: { type: :time_based, duration: 60 } ) # Create a :time_based budget with burst Zizq.define_budget( "controlled-throughput", allocation: 100, strategy: { type: :time_based, duration: 60, burst: 10 } ) # Create or replace a budget Zizq.define_budget( "controlled-fan-out", allocation: 10, strategy: { type: :while_in_flight }, replace: true ) # Update any setting of an existing budget # Raises ConflictError if settings are unsatisfiable. Zizq.update_budget("controlled-throughput", strategy: { burst: 5 }) # Delete an existing budget # Raises ConflictError if jobs still reference the budget Zizq.delete_budget("controlled-fan-out")
Zizq.define_budget refuses an existing key with Zizq::ConflictError
and leaves the stored policy alone. Hence it is ok for every process in a
horizontally scaled workload to declare its budgets on boot without
coordination, and those one that lose the race simply treat the conflict as
success.
Ruby:
begin Zizq.define_budget( "image-service", allocation: 10000, strategy: { type: :time_based, duration: 60 } ) rescue Zizq::ConflictError # ok, already declared end
Pass replace: true to overwrite instead. A replace changes the policy, not
the budget’s identity, so created_at remains true to when the budget was
first created.
Zizq.update_budget is a deep (recursive) merge patch, so it is valid to
change a single field inside the strategy without repeating all the others.
burst: nil is the one meaningful use of nil — it clears the bucket’s
ceiling back to the default (its total allocation).
Changing which budgets jobs are bound to
Bindings are mutable even after jobs are enqueued. This is allows making adjustments e.g. during an incident response, such as splitting one shared budget in two, or taking a rate limit off a job that is stuck behind it.
This can be done directly on a single job resource object already in hand:
Ruby:
# Bind a new budget to an existing Zizq::Resources::Job object # The cost is optional and default to 1. If the job is already # bound this raises Zizq::ConflictError. job.bind_budget("emails", cost: 2) # The same as above, but replace rather than raising. job.rebind_budget("emails", cost: 3) # Bind a new budget to an existing Zizq::Resources::Job object # creating one atomically if it does not already exist. create_with also # works on job.rebind_budget(...). job.bind_budget("emails", cost: 2, create_with: { allocation: 20000, strategy: { type: :time_based, duration: 3600 } }) # Change the cost of an existing binding. job.set_budget_cost("emails", 5) # Remove an existing binding from a job. job.unbind_budget("emails") # Remove all bindings from a job (job becomes completely unthrottled) job.unbind_all_budgets # Atomically replace the entire set of bindings on the job. # Passing the empty array is the equivalent of job.unbind_all_budgets. job.replace_budgets([{key: "emails", cost: 2}])
Each updates the job data, so its #budgets reflects the change without a
second read. Job#bind_budget conflicts if the job is already bound to that
budget; #rebind_budget replaces the binding whole.
The same operations run over a filtered selection of jobs through Zizq.query:
Ruby:
# Bind a new budget to all matching jobs. Ignore any that already have the # binding. Zizq.query.by_queue("emails").bind_budget("stripe", cost: 2) # The same as above, but overwrite rather than ignore existing. Zizq.query.by_queue("emails").rebind_budget("stripe", cost: 2) # Change the cost for the named binding on all matching jobs. Zizq.query.by_queue("emails").set_budget_cost("stripe", 3) # Remove an existing binding from all matching jobs. Ignore any that don't # have the binding. Zizq.query.by_queue("emails").unbind_budget("stripe") # Remove all bindings from all matching jobs. If the query is not filtered # this strips all jobs of their bindings, making them regular unthrottled # jobs. Zizq.query.by_queue("emails").unbind_all_budgets
Important
Only queued jobs (
scheduled,ready) can be rebound. An in-flight job has already debited its tokens, and jobs in terminal states are always immutable. The bulk forms report the ones they could not touch rather than skipping them silently:Zizq.query.by_queue("emails").set_budget_cost("stripe", 3) # => {changed: 1252, blocked: ["03guad54591kly4v1vrezu536", "03guad546m9cauqlh07z2ut84"]}
:blockedis always in-flight jobs, so it can be interpreted as a retry list.
Querying jobs bound to a budget
A budget cannot be deleted while anything remains bound to it. The
#by_budgets_key filter selects exactly what is bound, and works anywhere
jobs are filtered:
Ruby:
# See how many jobs have the "emails" budget binding Zizq.query.by_budgets_key("emails").count # Remove the "emails" budget binding from those jobs Zizq.query.by_budgets_key("emails").unbind_budget("emails") # This should now work (assuming nothing was in-flight) Zizq.delete_budget("emails")
Zizq::Resources::Job also reports its bindings:
Ruby:
job = Zizq.client.get_job("03guad54591kly4v1vrezu536") job.budgets # => [{key: "emails", cost: 2}]
Note that there is no create_with on a read — that was acted upon at
enqueue-time and is not permanently stored as part of the details of the job
itself.