Skip to content

Task Options & Uniqueness

Every @task declaration sets default scheduling, timeout, thread-offloading, and deduplication behavior. Callers can override scheduling and identity options per invocation in QueueService.enqueue(...).

Task State Machine & Retry Budget Accounting Tasks transition from pending or scheduled to running, and settle into completed, failed, cancelled, or expired while separating retry_count from infrastructure interruptions. pending scheduled_at == None Ready for immediate claim scheduled scheduled_at > now Delayed or backoff retry running (Fenced Lease) attempts = retry_count - interruptions Active heartbeat_at lease refresh Exception & attempts < max_retries -> scheduled Stale lease reclaim -> retry_count++, interruptions++ completed Result stored • completed_at set failed Retries exhausted • error recorded cancelled cancel_task() • cooperative abort expired expires_at passed before claim
Independent Task Clocks Comparison of expires_in/expires_at (queued deadline), timeout (per-attempt execution ceiling), and terminal_ttl/terminal_retention (post-settlement storage cleanup). Queue Expiration Clock expires_in / expires_at Ticks while pending or scheduled Stops the moment a worker claims task Never interrupts a running task Outcome: status = "expired" Execution Timeout Clock timeout=30.0 | timedelta(...) Ticks per execution attempt (`started_at`) Enforced via asyncio deadline scope Consumes retry budget if max_retries > 0 Outcome: retry or status = "failed" Terminal Retention Clock terminal_ttl / terminal_retention Ticks after completed/failed/cancelled/expired Native Redis/Valkey PEXPIRE (`terminal_ttl`) Maintenance Phase (`cleanup_terminal`) Preserves unique_until="forever"
Uniqueness Windows & Permanent TaskReservation Comparison of unique_until='terminal', duplicate policies ('return_existing' vs 'raise'), and unique_until='forever' backed by TaskReservation. In-Flight Deduplication unique_until="terminal" Active while pending, scheduled, or running Duplicate enqueue returns existing task_id Releases lock as soon as task settles Ideal for coalescing webhook bursts Duplicate Conflict Policies on_duplicate="return_existing" | "raise" "return_existing" returns active TaskResult "raise" raises DuplicateTaskError Effective key hashed from args or unique_key Inspect via get_task_identity() Permanent Reservation unique_until="forever" Backed by compact TaskReservation record Survives terminal_ttl / cleanup_terminal Failed/cancelled/expired releases lock to retry Reset via reset_task_identity()

Runnable Example

docs/examples/learn/task_options_and_uniqueness.py
"""Configuring task priority, delays, expiration, timeouts, and uniqueness reservations."""

from datetime import timedelta

from litestar_queues import QueueConfig, QueueService, WorkerConfig, task


@task(
    "billing.charge_invoice",
    queue="billing",
    priority=10,
    timeout=30.0,
    expires_in=timedelta(minutes=15),
    unique_by="arguments",
    unique_until="forever",
)
async def charge_invoice(invoice_id: str, amount_cents: int) -> dict[str, object]:
    """Charge an invoice at most once per unique argument signature."""
    return {"invoice_id": invoice_id, "amount_cents": amount_cents, "charged": True}


async def main() -> None:
    """Verify argument-based forever uniqueness and explicit reservation reset."""
    config = QueueConfig(
        queue_backend="memory", execution_backend="immediate", worker=WorkerConfig(placement="external")
    )
    async with QueueService(config=config) as service:
        first = await service.enqueue(charge_invoice, "inv-1001", 2500)
        duplicate = await service.enqueue(charge_invoice, "inv-1001", 2500)
        assert first.id == duplicate.id

        await first.refresh()
        assert first.record is not None
        assert first.record.key is not None

        reservation = await service.get_task_identity(first.record.key)
        assert reservation is not None
        assert reservation.task_id == first.id

        was_reset = await service.reset_task_identity(first.record.key)
        assert was_reset is True

        requeued = await service.enqueue(charge_invoice, "inv-1001", 2500)
        assert requeued.id != first.id

Priority & Delayed Execution

  • Priority (priority: int = 0): Higher integers are claimed before lower integers within the same due window (priority=10 runs before priority=0).
  • Relative Delay (run_after: float | timedelta | None): Defers initial eligibility by adding the duration to the enqueue timestamp and storing the record with status="scheduled".
  • Absolute Schedule (scheduled_at: datetime | None): Passed at enqueue(..., scheduled_at=...) to schedule a task for a specific UTC timestamp. Naive datetime objects are interpreted as UTC.

Lifecycle Clocks

litestar-queues separates queue waiting deadlines, execution attempt limits, and post-terminal storage retention into independent clocks:

Clock Configuration Active State Expiration Outcome
Queue Expiration Deadline expires_in / expires_at on @task or enqueue() pending or scheduled (before claim) Transitions unstarted task to expired and emits task.expired
Attempt Execution Timeout timeout on @task or enqueue() running (wraps dependency provider + task body) Cancels running coroutine with TimeoutError and applies retry policy
Terminal Retention TTL terminal_ttl on backend config / WorkerConfig.terminal_retention completed, failed, cancelled, expired Purges finished QueuedTaskRecord rows without touching TaskReservation

Expiration Evaluates Relative to Scheduled Time

When both run_after (or scheduled_at) and expires_in are set, expires_at is computed as scheduled_at + expires_in, giving a delayed task its full expiration window once it becomes due.

Synchronous Callables & Thread Offloading

litestar-queues runs on an asyncio event loop. Synchronous def task functions are offloaded to a managed ThreadPoolExecutor (QueueConfig.sync_thread_pool_size, defaulting to min(32, effective_cpu_count + 4)).

Function Kind sync_to_thread Setting Runtime Behavior
async def None or False Awaited directly on the worker event loop
async def True Rejected with QueueConfigurationError at registration
def (sync) None (default) Offloaded to the QueueService thread pool and emits QueueWarning recommending explicit sync_to_thread=True
def (sync) True Offloaded to the QueueService thread pool without warning
def (sync) False Executed inline on the event loop thread (use only for fast, non-blocking CPU work)

Task Identity & Deduplication

Deduplication prevents duplicate queue records from piling up when producers submit redundant work.

Identity Precedence Order

When QueueService.enqueue() resolves the effective identity key stored on QueuedTaskRecord.key, it evaluates sources in strict precedence order:

Precedence Source Key Format Description
Highest enqueue(..., key="...") Caller string Explicit per-call deduplication key
Second @task(..., key="...") Configured string Static key declared on the task definition
Third @task(..., unique_by="task") lq:u:v1:task:<sha256> At most one active instance of the task name regardless of arguments
Fourth @task(..., unique_by="arguments") lq:u:v1:args:<sha256> Canonical JSON hash of (task_name, bound_args, bound_kwargs)
Lowest None configured None Every enqueue() creates a distinct QueuedTaskRecord

When unique_by="arguments" is active, litestar-queues binds positional and keyword arguments against the callable's inspect.Signature, applies default parameter values, normalizes collections, and hashes the serialized payload. Payloads exceeding QueueConfig.max_argument_identity_bytes (default 65536 bytes) raise TaskIdentityTooLargeError.

Identity Lifetime: Terminal vs Forever

unique_until controls how long an identity key blocks subsequent enqueue() calls:

  • unique_until="terminal" (default): Blocks duplicate enqueues only while the existing record is non-terminal (pending, scheduled, or running). Matching enqueue() calls return a TaskResult pointing to the active record. Once the record reaches completed, failed, cancelled, or expired, the identity is automatically released.
  • unique_until="forever": Persists a lightweight TaskReservation (key, task_id, task_name, created_at) that survives terminal completion and routine cleanup_completed / terminal_ttl purges. Subsequent enqueues with the same identity return the existing reservation's task handle until an operator explicitly resets the key.

Inspecting and Resetting Forever Reservations

Use QueueService.get_task_identity(key) and QueueService.reset_task_identity(key) with the exact effective key (record.key) to inspect or clear a permanent reservation:

reservation = await queue_service.get_task_identity(record.key)
if reservation is not None:
    await queue_service.reset_task_identity(reservation.key)