Zizq — Official Ruby Client
Zizq (/zɪsk/) is a fast and durable job queue packed into a single native binary, built on an embedded LSM database — not on Redis, and not on your RDBMS. It works in any stack, crossing programming language boundaries.
This is the official Zizq client library for Ruby.
Features
- Multi-thread and/or multi-fiber concurrent worker (via
async) -
Zizq::Jobbased job classes, Active Job support, or low-level/custom - Enqueue and process jobs from one language to another
- Arbitrary named queues
- Granular job priorities
- Scheduled jobs
- Configurable backoff policies
- Configurable job retention policies
- Recurring jobs (cron)
- Job introspection and management APIs, with support for
jqquery filters - Concurrency control that doesn't impact other jobs
- Rate limiting with configurable burst
- Unique jobs (deduplicated)
- Batched jobs (folded/merged)
- Testing helpers
Installation
Note
If you have not yet installed the Zizq server, follow the Getting Started guide first.
Add it to your application's Gemfile:
gem 'zizq', '~> 0.7.0'Or install it manually:
$ gem install zizq -v 0.7.0Ruby 3.2.8 or newer is required. Client and server share version numbers — keep the client's major/minor at or below the server's.
Configuration
Out of the box, the client talks to a server at http://localhost:7890 —
fine for local development. For anything else, configure it with
Zizq.configure in your application's bootstrap (e.g. a Rails initializer):
require 'zizq'
Zizq.configure do |c|
c.url = 'https://zizq.your.network:7890'
c.logger = Logger.new('log/zizq.log')
c.tls.ca = '/path/to/server-ca-cert.pem'
# Optional worker defaults — applied to every Zizq::Worker
# instance and to runs of the `zizq-worker` executable. Explicit
# kwargs or CLI flags override these.
c.worker.queues = ['emails', 'payments']
c.worker.fiber_count = 25
endFor mutual TLS, also set c.tls.client_cert and c.tls.client_key.
Caution
If your server is exposed directly to the internet, it should require mutual TLS — otherwise anybody can talk to it.
Usage
Tip
This README is an overview. The full documentation covers each feature in depth — middleware, custom dispatchers, Active Job, job querying, and more.
Defining a job
In most Ruby applications, a job is a plain class that includes Zizq::Job.
The class declares its defaults with the zizq_* DSL and implements
#perform:
class SendEmailJob
include Zizq::Job
zizq_queue 'emails'
zizq_priority 100
zizq_retry_limit 5
def perform(user_id, template:)
user = User.find(user_id)
Mailer.deliver(user, template)
end
endEvery default — zizq_queue, zizq_priority, zizq_retry_limit,
zizq_backoff, zizq_retention, zizq_unique — can be overridden per
enqueue. The job's class name ("SendEmailJob") becomes the API-level job
type, so keep it stable once jobs are in flight.
Enqueuing jobs
Enqueue a job by passing the class and the arguments your #perform method
expects:
job = Zizq.enqueue(SendEmailJob, 42, template: 'welcome')
job.id # => "03fu0wm75gxgmfyfplwvazhex"Override defaults for a single call with Zizq.enqueue_with, or with a block
that mutates the request:
# Don't retry this one.
Zizq.enqueue_with(retry_limit: 0).enqueue(SendEmailJob, 42, template: 'welcome')
# Bump the priority via the block form.
Zizq.enqueue(SendEmailJob, 42, template: 'welcome') do |req|
req.priority = 1000
endSchedule a job for later with delay (seconds from now) or an absolute
ready_at:
Zizq.enqueue_with(delay: 3600).enqueue(SendEmailJob, 42, template: 'welcome')
Zizq.enqueue_with(ready_at: Time.new(2027, 3, 15, 14, 30)).enqueue(SendEmailJob, 42, template: 'welcome')To enqueue many jobs efficiently, Zizq.enqueue_bulk sends them in a single
atomic request — across queues and job types, and job classes vs raw enqueues
can be mixed in too:
Zizq.enqueue_bulk do |b|
signups.each { |user_id| b.enqueue(SendEmailJob, user_id, template: 'welcome') }
endJobs can also be enqueued without Zizq::Job by providing the named fields —
designed for lower-level code style, and for cross-language workflows where,
for example, a Ruby app enqueues jobs consumed by a Go service.
Zizq.enqueue(
type: "send_email",
queue: "comms",
payload: { user_id: 42, template: "welcome" }
)Concurrency control
The Zizq server supports constraining the number of in_flight set of jobs for
specific job types by binding a named budget to those jobs.
# Define a named budget called "notifications" that jobs can reference
# (generally somewhere in your application startup code).
Zizq.define_budget(
"notifications",
allocation: 10,
strategy: { type: :while_in_flight }
)
# Specify the budget on the Job class (or at enqueue time).
class SendNotificationJob
include Zizq::Job
zizq_budget "notifications"
def perform(id)
# ...
end
end
# Those jobs execute no more than 10 at any given time. Other jobs continue to
# flow normally.
100.times do |n|
Zizq.enqueue(SendNotificationJob, n)
endRate limiting
The Zizq server supports dispatching jobs to workers at a throttled rate by binding a named budget to those jobs. Jobs that exceed the limit do not block other work. The server is smart enough to "park" them until the moment they are ready to dispatch.
# Define a named budget called "image-service" that jobs can reference
# (generally somewhere in your application startup code).
Zizq.define_budget(
"image-service",
allocation: 1000,
strategy: {
type: :time_based,
duration: 1.minute # or just 60 without ActiveSupport
}
)
# Specify the budget on the Job class (or at enqueue time).
class NormalizeImageJob
include Zizq::Job
zizq_budget "image-service"
def perform(id)
# ...
end
end
# Those jobs are delivered by the server at a rate of 1000/minute without
# blocking other jobs. Your workers remain free of such knowledge.
5000.times do |n|
Zizq.enqueue(NormalizeImageJob, n)
endCross-language and low-level dispatch
When a Ruby app needs to process jobs enqueued by another language
(or itself using raw enqueue form), Zizq::Router maps type strings to
handler blocks operating on plain JSON payloads:
Zizq.configure do |c|
c.dispatcher = Zizq::Router.new do
route('send_email') do |payload|
Mailer.deliver(payload['user_id'], payload['template'])
end
# Apps that mix the two styles can fall back to Zizq::Job
# for anything not handled by an explicit route.
fallback { |job| Zizq::Job.call(job) }
end
endSee Custom Dispatchers
for full details. Dispatchers in Zizq are just objects that implement #call
with a single Zizq::Resources::Job argument, and Zizq::Router is just a
dispatcher itself.
Running a worker
Jobs are processed by a worker, typically in a separate process. The simplest
way is the zizq-worker executable bundled with the gem. Rails apps need
no arguments — zizq-worker auto-detects config/environment.rb when run
from the app's root:
$ bundle exec zizq-worker
I, [...] INFO -- : Zizq worker starting: 1 threads, 25 fibers, prefetch=50
I, [...] INFO -- : Connected. Listening for jobs.For Sinatra or other apps, pass the entrypoint explicitly:
$ bundle exec zizq-worker app.rbWorker defaults (queues, thread_count, fiber_count, prefetch) come
from your Zizq.configure { |c| c.worker.* } block. CLI flags
(--threads, --fibers, --queue, --all-queues, etc.) override the
configured defaults when needed. Leave --fibers 1 if your application
isn't fiber-safe — no Async context is loaded in that case. INT /
TERM trigger a graceful shutdown (drains in-flight jobs up to
--shutdown-deadline, default 30s).
For more control — for example running the worker in-process alongside a
Rack app — construct Zizq::Worker directly:
require 'zizq'
# Picks up queues, fiber_count, etc. from Zizq.configure { |c| c.worker.* };
# any kwarg here overrides those defaults.
worker = Zizq::Worker.new(queues: ['emails', 'payments'])
Signal.trap('INT') { worker.stop }
worker.run # blocks until the worker stops#run blocks until the worker terminates; #stop drains in-flight jobs
gracefully, #kill forces an immediate stop. On any unclean shutdown the
server returns unfinished jobs to the queue — no job is lost.
Recurring jobs (cron)
Define a cron schedule in your application's startup code. Definitions are idempotent — every process can safely define the same schedule, and Zizq keeps the server in sync by adding, replacing, and removing entries as the definition changes. Cron requires a Pro license on the server.
Zizq.define_crontab('maintenance', timezone: 'Europe/London') do |cron|
# Every 15 minutes.
cron.define_entry('refresh_warehouse', '*/15 * * * *').enqueue(
RefreshWarehouseJob, incremental: true
)
# 9am London time, every day.
cron.define_entry('daily_digest', '0 9 * * *').enqueue(SendDailyDigestJob)
endOnce defined, schedules can be inspected and managed via
Zizq.crontab('maintenance') — paused/resumed at the schedule level or per
entry, and deleted entirely when no longer needed.
Testing
Set c.test_mode = true in your test helper and Zizq swaps the real
client out for an in-memory Zizq::Test::Client that buffers enqueues
instead of dispatching them. Tests can then assert on what was
enqueued and drain the buffer through the configured dispatcher —
no running server required.
# test/test_helper.rb (or spec/spec_helper.rb)
Zizq::Test.enable!
class ActiveSupport::TestCase
setup { Zizq::Test.reset! }
end
# In a test
def test_signup_fans_out
SignupService.new.run
assert Zizq::Test.enqueued?(SendWelcomeEmailJob, user_id: 42)
assert_equal 2, Zizq::Test.pending_jobs(only_queues: 'emails').size
# Drain the buffer through Zizq.configuration.dequeue_middleware
# (same path the real worker takes — registered middleware runs too).
Zizq::Test.dispatch_enqueued_jobs
endSee Testing for full details.
Resources
Support & Feedback
If you need help using Zizq, create an issue on the zizq-ruby repo. Feedback is very welcome.
License
MIT — see LICENSE.