Qrush
Lightweight, production-ready job queue & task scheduler for Rust β Redis + Tokio, with an optional Actix / Axum dashboard.
πΊ Watch the video walkthrough β
The core is web-framework agnostic, and the optional built-in dashboard works with either Actix Web or Axum. Qrush provides both integrated and separate process modes, making it suitable for everything from simple background tasks to large-scale distributed systems.
- Features
- Feature Flags
- Quick Start
- Recommended Project Layout (
qrushes/) - Architecture
- API Reference
- Examples
- Integrated Mode (Detailed)
- Separate Process Mode (Detailed)
- Cron Expressions
- Metrics Endpoints
- Securing the Dashboard
- Production Tips
Features
- π Dual Deployment Modes: Integrated (single process) or separate worker process
- π§© Framework Choice: Optional dashboard for Actix Web or Axum; the queue/worker core needs neither
- β‘ High Performance: Built on Redis and Tokio for maximum throughput
- π Cron Scheduling: Full cron expression support for recurring tasks
- β±οΈ Delayed Jobs: Schedule jobs to run after a specified delay
- π Built-in Metrics UI: Real-time dashboard for monitoring queues, jobs, and workers
- π Security: Optional Basic Auth for metrics endpoints
- π― Type-Safe: Leverages Rust's type system for safe job handling
- π Graceful Shutdown: Clean worker shutdown with configurable grace periods
- π Scalable: Support for multiple queues with different priorities and concurrency levels
Feature Flags
The built-in dashboard is optional and works with either Actix or Axum β pick the one that matches your app.
| Feature | Default | Description |
|---|---|---|
dashboard-actix |
β | Metrics dashboard served with Actix Web (qrush::routes::metrics_route). Pulls in Actix Web, Tera, and the web stack. |
dashboard-axum |
β | Metrics dashboard served with Axum (qrush::routes::axum_route). Pulls in Axum, Tera, and the web stack. |
dashboard |
β | Back-compat alias for dashboard-actix. |
Library-only usage (default). No dashboard framework is enabled by default,
so a plain dependency gives you enqueue + workers with no web stack:
[]
= "2.1.1"
To mount the dashboard, opt into one framework:
# Actix
= { = "2.1.1", = ["dashboard-actix"] }
# Axum
= { = "2.1.1", = ["dashboard-axum"] }
Migrating from 1.x to 2.0
In 1.x the dashboard was Actix-only and enabled by default. In 2.0 it is framework-selectable and off by default. Nothing else changed β the route-wiring function and all queue/worker/cron APIs are the same.
| 1.x | 2.0 | |
|---|---|---|
| Dashboard default | on (Actix) | off |
| Enable Actix dashboard | (default) | features = ["dashboard-actix"] |
| Enable Axum dashboard | not available | features = ["dashboard-axum"] |
# 1.x
= "1.0.1"
# 2.0 β Actix (equivalent to the old default; no code changes needed)
= { = "2.1.1", = ["dashboard-actix"] }
If you only used enqueue + workers (no dashboard), a plain qrush = "2.1.1"
now pulls in less β the web stack is no longer compiled by default. See the
CHANGELOG for the full list of changes.
Quick Start
Installation
Add to your Cargo.toml:
[]
= "2.1.1"
= { = "1", = ["rt-multi-thread", "macros"] }
= { = "1", = ["derive"] }
= "0.1"
= "1"
= "0.3"
qrushbundles its own Redis client (with cluster support), so you don't need to depend onredisdirectly unless you use it yourself.
Basic Usage (Integrated Mode)
use Job;
use ;
use QueueConfig;
use register_job;
use async_trait;
use ;
use BoxFuture;
use Result;
async
Integrated Process Mode
Use this mode when: You want production-ready in the same server process
Recommended Project Layout (qrushes/ module)**
The snippets above inline everything into main to stay short. In a real app
you'll want main to stay minimal and keep all qrush wiring in one place. The
convention used by the reference demos is a self-contained qrushes/ module:
main only calls qrushes::initiate::initiate(), and every job, cron, and piece
of configuration lives under qrushes/.
src/
βββ main.rs # calls qrushes::initiate::initiate() β nothing else qrush-related
βββ qrushes/
βββ mod.rs # pub mod crons; pub mod initiate; pub mod jobs;
βββ initiate.rs # ALL wiring: Redis URL, auth, register jobs+crons, init queues
βββ jobs/
β βββ mod.rs
β βββ send_email_job.rs
βββ crons/
βββ mod.rs
βββ interval_1minutes_notify_slack_cron.rs
βββ interval_2minutes_notify_slack_cron.rs
main.rs β minimal
Everything qrush-specific collapses to a single call. The only framework-specific
line left in main is mounting the dashboard route.
Actix (features = ["dashboard-actix"]):
use ;
use qrush_metrics_routes;
async
Axum (features = ["dashboard-axum"]):
use Router;
use qrush_metrics_router;
async
qrushes/initiate.rs β the single entry point
initiate() owns the four-step boot sequence (set Redis URL β register β
register crons β initialize) plus the
optional dashboard auth. It is framework-agnostic β the same file works under
Actix and Axum.
use ;
use CronScheduler;
use ;
use register_job;
use crateInterval1MinutesNotifySlackCron;
use crateInterval2MinutesNotifySlackCron;
use crateSendEmailJob;
/// Parse a `user:password` pair for the optional `QRUSH_BASIC_AUTH` gate.
/// Configure and start qrush. Reads `REDIS_URL` and the optional
/// `QRUSH_BASIC_AUTH`, registers jobs + crons, initializes the queues, and
/// seeds a couple of demo jobs so the dashboard has data.
pub async
Because the cron registration is wrapped (log-and-continue instead of
?),initiate()is restart-safe β see the restart note. If your app already has auser:passwordparser elsewhere, import that instead of the smallparse_user_passshown here.
qrushes/jobs/send_email_job.rs β one job per file
Each job is a plain Job plus a type_name()/handler() pair so
a worker can rebuild it from Redis. type_name() must match name().
use async_trait;
use BoxFuture;
use ;
use Job;
qrushes/crons/interval_1minutes_notify_slack_cron.rs β one cron per file
A cron file is the same as a job file plus a CronJob
impl (a cron_expression + a unique cron_id). Here perform() does real
work β POSTing to a Slack incoming webhook, the HTTP equivalent of
curl -X POST -H 'Content-type: application/json' --data '{"text":"β¦"}' <webhook>:
use async_trait;
use BoxFuture;
use ;
use json;
use CronJob;
use Job;
The 2-minute variant is identical apart from cron_expression ("0 */2 * * * *")
and a distinct cron_id β each CronJob needs its own cron_id, or the
second registration collides with the first in Redis.
Returning
Errfrom a cron'sperform()(e.g. a non-2xx webhook response) triggers the same retry / dead-letter path as any other job. The Slack webhook needs an HTTP client β the demos usereqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }.
Architecture
QRush supports two deployment modes:
Integrated Mode
Workers run in the same process as your application. Perfect for small to medium applications.
βββββββββββββββββββββββ
β Application β
β (Single Process) β
β β
β β’ HTTP Server β
β β’ Enqueue Jobs β
β β’ Process Jobs β β Workers here
βββββββββββββββββββββββ
Separate Process Mode
Workers run in a dedicated process. Recommended for production environments.
βββββββββββββββββββββββ βββββββββββββββββββββββ
β Web Server β β qrush-engine β
β (cargo run) β β (separate process) β
β β β β
β β’ HTTP Server β β β’ Worker Pools β
β β’ Enqueue Jobs ββββββΌββRedisβββΌββΆ Process Jobs β
β β’ Serve Routes β β β’ Cron Scheduler β
βββββββββββββββββββββββ βββββββββββββββββββββββ
Documentation
Integrated Mode
See Part 1: Integrated Mode below for complete setup instructions.
Separate Process Mode
See Part 2: Separate Process Mode below for production deployment.
API Reference
Core Traits
Job: Implement this trait for your job types. Onlyperform,name, andqueueare required; thebefore/after/on_error/alwayslifecycle hooks are optional overrides.CronJob: Implement for recurring scheduled jobs
Core Functions
enqueue(job) -> QrushResult<String>: Enqueue a job immediately; returns the job IDenqueue_in(job, delay_secs) -> QrushResult<String>: Enqueue a job with a delay; returns the job IDregister_job(name, handler): Register a job handlerQueueConfig::initialize(redis_url, queues): Start worker pools and the cron schedulerset_basic_auth(Some(QrushBasicAuthConfig { .. })): Protect the dashboard with HTTP Basic Auth
Failed jobs are retried automatically with
exponential backoff and moved to a dead-letter queue after MAX_RETRIES (3).
Cron Scheduling
All under qrush::cron::cron_scheduler::CronScheduler (see Cron Jobs):
register_cron_job(job) -> Result<()>: Persist a schedule to Redislist_cron_jobs() -> Result<Vec<CronJobMeta>>: List registered cron jobsrun_now(cron_id) -> Result<String>: Enqueue a cron job immediatelytoggle_cron_job(cron_id, enabled) -> Result<()>: Pause / resume a scheduledelete_cron_job(cron_id) -> Result<()>: Remove a schedule
Errors
The public API returns QrushResult<T> (Result<T, QrushError>). QrushError
distinguishes Redis, Serialization, and Config failures, and implements
std::error::Error, so it still propagates through ? in anyhow-based code.
Engine Runtime
qrush::engine::run_engine(redis_url, queues, shutdown_grace_secs): Run worker processqrush::engine::parse_queues(spec): Parse queue specification string
Command-Line Interface
The crate also ships reference binaries β qrush (a management CLI with
start/stop/status/stats/queues/jobs subcommands) and qrush-engine
(the worker process) β that you can adapt for your own app. See
src/bin/cli.md for the full CLI guide.
Examples
Runnable dashboard examples
The repo ships a complete, runnable dashboard example for each framework. With a
Redis instance available (REDIS_URL, defaults to redis://127.0.0.1:6379):
# Actix β serves http://127.0.0.1:8080/qrush/metrics
# Axum β serves http://127.0.0.1:8080/qrush/metrics
Job Lifecycle Hooks
Beyond perform, the Job trait exposes optional hooks that
wrap each execution. All are async and have default no-op implementations, so
you only override the ones you need:
| Hook | When it runs | Signature | Notes |
|---|---|---|---|
before |
Before perform |
async fn before(&self) -> Result<()> |
Return Err to skip the job β it is marked skipped (a terminal, non-failure state) and perform never runs. |
perform |
The actual work | async fn perform(&self) -> Result<()> |
Return Err to trigger retry / dead-letter. |
after |
After a successful perform |
async fn after(&self) |
Skipped if perform errored. |
on_error |
After a failed perform |
async fn on_error(&self, err: &anyhow::Error) |
Runs before the retry is scheduled. Good for logging/alerting. |
always |
After every attempt that ran perform |
async fn always(&self) |
Runs on both success and failure (but not when before skipped the job). |
use Job;
use async_trait;
use ;
use ;
Delayed Jobs
enqueue_in(job, delay_secs) runs a job after a delay instead of immediately.
It returns the job ID and is otherwise identical to enqueue β same job type,
same worker, same retry semantics.
use ;
// Run now.
let id = enqueue.await?;
// Run in 10 minutes (600 seconds).
let id = enqueue_in.await?;
Delayed jobs sit in a Redis sorted set keyed by their run-at timestamp; a
dedicated delayed-worker pool (started by QueueConfig::initialize) promotes
them onto their queue once due. Precision is bounded by the poll interval, so
treat the delay as "at least N seconds", not an exact wall-clock alarm.
Retries & Dead-Letter Queue
When perform returns Err, QRush retries the job automatically β you don't
schedule retries yourself:
on_erroris called, and the error string is stored on the job.- The job's retry counter increments. While it's
<= 3(MAX_RETRIES), the job is re-queued with exponential backoff plus jitter (10s * 2^retries, jittered to avoid thundering-herd retries) and its status becomesretrying. - After the 3rd retry is exhausted, the job moves to the dead-letter queue
(
status = dead) instead of being dropped. Inspect and requeue dead jobs from the dashboard at/qrush/metrics/extras/dead(or the dead-jobs view).
Job status values you'll see in Redis / on the dashboard:
| Status | Meaning |
|---|---|
pending |
Enqueued, waiting for a worker |
delayed |
Scheduled via enqueue_in, not yet due |
retrying |
Failed once or more; waiting for its backoff to elapse |
skipped |
before() returned Err; terminal, treated as a non-failure |
success |
perform() completed successfully |
dead |
Retries exhausted; parked in the dead-letter queue |
failed |
Could not run at all (e.g. no handler registered for the job name) |
Retries and the dead-letter queue are handled by the worker process, so they apply wherever
QueueConfig::initializeruns β the app in integrated mode, or the engine binary in separate process mode.
Cron Jobs
A cron job is a regular Job that runs on a schedule instead of
being enqueued by hand. The work still lives in Job::perform; CronJob only
adds when to run it. Follow these three steps.
Step 1 β Define the job and its perform()
This is identical to any other QRush job: implement Job (the work + a handler
so a worker can rebuild it from Redis).
use Job;
use register_job;
use async_trait;
use ;
use BoxFuture;
use Result;
Step 2 β Add the schedule (CronJob)
Attach a cron expression and a unique id to the same type.
use CronJob;
Step 3 β Register and start it in main
The job above is framework-agnostic; only main differs. In both frameworks
the order is the same:
set_redis_url(...)β required before any Redis call.register_job(...)β so a worker can run the job.CronScheduler::register_cron_job(...)β saves the schedule to Redis.QueueConfig::initialize(...)β starts the workers and the cron scheduler.
β οΈ The cron scheduler only runs where
QueueConfig::initializeis called. In Separate Process Mode that's the engine binary, not the web server β put steps 2β4 there.
Actix (features = ["dashboard-actix"]):
use ;
use register_job;
use CronScheduler;
use qrush_metrics_routes;
use ;
async
Axum (features = ["dashboard-axum"]):
use ;
use register_job;
use CronScheduler;
use qrush_metrics_router;
use Router;
async
That's it. When the schedule fires, QRush enqueues the job onto its queue()
and a worker runs perform(). Watch it (and manage schedules) on the dashboard
at /qrush/metrics/extras/cron.
On restart:
register_cron_joberrors if a job with the samecron_idalready exists in Redis, so the?above would abort a second boot. The schedule already survives restarts, so either skip re-registering, treat the duplicate as non-fatal (log and continue instead of?), or callCronScheduler::delete_cron_job("hourly_email")first to re-seed it.
register_job vs register_cron_job β why a cron needs both
They answer two different questions β one is how to run a job type, the other is when to run it:
register_job(name, handler)β the how. It maps a job's name to a handler that rebuilds the struct from its stored JSON so a worker can runperform(). It lives in an in-memory registry, so it must be called on every startup, in every process that runs workers. Miss it and the job fires but the worker can't reconstruct it (statusfailed).CronScheduler::register_cron_job(job)β the when. It persists the schedule (cron expression, timezone, next-run, payload) to Redis, so the scheduler enqueues the job when it's due. Miss it and the handler exists but nothing ever triggers it.
When a schedule fires, the stored payload is enqueued tagged with the job name,
and the worker resolves the handler via the registry register_job populated β
which is why a cron calls both.
register_job |
register_cron_job |
|
|---|---|---|
| Stores | in-memory HashMap |
Redis (durable) |
| Answers | how to rebuild & run | when to fire |
| Call frequency | every startup, every worker process | once (persists across restarts) |
| Duplicate handling | overwrites silently | errors if cron_id already exists |
| Needed by | every job a worker runs | only cron / scheduled jobs |
| Sync / async | sync | async (writes to Redis) |
Rule of thumb: a plain job = register_job + enqueue(...); a cron job =
register_job and register_cron_job. In separate process
mode, register_cron_job must run where the
scheduler runs β the engine, not the web server.
Managing cron jobs
Manage schedules from the dashboard at /qrush/metrics/extras/cron, or
programmatically via CronScheduler:
use CronScheduler;
list_cron_jobs.await?; // -> Vec<CronJobMeta>
run_now.await?; // enqueue once, right now
toggle_cron_job.await?; // pause
toggle_cron_job.await?; // resume
delete_cron_job.await?; // remove entirely
To register a job that starts paused, override enabled() on the CronJob
impl (it defaults to true); enable it later from the dashboard or with
toggle_cron_job:
A disabled job stays registered but is skipped and removed from the run schedule until re-enabled.
Multiple Queues
let queues = vec!;
Metrics UI
Requires a dashboard feature β
dashboard-actixordashboard-axum(not enabled by default). See Feature Flags.
Access the built-in metrics dashboard at /qrush/metrics:
- Queue statistics and job counts
- Worker status and health
- Cron job management
- Job retry and deletion
- CSV export
Requirements
- Rust 1.89.0 or later
- Redis 6.0 or later
- Tokio runtime (multi-threaded)
Environment Variables
QRush itself only reads REDIS_URL (and only where you pass it β most APIs take
the URL explicitly). The other variables below are conventions used by the
example binaries; your code decides whether to read them.
# Read by qrush where a Redis URL is expected
REDIS_URL=redis://127.0.0.1:6379
# Conventions (you read these yourself β see the sections linked)
QRUSH_BASIC_AUTH=admin:password # dashboard auth β you parse it and call set_basic_auth()
RUST_LOG=info,qrush=info # tracing filter, honored by tracing_subscriber
β οΈ Setting
QRUSH_BASIC_AUTHalone does nothing β the crate never reads it. Dashboard auth is configured programmatically; see Securing the Dashboard.
Detailed Documentation
Integrated Mode (Detailed)
Use this mode when: You want a simple setup with workers running in the same process as your web server.
1. Add Dependencies
[]
# Pick the dashboard framework you use: "dashboard-actix" or "dashboard-axum"
= { = "2.1.1", = ["dashboard-actix"] }
= "4" # or: axum = "0.8"
= { = "1", = ["rt-multi-thread", "macros"] }
= { = "1", = ["derive"] }
= "0.1"
= "1"
= "0.3"
2. Define a Job
use Job;
use async_trait;
use ;
use BoxFuture;
use Result;
3. Initialize QRush
The queue/worker setup is identical for both frameworks β only the dashboard
wiring differs. The dashboard mounts at /qrush/metrics/... in both cases.
Actix (features = ["dashboard-actix"]):
use ;
use register_job;
use qrush_metrics_routes;
use ;
async
Axum (features = ["dashboard-axum"]):
use ;
use register_job;
use qrush_metrics_router;
use Router;
async
4. Enqueue Jobs
use ;
// Immediate
enqueue.await?;
// Delayed (300 seconds)
enqueue_in.await?;
Separate Process Mode (Detailed)
Use this mode when: You want production-ready separation with workers in a dedicated process.
Recommended Project Layout (qrushes_engines/ module)
Integrated mode keeps all wiring in qrushes/.
Separate process mode uses the same idea in a self-contained
qrushes_engines/ module β the engine binary's main only calls
qrushes_engines::initiate::initiate(), and every job, cron, and piece of engine
configuration lives under qrushes_engines/.
There are only two differences from the integrated qrushes/ layout:
initiate()ends withrun_engine(...)instead ofQueueConfig::initialize(...).run_enginestarts the worker pools, the delayed-job handler, and the cron scheduler, then blocks untilSIGINT/SIGTERMβ so it is the last thinginitiate()does, not a call it returns from.- Jobs and crons live in your crate's library (
src/lib.rs), because the engine process and the web server are two binaries that both need the same job/cron types. Put the module in the lib and both canuse your_app::qrushes_engines::β¦.
src/
βββ lib.rs # pub mod qrushes_engines;
βββ main.rs # web server: enqueue + dashboard, NO workers
βββ bin/
β βββ qrush_engine.rs # worker process: calls qrushes_engines::initiate::initiate()
βββ qrushes_engines/
βββ mod.rs # pub mod initiate; pub mod jobs; pub mod crons;
βββ initiate.rs # shared registry + two entry points: initiate_web() / initiate_engine()
βββ jobs/
β βββ mod.rs # pub mod send_email_job;
β βββ send_email_job.rs
βββ crons/
βββ mod.rs # pub mod interval_1minutes_notify_slack_cron;
βββ interval_1minutes_notify_slack_cron.rs
src/lib.rs β expose the module to both binaries
src/bin/qrush_engine.rs β minimal worker process
Everything engine-specific collapses to a single call, exactly like main.rs does
in integrated mode. Replace your_app with your crate's name (the name under
[package] in Cargo.toml).
use qrushes_engines;
async
src/qrushes_engines/mod.rs
src/qrushes_engines/initiate.rs β two entry points, one registry
Both processes must register the same job/cron handlers β the web server to
enqueue/serialize them, the engine to run them. So the register_job(...) list
lives here once, in a shared register_all(), and two thin entry points build
on it:
initiate_web()β registers the handlers and returns. The web server calls this; it does not start workers.initiate_engine()β registers the handlers, registers the cron schedules, then callsrun_engine(...), which starts the worker pools + delayed handler + cron scheduler and blocks untilSIGINT/SIGTERM.
Keeping registration in one function means adding a job is a one-line change that both processes pick up β you can't forget to register it in one of them.
use set_redis_url;
use CronScheduler;
use ;
use register_job;
use crateInterval1MinutesNotifySlackCron;
use crateSendEmailJob;
/// Single source of truth for the type registry: set the Redis URL and register
/// every job + cron handler. Shared by both processes. Returns the Redis URL so
/// the engine can hand it to `run_engine`.
/// Web-server entry point: register handlers so jobs can be enqueued, but do NOT
/// start workers β the engine process owns those.
pub async
/// Engine entry point: register handlers + cron schedules, then run the workers.
/// `run_engine` owns `QueueConfig::initialize` internally and BLOCKS until
/// shutdown (with a 5s graceful-shutdown grace period).
pub async
β οΈ The cron scheduler runs only where
run_engineruns β the engine process, viainitiate_engine().initiate_web()deliberately skips both the cron registration and the workers.
These are the same job/cron types as integrated mode β a plain
Job (plus a CronJob impl for crons) with a
type_name()/handler() pair β so a job enqueued by the web server deserializes
and runs in the engine process. They just live in the lib under qrushes_engines/
instead of qrushes/.
src/qrushes_engines/jobs/send_email_job.rs β one job per file
use async_trait;
use BoxFuture;
use ;
use Job;
src/qrushes_engines/jobs/mod.rs just re-exports it:
src/qrushes_engines/crons/interval_1minutes_notify_slack_cron.rs β one cron per file
use async_trait;
use BoxFuture;
use ;
use json;
use CronJob;
use Job;
src/qrushes_engines/crons/mod.rs just re-exports it:
The web server (src/main.rs)
The web server reuses the same module but does not start workers β its main
calls qrushes_engines::initiate::initiate_web() (register handlers only), then
mounts the dashboard. See Web Server (No Workers) below
for the full Actix/Axum main.rs.
The engine binary, its initiate.rs, and the job/cron files were all defined
above. The two steps below just wire the two processes together β you do
not create qrush_engine.rs again.
1. Register the engine binary in Cargo.toml
The engine binary above uses tracing_subscriber for logging and dotenvy to
load .env, so add them alongside the [[bin]] entry. Adding this second binary
makes a bare cargo run ambiguous (error: could not determine which binary to run), so set default-run to your web binary β then cargo run starts the
web server and cargo run --bin qrush_engine starts the worker:
[]
= "your_app"
# ...
= "your_app" # so a bare `cargo run` picks the web server, not the engine
[]
= { = "0.3", = ["env-filter"] }
= "0.15"
[[]]
= "qrush_engine"
= "src/bin/qrush_engine.rs"
The web-server binary is named after your package (
src/main.rsβ thenameunder[package]). Withoutdefault-runyou must always disambiguate:cargo run --bin your_app.
2. Web Server (No Workers)
The web server's main.rs calls initiate_web() β the same registry as the
engine, minus the workers β then mounts the dashboard. Because initiate_web()
never calls run_engine/QueueConfig::initialize, it returns immediately and the
HTTP server starts. Just like integrated mode, main stays minimal.
Actix (features = ["dashboard-actix"]):
use qrushes_engines;
use qrush_metrics_routes;
use ;
async
Axum (features = ["dashboard-axum"]):
use qrushes_engines;
use qrush_metrics_router;
use Router;
async
3. Run Both Processes
Terminal 1 - Web Server: (needs default-run from step 1; otherwise use cargo run --bin your_app)
Terminal 2 - Worker Engine:
Cron Expressions
QRush accepts both 6-field (sec min hour day month weekday) and 5-field
(min hour day month weekday) expressions. A 5-field expression defaults seconds
to 0, so */5 * * * * and 0 */5 * * * * are equivalent.
| Expression | Meaning |
|---|---|
"* * * * *" |
Every minute (5-field) |
"0 * * * * *" |
Every minute (6-field) |
"0 */5 * * * *" |
Every 5 minutes |
"0 0 * * * *" |
Every hour |
"0 0 0 * * *" |
Daily at midnight |
"0 30 9 * * *" |
Daily at 09:30 |
"0 0 0 * * 1" |
Every Monday at midnight |
"0 0 9 * * MON-FRI" |
Weekdays at 09:00 |
"0 0 0 1 * *" |
First day of every month |
"0 0 12 1 JAN *" |
Jan 1st at noon |
Each field supports the usual operators:
*β any valueaβ an exact valuea,b,cβ a lista-bβ an inclusive range*/nβ a step over the whole range (e.g.*/15in minutes)a-b/nβ a step within a range- Names: months
JANβDEC, weekdaysSUNβSAT(case-insensitive). For the weekday field, both0and7mean Sunday.
Timezone. Expressions evaluate in UTC by default. Override per job with
fn timezone(&self) -> &'static str on the CronJob impl, returning any IANA
name (e.g. "Asia/Kolkata", "America/New_York") β so "0 0 9 * * *" fires at
09:00 in that zone, DST included.
Precision & missed runs. The scheduler ticks every ~5 seconds, so a job fires within a few seconds of its scheduled time (don't rely on sub-5s precision). If the scheduler was down when a run was due, that run fires once on the next tick and is then re-anchored to its next future slot β missed cycles are not backfilled one-per-cycle. Claiming is atomic in Redis, so running multiple engine processes will not double-fire the same job.
Metrics Endpoints
Paths assume the dashboard is mounted at /qrush (as in the examples). The
Actix and Axum adapters expose the same routes:
| Method & Path | Purpose |
|---|---|
GET /qrush/metrics |
Dashboard overview |
GET /qrush/metrics/health |
Health check (returns healthy) |
GET /qrush/metrics/queues/{queue} |
Per-queue details |
GET /qrush/metrics/queues/{queue}/export |
Export a queue's jobs as CSV |
GET /qrush/metrics/extras/summary |
Aggregate metrics summary |
GET /qrush/metrics/extras/delayed |
Delayed (scheduled-later) jobs |
GET /qrush/metrics/extras/scheduled |
Scheduled jobs |
GET /qrush/metrics/extras/retry |
Jobs waiting to retry |
GET /qrush/metrics/extras/failed |
Failed jobs |
GET /qrush/metrics/extras/dead |
Dead-letter queue |
GET /qrush/metrics/extras/cron |
Cron job management |
POST /qrush/metrics/jobs/action |
Job actions (retry / delete) |
POST /qrush/metrics/cron/action |
Cron actions (run-now / toggle / delete) |
Securing the Dashboard (Basic Auth)
The dashboard is open by default. To require HTTP Basic Auth, register
credentials with set_basic_auth before you start the web server. Once
credentials are set, the built-in middleware (already wired into both the Actix
and Axum routers) enforces them on every /qrush/metrics/... request using a
constant-time credential comparison.
use ;
// Read from the environment (recommended) β the crate does NOT do this for you.
if let Ok = var
// ...then mount the dashboard and start the server as usual.
- Call
set_basic_authonce, during startup, before serving requests. - Passing
None(or never calling it) leaves the dashboard open. - There's no env-var auto-wiring:
QRUSH_BASIC_AUTHis only a naming convention β you read it and callset_basic_authyourself, as above. - Basic Auth sends credentials base64-encoded, not encrypted. Terminate TLS in front of the dashboard (reverse proxy) for anything internet-facing.
Production Tips
- Use separate process mode for production
- Protect the dashboard with Basic Auth (and put TLS in front of it)
- Configure appropriate queue concurrency based on your workload
- Monitor Redis memory usage
- Use graceful shutdown for zero-downtime deployments
- Scale workers horizontally by running multiple engine processes
License
This project is licensed under the MIT License - see the LICENSE file for details.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Support
- Video walkthrough: @srotas-space on YouTube
- Documentation: docs.rs/qrush
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Made with β€οΈ by Srotas Space
π₯ Contributors
- Sandeep Maurya - Creator & Lead Developer LinkedIn