Add a background worker
Goal: move slow or non-request-critical work (sending a report, calling a third-party API, resizing an image) out of the request path and into a background job.
Prerequisites
- A queue backend configured (Redis, Postgres, or SQLite) if you want jobs to survive a restart. If you haven’t decided yet, see Choose a queue backend. For local dev you can skip this — the default
BackgroundQueuemode with noqueue:config still works, it just won’t persist jobs (jobs are dropped with a logged error if no provider is populated). Many apps start withworkers.mode: BackgroundAsync, which needs no queue backend at all.
1. Generate the worker
cargo loco generate worker report_workerThis creates src/workers/report_worker.rs, adds pub mod report_worker; to src/workers/mod.rs, and injects a registration call into connect_workers in src/app.rs. It also generates a test stub under tests/workers/.
The generated struct is always named Worker (scoped inside its own workers::report_worker module), with an empty WorkerArgs struct for you to fill in:
use serde::{Deserialize, Serialize};use loco_rs::prelude::*;
pub struct Worker { pub ctx: AppContext,}
#[derive(Deserialize, Debug, Serialize)]pub struct WorkerArgs {}
#[async_trait]impl BackgroundWorker<WorkerArgs> for Worker { fn build(ctx: &AppContext) -> Self { Self { ctx: ctx.clone() } }
fn class_name() -> String { "ReportWorker".to_string() }
async fn perform(&self, _args: WorkerArgs) -> Result<()> { // TODO: your job logic goes here Ok(()) }}2. Add typed arguments and job logic
Fill in WorkerArgs with whatever data the job needs (it’s serialized into the queue, so keep it small and Serialize + Deserialize), then implement perform:
use loco_rs::prelude::*;use serde::{Deserialize, Serialize};
pub struct DownloadWorker { pub ctx: AppContext,}
#[derive(Deserialize, Debug, Serialize)]pub struct DownloadWorkerArgs { pub user_guid: String,}
#[async_trait]impl BackgroundWorker<DownloadWorkerArgs> for DownloadWorker { fn build(ctx: &AppContext) -> Self { Self { ctx: ctx.clone() } }
async fn perform(&self, args: DownloadWorkerArgs) -> Result<()> { // .. do the actual work, use self.ctx for DB/cache/etc .. println!("processing download for {}", args.user_guid); Ok(()) }}(This example mirrors examples/demo/src/workers/downloader.rs.)
3. Confirm it’s registered
The generator already injected this, but it’s worth knowing what it did — Hooks::connect_workers is where every worker is registered against the shared Queue:
#[async_trait]impl Hooks for App { // .. async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> { queue.register(DownloadWorker::build(ctx)).await?; Ok(()) } // ..}If you wrote the worker manually instead of generating it, add the queue.register(...) line yourself.
4. Enqueue a job
Call the trait’s perform_later from a controller, task, or another worker:
DownloadWorker::perform_later( &ctx, DownloadWorkerArgs { user_guid: "foo".to_string(), },).await?;perform_later returns Result<String> — the job id, not Result<()>. In BackgroundQueue mode the id is assigned by the queue provider; in ForegroundBlocking/BackgroundAsync mode (or when no provider is configured) Loco generates a fresh UUID so you always get a stable handle back:
let job_id: String = DownloadWorker::perform_later(&ctx, args).await?;If you need higher/lower priority for this particular job, use perform_later_with_priority instead — see Choose a queue backend for priority semantics shared across all three backends:
DownloadWorker::perform_later_with_priority(&ctx, args, Some(50)).await?;5. Run the worker process
How you run workers depends on workers.mode (see Choose a queue backend):
# BackgroundQueue mode: run a dedicated worker processcargo loco start --worker
# or run server + worker in the same processcargo loco start --server-and-workerBackgroundAsync and ForegroundBlocking modes don’t need a separate worker process — jobs run inside whichever process called perform_later.
Filtering by tags
Give a worker tags, then start a worker process that only picks up matching jobs:
fn tags() -> Vec<String> { vec!["download".to_string(), "network".to_string()]}cargo loco start --worker download,networkA worker started with no tags (cargo loco start --worker) only processes untagged jobs; --all and --server-and-worker don’t support tag filtering.
6. Verify
Test with ForegroundBlocking mode set in config/test.yaml, so perform_later runs synchronously and returns only once the job is done:
use loco_rs::testing::prelude::*;
#[tokio::test]#[serial]async fn test_run_download_worker() { let boot = boot_test::<App, Migrator>().await.unwrap();
assert!( DownloadWorker::perform_later( &boot.app_context, DownloadWorkerArgs { user_guid: "foo".to_string() } ) .await .is_ok() );
// .. assert side effects here ..}Put worker tests under tests/workers/ — the generator does this for you automatically.
Reference
- Every
queue:/workers:YAML key: Configuration reference cargo loco start/jobsflags: CLI referenceworker/worker_redisfeature flags: Feature flags reference