> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useduro.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Queues & Workers

> Five BullMQ queues, four repeatable scanners, one consistent scan/process pattern — the asynchronous backbone.

Everything that happens without a caller happens here. The `billing-worker` is a single Node process hosting a set of BullMQ workers and repeatable jobs, all over the shared Redis.

## The queue topology

```mermaid theme={null}
flowchart TB
    subgraph repeat["Repeatable jobs (cron-like)"]
        BSCAN["billing scan · 60s"]
        RSCAN["reminder scan · 60m"]
        DSCAN["dunning scan · 60s"]
        WSCAN["webhook scan · 15s"]
    end

    subgraph queues["Queues"]
        BILLING["BILLING<br/>scan · reminder-scan · renew · remind"]
        DUNNING["DUNNING<br/>scan · process"]
        WEBHOOK["WEBHOOK_DELIVERY<br/>scan · deliver"]
        EMAIL["EMAIL<br/>send"]
        RECON["RECONCILE<br/>(payouts — July)"]
    end

    BSCAN & RSCAN --> BILLING
    DSCAN --> DUNNING
    WSCAN --> WEBHOOK
    BILLING -.receipts, reminders.-> EMAIL
    DUNNING -.dunning, recovery.-> EMAIL
    EMAIL --> SMTP["SMTP"]
```

## One pattern, everywhere

Every async concern in Duro follows the same **scan → enqueue → process** shape. Learn it once and you understand the billing engine, the recovery engine, and webhook delivery:

```mermaid theme={null}
flowchart LR
    SCAN["repeatable 'scan'<br/>cross-tenant, both modes<br/>cheap query, no work"] --> ENQ["enqueue one job<br/>per item<br/>jobId = dedup key"]
    ENQ --> PROC["per-item processor<br/>builds RepositoryContext,<br/>does the real work"]
```

Why this shape, every time:

* **The scan is cheap and short.** It selects ids and enqueues. It never holds a transaction open while charging a gateway.
* **The work is isolated and retryable.** Each item is its own job — it can fail, back off, and retry without affecting its neighbours. BullMQ's `attempts` + exponential backoff handle transient failures for free.
* **`jobId` deduplicates.** If a scan fires before the previous item finished, the duplicate collapses. Idempotency at the queue layer.
* **Both modes, always.** A scanner has no "current mode," so it sweeps `test` and `live`. This is the *only* place that loops both — request-scoped code is always single-mode.

## The scanners

| Scanner  | Interval | Finds                                                  | Enqueues  |
| -------- | -------- | ------------------------------------------------------ | --------- |
| billing  | 60s      | `active`/`trialing` subs with `currentPeriodEnd ≤ now` | `renew`   |
| reminder | 60m      | `active` subs renewing within 60 days                  | `remind`  |
| dunning  | 60s      | `scheduled` schedules with `nextAttemptAt ≤ now`       | `process` |
| webhook  | 15s      | undispatched events + due deliveries                   | `deliver` |

The reminder scan runs hourly rather than every minute — renewal reminders aren't time-critical to the second, and a 60-minute cadence keeps the re-evaluation churn low. The webhook scan runs fastest (15s) because delivery latency is the thing merchants feel.

## Email as a queue, not an await

Sending email is IO that can be slow and flaky, so it's never awaited in a request. Producers enqueue an `EmailJobData` and return; the dedicated email worker drains the queue and talks to SMTP.

```mermaid theme={null}
sequenceDiagram
    participant P as Producer (any service)
    participant Q as EMAIL queue (Redis)
    participant W as Email worker
    participant S as SMTP
    P->>Q: enqueue { to, subject, html, text }
    Note over P: returns immediately (no await on send)
    Q->>W: deliver job
    W->>S: send
```

The welcome/KYC emails (core-api), receipts and dunning reminders (worker), and recovery notices all flow through this one queue. [See the email system →](/platform/emails)

## Graceful shutdown

On `SIGTERM`/`SIGINT` the worker closes the HTTP health server, then awaits every BullMQ worker and queue close, disconnects Redis and Postgres, and exits — with a 25-second hard backstop so a hung connection can't wedge a deploy.

Next: the [email & template system](/platform/emails).
