Qrush
A lightweight, production-ready job queue and task scheduler for Rust applications built on Redis and Tokio. 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
- π 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.0.2"
To mount the dashboard, opt into one framework:
# Actix
= { = "2.0.2", = ["dashboard-actix"] }
# Axum
= { = "2.0.2", = ["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.0.2", = ["dashboard-actix"] }
If you only used enqueue + workers (no dashboard), a plain qrush = "2.0.2"
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.0.2"
= { = "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
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.
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.0.2", = ["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.
1. Create Engine Binary
Create src/bin/qrush_engine.rs:
use ;
use set_redis_url;
use register_job;
use CronScheduler;
// Your job types
use NotifyUser;
async
2. Update Cargo.toml
The engine binary above uses tracing_subscriber for logging and dotenvy
to load .env, so add them alongside the [[bin]] entry:
[]
= { = "0.3", = ["env-filter"] }
= "0.15"
[[]]
= "qrush_engine"
= "src/bin/qrush_engine.rs"
3. Web Server (No Workers)
In your web server main.rs, only register jobs for enqueueing β do not
call QueueConfig::initialize (that starts workers; the engine process owns
them here). Use whichever framework you enabled. To also expose the dashboard,
mount it exactly as shown in Integrated Mode β Initialize QRush.
Actix (features = ["dashboard-actix"]):
use set_redis_url;
use register_job;
use qrush_metrics_routes;
use ;
async
Axum (features = ["dashboard-axum"]):
use set_redis_url;
use register_job;
use qrush_metrics_router;
use Router;
async
4. Run Both Processes
Terminal 1 - Web Server:
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.
Common examples:
| 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
- Documentation: docs.rs/qrush
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Made with β€οΈ by Srotas Space