queue

Tasks

Enqueue options, the event envelope, OTel trace propagation across the queue, and task-type naming conventions.

The Client and its Enqueue method are enough to submit a raw *asynq.Task, but every layer built on queue, from the events module's listener fan-out to the scheduler's cron entries, shares three more things: a common set of enqueue options, a common envelope format for event payloads, and a common task-type naming scheme. This page covers all three; day to day you'll meet them again, unchanged, wherever queue is used underneath.

Enqueue options

queue re-exports the useful subset of asynq's options so common cases don't require importing asynq:

OptionEffect
OnQueue(name)routes to a named queue (matched against the worker's WORKER_QUEUES weights); unset means default
MaxRetry(n)how many retries before a failing task is archived
Timeout(d)bounds a single execution attempt
Deadline(t)absolute deadline across all attempts
ProcessIn(d)delays processing by d
Unique(ttl)suppresses an identical task (same type + payload) while a previous one is pending
Retention(d)keeps the completed task in Valkey for d, for inspection
TaskID(id)explicit task ID; a conflict surfaces as ErrDuplicate (the basis of idempotent delivery)

The options look the same everywhere a task is born: at Enqueue, at the events.Listen listener registration, and on scheduler entries. The shop's mail queue, for example, appears in the listener registration:

events.Listen(reg, "sendOrderConfirmation", sendOrderConfirmation(deps), queue.OnQueue("mail"))

When you need the full asynq option set, queue.AsynqOptions(opts...) converts these into raw []asynq.Option, and asynq's own API remains open alongside Enqueue: an escape hatch, not a forbidden zone.

The Envelope

Every event task payload is the same wire format, queue.Envelope:

type Envelope struct {
    ID         string            `json:"id"`         // dispatch id, doubles as the asynq TaskID
    Name       string            `json:"name"`       // the event name (EventName())
    Payload    json.RawMessage   `json:"payload"`    // the JSON-encoded event value
    Metadata   map[string]string `json:"metadata,omitempty"` // W3C trace context
    OccurredAt time.Time         `json:"occurred_at"`
}

The envelope travels unchanged from the dispatcher (or the outbox) through the fan-out task to every listener task: the listener sees the same ID, payload and trace context the producer set. env.Task() renders it as the fan-out task; queue.DecodeEnvelope(task) unpacks it on the other side.

Trace propagation across the queue

env.InjectTrace(ctx) writes the context's trace context into Metadata in W3C form (traceparent/tracestate); on the worker side, env.ExtractTrace(ctx) continues the same trace. That makes the HTTP request → outbox → relay → worker → listener path a single trace, in the shop from placing the order to the confirmation e-mail. The dispatchers and the worker middleware do this automatically; see the OpenTelemetry page for details.

Task types

Four task-type prefixes cover every task that flows through queue in the async category, and the helpers below are the single source of the wire names:

HelperTask typeCreated by
EventTaskType(name)event:shop.orderPlaceddispatch / outbox relay (the starting point of the fan-out)
ListenerTaskType(event, listener)listener:shop.orderPlaced:sendOrderConfirmationthe worker's fan-out handler, one per listener
ScheduleTaskType(name)schedule:shop.someEventthe scheduler, when firing a scheduled event
JobTaskType(name)job:shop.nightlySalesReportthe scheduler, when firing a scheduled job

The kit's worker routes tasks to its four built-in handlers by these prefixes; the IsEventTaskType/IsListenerTaskType/IsJobTaskType/IsScheduleTaskType and JobName helpers give the same classification to anyone working with raw tasks (say, in a handler registered via WithMux). These are also the names you see in Asynqmon and in the worker's logs: you can tell at a glance which task is which listener of which event. The events module and the worker chassis are what actually produce and consume tasks under these names; this page only defines the naming, not the fan-out itself.

When to reach for it directly

  • Your own, non-event task: build an asynq.NewTask(...), submit it with client.Enqueue, and register a handler for it in the worker with WithMux.
  • An idempotent "act exactly once" enqueue: TaskID + ErrDuplicate handling, as on the Overview page.
  • Everything else (event dispatch, scheduling) belongs to the higher layers: start at events.
Copyright © 2026