Skip to content

Jobs

A job is any type that is Serialize + DeserializeOwned + Send + Sync + 'static and derives Job. The derive binds it to one variant of one queue set, and that binding is what makes the rest of the API type-safe.

#[derive(Debug, Serialize, Deserialize, Job)]
#[job(queue = AppQueues::Emails, retry(max_attempts = 5))]
struct SendEmail {
to: String,
body: String,
}
Key Default Meaning
queue = AppQueues::Emails required The variant this job lives on. Everything before the last segment is the queue set type
name = "emails.send" module_path!() + "::" + type name The job type name carried in every envelope and used to route to a handler
retry(...) inherit from the queue Retry policy override for this job type
crate = "path" auto Same as on #[queues]; normally unnecessary

The queue path is doing double duty: its last segment becomes the constant Job::QUEUE, and everything before it becomes the associated type Job::Queue. That is why it needs at least two segments — #[job(queue = Emails)] alone cannot say which queue set Emails belongs to, and is a compile error.

Full ranges and error messages are in Job attributes.

pub trait Job: Serialize + DeserializeOwned + Send + Sync + 'static {
type Queue: QueueSet;
const NAME: &'static str;
const QUEUE: Self::Queue;
fn retry_policy() -> Option<RetryPolicy> { None }
}

NAME is the string that travels in every envelope’s job_type field and is the key a worker routes on. It defaults to the fully qualified type name, so two identically named structs in different modules do not collide.

retry_policy() returns None unless the job has its own retry(...), and None means “use whatever the queue says”. See Retries and backoff for the precedence rules.

Producer<Q, _> and Worker<Q, _> are generic over the queue set, and every method that takes a job requires J: Job<Queue = Q>. A job from another application’s queue set is therefore a type error rather than a message that quietly lands on the wrong queue:

producer.enqueue(&BuildReport { id: 1 }).await?; // BuildReport belongs to OtherQueues
error[E0271]: type mismatch resolving `<BuildReport as Job>::Queue == AppQueues`
--> tests/compile_fail/enqueue_foreign_job.rs:35:22
|
35 | producer.enqueue(&BuildReport { id: 1 }).await.unwrap();
| ------- ^^^^^^^^^^^^^^^^^^^^^^ type mismatch resolving `<BuildReport as Job>::Queue == AppQueues`
| |
| required by a bound introduced by this call
|
note: expected this to be `AppQueues`
--> tests/compile_fail/enqueue_foreign_job.rs:25:15
|
25 | #[job(queue = OtherQueues::Reports)]
| ^^^^^^^^^^^
note: required by a bound in `queuey::Producer::<Q, B>::enqueue`
|
| pub async fn enqueue<J: Job<Queue = Q>>(&self, job: &J) -> Result<uuid::Uuid> {
| ^^^^^^^^^ required by this bound in `Producer::<Q, B>::enqueue`

The same bound rejects registering a foreign handler on a worker, and rejects wrapping a foreign job in FnHandler. All three cases are covered by compile-fail tests in the repository.

The payload is serialized to JSON and carried in the envelope’s payload field, so anything serde can represent works: nested structs, enums, Option, collections, Uuid, chrono types with their serde features on.

Two practical constraints:

Keep payloads small and stable. A message may sit in a queue across a deploy, so a payload has to be decodable by the next version of your code. A field added with #[serde(default)] is safe; a field removed or retyped is not. A body that no longer decodes is dead-lettered with decode error: ... rather than retried, because retrying it would never succeed.

Reference, do not embed. Put an id in the job and load the row in the handler. That keeps the queue small and means the handler always works on current data rather than on a snapshot from whenever the job was enqueued.

Nothing requires a job to be a struct. Any serde-able type works, including an enum, which is a compact way to model a small family of related work on one queue:

#[derive(Debug, Serialize, Deserialize, Job)]
#[job(queue = AppQueues::Emails)]
enum Notify {
Welcome { user_id: u64 },
PasswordReset { user_id: u64, token: String },
}

One handler then receives all variants. Generic job types are not supported: #[derive(Job)] on a type with type parameters is a compile error, because NAME and QUEUE are constants and could not be monomorphised into a single wire identity.