# fang: background task processing for Rust with a database as the queue

> fang is a Rust library that turns PostgreSQL, SQLite or MySQL into an asynchronous task queue, with scheduled and cron tasks, uniqueness, per-type workers and retries with custom backoff. You pick the backend through Cargo features, define tasks against the Runnable or AsyncRunnable trait, and run the schema migrations yourself.

**ayrat555/fang** — Background processing for Rust

- Repository: https://github.com/ayrat555/fang
- Website: https://fang.badykov.com/
- Stars: 724 · Forks: 32
- Language: Rust
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/ayrat555-fang

## A Rust job queue that lives in the database you already run

fang is a background task processing library for Rust that uses PostgreSQL, SQLite or MySQL as an asynchronous task queue. The documentation site is at fang.badykov.com, and the crate is published on crates.io under the name fang.

The design choice that defines the library is that the queue is a table in a database you already operate, not a separate broker process. There is no new server to deploy and no new failure mode, and the README frames the feature set around what you can do with a task sitting in a row: run it later at a chosen moment, run it on a schedule, make it unique, restrict which workers pick it up, and retry it with a backoff.

The audience is Rust web services and workers that want background work without hand-rolling a job table. The six features the README lists are async and threaded workers, tasks scheduled at any time in the future, periodic tasks driven by cron expressions, unique tasks that are not duplicated in the queue, single-purpose workers that execute only tasks of a specific type, and retries with a custom backoff mode.

Note the word library rather than service. fang gives you the task model, the storage and the worker loop, but the scheduling decisions and the retry policy are yours to write in the trait methods, which is where most of the real behaviour ends up.

## Cargo features pick the backend and the execution model

The crate is configured entirely through Cargo features, and the README gives a separate dependency snippet for each choice. The blocking feature covers synchronous work.

```toml
[dependencies]
fang = { version = "0.11.0" , features = ["blocking"], default-features = false }
```

The asynk feature covers tokio-based asynchronous work, and it comes in one variant per supported database.

```toml
[dependencies]
fang = { version = "0.11.0" , features = ["asynk-postgres"], default-features = false }
```

The same snippet shape works for the SQLite and MySQL variants, named asynk-sqlite and asynk-mysql. There is also a derive macro variant, where the database name is substituted into the feature list.

```toml
[dependencies]
fang = { version = "0.11.0" , features = ["asynk-{database}", "derive-error" ], default-features = false }
```

Or you can take everything at once.

```toml
fang = { version = "0.11.0" }
```

The pattern of default-features = false with an explicit feature list is worth noting, because it means the queue backend and the execution model are decided at compile time rather than at runtime. A service that wants a SQLite queue in tests and PostgreSQL in production carries both feature sets, and the README states that the crate supports rustc 1.77 and newer.

## The fang_tasks table and the migrations you have to apply

fang stores everything in a single table called fang_tasks, and the README asks you to create it before running workers. The migration for each database is published in the crate under fang/{database}-migrations, where database is postgres, mysql or sqlite.

Migrations can also be run as code rather than as separate files, by importing the feature that matches your backend, migrations-{database}.

```toml
[dependencies]
fang = { version = "0.11.0" , features = ["asynk-postgres", "migrations-postgres" ], default-features = false }
```

With that feature in place the migration function is available in code.

```rust
use fang::run_migrations_postgres;
run_migrations_postgres(&mut connection).unwrap();
```

The two approaches suit different situations. Files in the migrations directory fit a project that already has a migration tool and wants the schema change reviewed like any other, while running migrations as code keeps the schema definition inside the dependency and avoids a separate step before the first deploy.

Either way this is an operational responsibility that does not disappear. The table has to exist before a worker starts polling, and the README does not describe a fallback for a missing or mismatched schema, so the migration belongs in your normal deploy sequence rather than in a manual step.

## Every task implements Runnable, and the trait methods are the policy

Under the blocking feature, each task implements the fang::Runnable trait, which is what fang uses to execute it. The trait carries more than a run method, and those extra methods are where the queue behaviour is configured.

The run method receives a reference to a dyn Queueable, described in the README as a struct you can reuse to manipulate the task queue, for example to add a new job while the current job is running. That is how a task spawns follow-up work without reaching for a global client.

```rust
    fn cron(&self) -> Option<Scheduled> {
        let expression = "0/20 * * * Aug-Sep * 2022/1";
        Some(Scheduled::CronPattern(expression.to_string()))
    }

    // the maximum number of retries. Set it to 0 to make it not retriable
    // the default value is 20
    fn max_retries(&self) -> i32 {
        20
    }

    // backoff mode for retries
    fn backoff(&self, attempt: u32) -> u32 {
        u32::pow(2, attempt)
    }
```

Those defaults matter. A task that does not override max_retries inherits 20 attempts, and a task that does not override cron returns None, meaning it is not scheduled and runs as soon as it is inserted. A task that returns true from uniq is not inserted twice, and the existing record is returned for any insertion operation. task_type defaults to common, which is how single-purpose workers filter the queue.

For error handling, the README recommends implementing From<FangError> for a custom error type so the ? operator works inside run, and the ToFangError macro can generate that, though the macro requires the derive-error feature.

## AsyncRunnable mirrors Runnable for tokio, with a naming trap

The asynk feature has its own trait, fang::AsyncRunnable, and the same set of policy methods. The run method is async and receives a mutable reference to a dyn AsyncQueueable, and the implementation is annotated with async_trait as well as the typetag attribute that the blocking version also needs.

```rust
use fang::AsyncRunnable;
use fang::asynk::async_queue::AsyncQueueable;
use fang::serde::{Deserialize, Serialize};
use fang::async_trait;
```

The typetag attribute is what makes a task deserialisable from the stored row, and the README notes that two implementations of the AsyncRunnable trait with the same name will cause a failure in the typetag crate. That is a sharp edge: the name of your task type has to be unique across the whole set of async tasks in the process, and the failure appears in the typetag layer rather than in fang itself.

The same applies to the trait method names, which are the same in both worlds: uniq, task_type, cron, max_retries and backoff, with the defaults described in the README, namely a task type of common, no schedule, twenty retries, and an exponential backoff written as u32::pow(2, attempt).

That last default is worth a second look in production. Two to the power of the attempt number grows quickly, so a task that fails on its first attempt and then keeps failing will be retried with gaps measured in hours by the twentieth attempt, unless you override backoff.

## One table, many worker types, and the Makefile behind the tests

The single-table design has a visible consequence in the feature list. Tasks are stored in one table, but workers can execute only tasks of the specific type they are interested in, which is what the README calls single-purpose workers. task_type is the filter, and the default value common is the bucket for everything that does not set one.

The repository shows how the project is developed. The Cargo.toml defines a workspace whose members are the fang crate, the fang-derive-error crate, its example, and four example programs covering simple and cron workers in both the blocking and asynk models.

```toml
[workspace]
members = [
  "fang",
  "fang-derive-error",
  "fang-derive-error/example",
  "fang/fang_examples/asynk/simple_async_worker",
  "fang/fang_examples/asynk/simple_cron_async_worker",
  "fang/fang_examples/blocking/simple_cron_worker",
  "fang/fang_examples/blocking/simple_worker"
]
resolver = "2"
```

The Makefile drives the test environment through containers, with one target per database.

```bash
DB_TARGETS=db_postgres db_mysql db_sqlite
```

The Postgres target starts a container on port 5432 and then runs the Diesel migrations against it, the MySQL target does the same on port 3306, and the SQLite target operates on a local file. The examples in the workspace are the fastest way to see a worker running against a real backend.

## Version 0.11.0 arrived after a long gap, on a 0.x line

The release history is worth reading closely. The current release is 0.11.0, published on 2026-07-02, and the last push to the repository carries the same date, so the source tree and the tagged release agree. Before that, 0.11.0-rc0 was published on 2024-04-21, and 0.10.4.1 on 2023-08-22.

That is a two-year gap between the release candidate and the final release. It tells you the project was slow to finish this line, which matters more for a library that owns your job queue than for a utility, because an unfinished 0.x release means the API you pin may still be shaped by a maintainer with time to change it.

The 0.x version number is the other signal. While the crate stays below 1.0, Cargo treats minor version bumps as breaking changes, so you should expect to read the changelog before every upgrade rather than bumping on autopilot. The changelog file is present in the repository, and there is a LICENSE file under the name LICENCE, with the project MIT licensed.

The documentation lives on a separate site rather than in the repository, and the README's usage section is truncated in the version you can read, so the hosted docs are the reference for anything beyond task definition.

## Conclusion

fang fits Rust services that need to move work out of the request path without introducing a separate broker, especially teams already running one of the three supported databases and willing to treat a table as the queue. It does not fit a codebase that cannot accept database-backed queues, that needs a broker with delivery guarantees the table approach does not model, or that wants the 0.x API frozen, since the crate is still on a 0.11 release line. Before adopting it, read the trait methods you will override, uniq, task_type, cron, max_retries and backoff, since their defaults define the behaviour your workers inherit, and run the migrations for your chosen backend from the fang directory before the first worker starts.

## FAQ

### Which databases can fang use as a task queue?

PostgreSQL, SQLite and MySQL, each selected through a Cargo feature named asynk-postgres, asynk-sqlite or asynk-mysql. The blocking feature runs tasks in threads rather than as tokio tasks.

### How do I create the fang_tasks table in my database?

The README says to create the fang_tasks table, with the migration for each database published in the crate under fang/{database}-migrations. Migrations can also be run as code by enabling the migrations-{database} feature and calling the corresponding run_migrations function.

### How do I stop a task from being queued twice in fang?

Implement uniq to return true. The README states that a task is not inserted again if it is already in storage, and that the existing record is returned for any insertion operation.

### How do retries and backoff work in fang?

The max_retries method sets the maximum number of retries, with a default of 20, and setting it to 0 makes the task not retriable. The backoff method receives the attempt number and returns the wait, with the README example using u32::pow(2, attempt).

### What is the minimum Rust version for fang?

The README states that fang supports rustc 1.77 and newer. The crate is published as version 0.11.0 and requires Cargo feature selection, with default-features = false when you pick a specific backend.

### Can a fang task enqueue another task while it runs?

Yes. The run method receives a queue handle, described in the README as a struct that implements fang::Queueable, which you can reuse to add a new job during the current job's execution.

## Sources

- [ayrat555/fang on GitHub](https://github.com/ayrat555/fang)
- [License: MIT](https://github.com/ayrat555/fang/blob/master/LICENSE)
- [Project website](https://fang.badykov.com/)
- [README](https://github.com/ayrat555/fang/blob/master/README.md)
- [Releases](https://github.com/ayrat555/fang/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ayrat555-fang
