# Qrush
[](https://crates.io/crates/qrush)
[](https://docs.rs/qrush)
[](LICENSE)
[](https://crates.io/crates/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.
| `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:
```toml
[dependencies]
qrush = "2.0.0"
```
To mount the dashboard, opt into one framework:
```toml
# Actix
qrush = { version = "2.0.0", features = ["dashboard-actix"] }
# Axum
qrush = { version = "2.0.0", features = ["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.
| Dashboard default | on (Actix) | off |
| Enable Actix dashboard | (default) | `features = ["dashboard-actix"]` |
| Enable Axum dashboard | not available | `features = ["dashboard-axum"]` |
```toml
# 1.x
qrush = "1.0.1"
# 2.0 β Actix (equivalent to the old default; no code changes needed)
qrush = { version = "2.0.0", features = ["dashboard-actix"] }
```
If you only used `enqueue` + workers (no dashboard), a plain `qrush = "2.0.0"`
now pulls in **less** β the web stack is no longer compiled by default. See the
[CHANGELOG](CHANGELOG.md) for the full list of changes.
## Quick Start
### Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
qrush = "2.0.0"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
serde = { version = "1", features = ["derive"] }
async-trait = "0.1"
anyhow = "1"
futures = "0.3"
```
> `qrush` bundles its own Redis client (with cluster support), so you don't need
> to depend on `redis` directly unless you use it yourself.
### Basic Usage (Integrated Mode)
```rust
use qrush::job::Job;
use qrush::queue::{enqueue, enqueue_in};
use qrush::config::QueueConfig;
use qrush::registry::register_job;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use futures::future::BoxFuture;
use anyhow::Result;
#[derive(Clone, Serialize, Deserialize)]
pub struct EmailJob {
pub to: String,
pub subject: String,
}
#[async_trait]
impl Job for EmailJob {
async fn perform(&self) -> Result<()> {
println!("Sending email to {}: {}", self.to, self.subject);
// Your email sending logic here
Ok(())
}
fn name(&self) -> &'static str { "EmailJob" }
fn queue(&self) -> &'static str { "default" }
}
impl EmailJob {
pub fn name() -> &'static str { "EmailJob" }
pub fn handler(payload: String) -> BoxFuture<'static, Result<Box<dyn Job>>> {
Box::pin(async move {
let job: EmailJob = serde_json::from_str(&payload)?;
Ok(Box::new(job) as Box<dyn Job>)
})
}
}
#[tokio::main]
async fn main() -> Result<()> {
// Set Redis URL
std::env::set_var("REDIS_URL", "redis://127.0.0.1:6379");
// Register job
register_job(EmailJob::name(), EmailJob::handler);
// Initialize queues
let queues = vec![
QueueConfig::new("default", 5, 0),
];
QueueConfig::initialize(
"redis://127.0.0.1:6379".to_string(),
queues
).await?;
// Enqueue a job
enqueue(EmailJob {
to: "user@example.com".to_string(),
subject: "Hello!".to_string(),
}).await?;
// Keep running
tokio::signal::ctrl_c().await?;
Ok(())
}
```
## 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](#integrated-mode-detailed) below for complete setup instructions.
### Separate Process Mode
See [Part 2: Separate Process Mode](#separate-process-mode-detailed) below for production deployment.
## API Reference
### Core Traits
- `Job`: Implement this trait for your job types
- `CronJob`: Implement for recurring scheduled jobs
### Core Functions
- `enqueue(job) -> QrushResult<String>`: Enqueue a job immediately; returns the job ID
- `enqueue_in(job, delay_secs) -> QrushResult<String>`: Enqueue a job with a delay; returns the job ID
- `register_job(name, handler)`: Register a job handler
- `QueueConfig::initialize(redis_url, queues)`: Start worker pools **and** the cron scheduler
### Cron Scheduling
All under `qrush::cron::cron_scheduler::CronScheduler` (see [Cron Jobs](#cron-jobs)):
- `register_cron_job(job) -> Result<()>`: Persist a schedule to Redis
- `list_cron_jobs() -> Result<Vec<CronJobMeta>>`: List registered cron jobs
- `run_now(cron_id) -> Result<String>`: Enqueue a cron job immediately
- `toggle_cron_job(cron_id, enabled) -> Result<()>`: Pause / resume a schedule
- `delete_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 process
- `qrush::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`](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`):
```sh
# Actix β serves http://127.0.0.1:8080/qrush/metrics
cargo run --example actix_dashboard --features dashboard-actix
# Axum β serves http://127.0.0.1:8080/qrush/metrics
cargo run --example axum_dashboard --features dashboard-axum
```
### Cron Jobs
A cron job is a regular [`Job`](#basic-usage-integrated-mode) 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).
```rust
use qrush::job::Job;
use qrush::registry::register_job;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use futures::future::BoxFuture;
use anyhow::Result;
#[derive(Clone, Serialize, Deserialize)]
pub struct EmailJob {
pub to: String,
pub subject: String,
}
#[async_trait]
impl Job for EmailJob {
// π This is the execution β it runs every time the schedule fires.
async fn perform(&self) -> Result<()> {
println!("Sending email to {}: {}", self.to, self.subject);
// Your recurring work goes here.
Ok(())
}
fn name(&self) -> &'static str { "EmailJob" }
fn queue(&self) -> &'static str { "default" }
}
impl EmailJob {
pub fn name() -> &'static str { "EmailJob" }
// Lets a worker rebuild the job from its stored payload.
pub fn handler(payload: String) -> BoxFuture<'static, Result<Box<dyn Job>>> {
Box::pin(async move {
let job: EmailJob = serde_json::from_str(&payload)?;
Ok(Box::new(job) as Box<dyn Job>)
})
}
}
```
#### Step 2 β Add the schedule (`CronJob`)
Attach a cron expression and a unique id to the same type.
```rust
use qrush::cron::cron_job::CronJob;
#[async_trait]
impl CronJob for EmailJob {
// 6-field: sec min hour day month weekday. See "Cron Expressions" below.
fn cron_expression(&self) -> &'static str { "0 0 * * * *" } // every hour
fn cron_id(&self) -> &'static str { "hourly_email" } // must be unique
}
```
#### 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:
1. `set_redis_url(...)` β required before any Redis call.
2. `register_job(...)` β so a worker can run the job.
3. `CronScheduler::register_cron_job(...)` β saves the schedule to Redis.
4. `QueueConfig::initialize(...)` β starts the workers **and** the cron scheduler.
> β οΈ The cron scheduler only runs where `QueueConfig::initialize` is called. In
> [Separate Process Mode](#separate-process-mode-detailed) that's the engine
> binary, not the web server β put steps 2β4 there.
**Actix** (`features = ["dashboard-actix"]`):
```rust
use qrush::config::{set_redis_url, QueueConfig};
use qrush::registry::register_job;
use qrush::cron::cron_scheduler::CronScheduler;
use qrush::routes::metrics_route::qrush_metrics_routes;
use actix_web::{web, App, HttpServer};
#[actix_web::main]
async fn main() -> anyhow::Result<()> {
let redis_url = std::env::var("REDIS_URL")
.unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
set_redis_url(redis_url.clone())?; // 1
register_job(EmailJob::name(), EmailJob::handler); // 2
let job = EmailJob { // 3
to: "user@example.com".into(),
subject: "Hourly report".into(),
};
CronScheduler::register_cron_job(job).await?;
let queues = vec![QueueConfig::new("default", 5, 0)]; // 4
QueueConfig::initialize(redis_url, queues).await?;
// Serve the dashboard at http://127.0.0.1:8080/qrush/metrics
HttpServer::new(|| {
App::new().service(web::scope("/qrush").configure(qrush_metrics_routes))
})
.bind("0.0.0.0:8080")?
.run()
.await?;
Ok(())
}
```
**Axum** (`features = ["dashboard-axum"]`):
```rust
use qrush::config::{set_redis_url, QueueConfig};
use qrush::registry::register_job;
use qrush::cron::cron_scheduler::CronScheduler;
use qrush::routes::axum_route::qrush_metrics_router;
use axum::Router;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let redis_url = std::env::var("REDIS_URL")
.unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
set_redis_url(redis_url.clone())?; // 1
register_job(EmailJob::name(), EmailJob::handler); // 2
let job = EmailJob { // 3
to: "user@example.com".into(),
subject: "Hourly report".into(),
};
CronScheduler::register_cron_job(job).await?;
let queues = vec![QueueConfig::new("default", 5, 0)]; // 4
QueueConfig::initialize(redis_url, queues).await?;
// Serve the dashboard at http://127.0.0.1:8080/qrush/metrics
let app = Router::new().nest("/qrush", qrush_metrics_router());
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, app).await?;
Ok(())
}
```
**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_job` errors if a job with the same `cron_id`
> already 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 call
> `CronScheduler::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`:
```rust
use qrush::cron::cron_scheduler::CronScheduler;
CronScheduler::list_cron_jobs().await?; // -> Vec<CronJobMeta>
CronScheduler::run_now("hourly_email").await?; // enqueue once, right now
CronScheduler::toggle_cron_job("hourly_email", false).await?; // pause
CronScheduler::toggle_cron_job("hourly_email", true).await?; // resume
CronScheduler::delete_cron_job("hourly_email").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`:
```rust
fn enabled(&self) -> bool { false }
```
A disabled job stays registered but is skipped and removed from the run schedule
until re-enabled.
### Multiple Queues
```rust
let queues = vec![
QueueConfig::new("default", 5, 0), // 5 workers, priority 0
QueueConfig::new("critical", 10, 0), // 10 workers, priority 0
QueueConfig::new("low", 2, 1), // 2 workers, priority 1
];
```
### Metrics UI
> Requires a dashboard feature β `dashboard-actix` or `dashboard-axum` (not
> enabled by default). See [Feature Flags](#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
```bash
# Required
REDIS_URL=redis://127.0.0.1:6379
# Optional
QRUSH_BASIC_AUTH=admin:password # Basic auth for metrics
RUST_LOG=info,qrush=info # Logging level
```
# 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
```toml
[dependencies]
# Pick the dashboard framework you use: "dashboard-actix" or "dashboard-axum"
qrush = { version = "2.0.0", features = ["dashboard-actix"] }
actix-web = "4" # or: axum = "0.8"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
serde = { version = "1", features = ["derive"] }
async-trait = "0.1"
anyhow = "1"
futures = "0.3"
```
### 2. Define a Job
```rust
use qrush::job::Job;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use futures::future::BoxFuture;
use anyhow::Result;
#[derive(Clone, Serialize, Deserialize)]
pub struct NotifyUser {
pub user_id: String,
pub message: String,
}
#[async_trait]
impl Job for NotifyUser {
async fn perform(&self) -> Result<()> {
println!("Notify {} -> {}", self.user_id, self.message);
Ok(())
}
fn name(&self) -> &'static str { "NotifyUser" }
fn queue(&self) -> &'static str { "default" }
}
impl NotifyUser {
pub fn name() -> &'static str { "NotifyUser" }
pub fn handler(payload: String) -> BoxFuture<'static, Result<Box<dyn Job>>> {
Box::pin(async move {
let job: NotifyUser = serde_json::from_str(&payload)?;
Ok(Box::new(job) as Box<dyn Job>)
})
}
}
```
### 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"]`):
```rust
use qrush::config::{QueueConfig, set_redis_url};
use qrush::registry::register_job;
use qrush::routes::metrics_route::qrush_metrics_routes;
use actix_web::{web, App, HttpServer};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Set Redis URL
let redis_url = std::env::var("REDIS_URL")
.unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
set_redis_url(redis_url.clone())?;
// Register jobs
register_job(NotifyUser::name(), NotifyUser::handler);
// Initialize queues
let queues = vec![
QueueConfig::new("default", 5, 0),
];
QueueConfig::initialize(redis_url, queues).await?;
// Start web server
HttpServer::new(|| {
App::new()
.service(web::scope("/qrush").configure(qrush_metrics_routes))
})
.bind("0.0.0.0:8080")?
.run()
.await
}
```
**Axum** (`features = ["dashboard-axum"]`):
```rust
use qrush::config::{QueueConfig, set_redis_url};
use qrush::registry::register_job;
use qrush::routes::axum_route::qrush_metrics_router;
use axum::Router;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Set Redis URL
let redis_url = std::env::var("REDIS_URL")
.unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
set_redis_url(redis_url.clone())?;
// Register jobs
register_job(NotifyUser::name(), NotifyUser::handler);
// Initialize queues
let queues = vec![
QueueConfig::new("default", 5, 0),
];
QueueConfig::initialize(redis_url, queues).await?;
// Mount the dashboard under /qrush
let app = Router::new().nest("/qrush", qrush_metrics_router());
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, app).await?;
Ok(())
}
```
### 4. Enqueue Jobs
```rust
use qrush::queue::{enqueue, enqueue_in};
// Immediate
enqueue(NotifyUser {
user_id: "123".to_string(),
message: "Hello".to_string(),
}).await?;
// Delayed (300 seconds)
enqueue_in(NotifyUser {
user_id: "123".to_string(),
message: "Reminder".to_string(),
}, 300).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`:
```rust
use qrush::engine::{run_engine, parse_queues};
use qrush::config::set_redis_url;
use qrush::registry::register_job;
use qrush::cron::cron_scheduler::CronScheduler;
// Your job types
use your_app::jobs::NotifyUser;
#[tokio::main(flavor = "multi_thread")]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
// Setup logging
tracing_subscriber::fmt::init();
// Get Redis URL
let redis_url = std::env::var("REDIS_URL")
.unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
// Set Redis URL (needed before registering cron jobs)
set_redis_url(redis_url.clone())?;
// Register jobs
register_job(NotifyUser::name(), NotifyUser::handler);
// Register cron jobs (optional)
let daily_report = DailyReportJob { /* ... */ };
CronScheduler::register_cron_job(daily_report).await?;
// Parse queues: "name:concurrency:priority"
let queues = parse_queues("default:5:0,critical:10:0");
// Run engine
run_engine(redis_url, queues, 5).await
}
```
### 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:
```toml
[dependencies]
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
dotenvy = "0.15"
[[bin]]
name = "qrush_engine"
path = "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](#3-initialize-qrush).
**Actix** (`features = ["dashboard-actix"]`):
```rust
use qrush::config::set_redis_url;
use qrush::registry::register_job;
use qrush::routes::metrics_route::qrush_metrics_routes;
use actix_web::{web, App, HttpServer};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let redis_url = std::env::var("REDIS_URL")
.unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
set_redis_url(redis_url).expect("failed to set Redis URL");
// Register jobs (for serialization only β no worker initialization).
register_job(NotifyUser::name(), NotifyUser::handler);
HttpServer::new(|| {
App::new().service(web::scope("/qrush").configure(qrush_metrics_routes))
})
.bind("0.0.0.0:8080")?
.run()
.await
}
```
**Axum** (`features = ["dashboard-axum"]`):
```rust
use qrush::config::set_redis_url;
use qrush::registry::register_job;
use qrush::routes::axum_route::qrush_metrics_router;
use axum::Router;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let redis_url = std::env::var("REDIS_URL")
.unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
set_redis_url(redis_url)?;
// Register jobs (for serialization only β no worker initialization).
register_job(NotifyUser::name(), NotifyUser::handler);
let app = Router::new().nest("/qrush", qrush_metrics_router());
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, app).await?;
Ok(())
}
```
### 4. Run Both Processes
**Terminal 1 - Web Server:**
```bash
export REDIS_URL=redis://127.0.0.1:6379
cargo run
```
**Terminal 2 - Worker Engine:**
```bash
export REDIS_URL=redis://127.0.0.1:6379
cargo run --bin qrush_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:
| `"* * * * *"` | 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 value
- `a` β an exact value
- `a,b,c` β a list
- `a-b` β an inclusive range
- `*/n` β a step over the whole range (e.g. `*/15` in minutes)
- `a-b/n` β a step within a range
- **Names**: months `JAN`β`DEC`, weekdays `SUN`β`SAT` (case-insensitive).
For the weekday field, both `0` and `7` mean 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.
```rust
#[async_trait]
impl CronJob for EmailJob {
fn cron_expression(&self) -> &'static str { "0 0 9 * * *" } // 9 AMβ¦
fn cron_id(&self) -> &'static str { "morning_email" }
fn timezone(&self) -> &'static str { "Asia/Kolkata" } // β¦IST
}
```
**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
- `GET /qrush/metrics` - Dashboard overview
- `GET /qrush/metrics/queues/{queue}` - Queue details
- `GET /qrush/metrics/extras/cron` - Cron job management
- `GET /qrush/metrics/extras/workers` - Worker status
- `GET /qrush/metrics/health` - Health check
- `POST /qrush/metrics/jobs/action` - Job actions (retry/delete)
## Production Tips
- Use separate process mode for production
- Set `QRUSH_BASIC_AUTH` to protect metrics endpoints
- 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](LICENSE) file for details.
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## Support
- **Documentation**: [docs.rs/qrush](https://docs.rs/qrush)
- **Issues**: [GitHub Issues](https://github.com/srotas-space/qrush/issues)
- **Discussions**: [GitHub Discussions](https://github.com/srotas-space/qrush/discussions)
---
Made with β€οΈ by [Srotas Space](https://open-source.srotas.space)