Skip to content
Back to skills

Async Jobs

ASecurity

Async jobs — idempotency, retry strategy, DLQ, observability.

  • 6 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 5, 2026
developmenttypescriptpythongophpnodeawsazuredatabase

Security analysis

A100/100

Scanned September 5, 2026

npx -y skills add Dev-Toolbelt/dev-team-agents --skill async-jobs --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Async Jobs?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Async Jobs
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/dev-toolbelt-async-jobs/badge)](https://www.skillsdirectory.com/skills/dev-toolbelt-async-jobs)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: async-jobs
description: Async jobs — idempotency, retry strategy, DLQ, observability.
---

## Detection Signals

- `queue`, `worker`, `job`, or `consumer` directories or files in the project
- Dependencies: `laravel/horizon`, `sidekiq`, `celery`, `bullmq`, `bee-queue`, `faktory`, `resque`, `delayed_job`
- Cloud queue services: `aws-sdk` with SQS usage, `@google-cloud/pubsub`, Azure Service Bus, RabbitMQ, Kafka
- `QUEUE_*` / `REDIS_QUEUE_*` / `SQS_*` env vars
- Cron/scheduler configs: `app/Console/Kernel.php`, `celerybeat`, `node-cron`, `whenever` gem

---

## When to Use a Job vs. Synchronous Processing

Use a background job when:
- The operation takes longer than the acceptable HTTP response time (~200ms threshold for user-facing actions)
- The operation involves external services that can be slow or unavailable (email, SMS, payment processor, webhooks)
- The operation can be retried independently if it fails (idempotent by nature or can be made so)
- Fan-out: one event triggers multiple independent downstream actions

Keep it synchronous when:
- The user needs the result to proceed (e.g., payment status before showing confirmation)
- The operation is fast and its failure should block the response (e.g., saving the primary record)
- Ordering guarantee is critical and queues add non-determinism

---

## Idempotency — Most Critical Rule

**Every job must be safe to run more than once with the same payload.**

Queues guarantee at-least-once delivery — a job will be retried on failure and may execute multiple times. A non-idempotent job causes duplicate records, double charges, or double notifications.

```python
# ❌ Non-idempotent — creates a duplicate on retry
def send_welcome_email(user_id: str):
    user = User.find(user_id)
    Email.send(to=user.email, template="welcome")

# ✅ Idempotent — checks state before acting
def send_welcome_email(user_id: str):
    user = User.find(user_id)
    if user.welcome_email_sent_at is not None:
        return  # already sent — safe to skip
    Email.send(to=user.email, template="welcome")
    user.update(welcome_email_sent_at=now())
```

**Idempotency patterns:**
- **State check**: read current state before acting; bail out if the action was already completed
- **Unique constraint**: use a database unique key on `(job_type, entity_id)` to prevent duplicate execution
- **Idempotency key**: pass a UUID with the job payload; store it as processed after first execution

---

## Payload Validation

Treat job payloads with the same rigor as HTTP input — validate before processing:

```ts
// ✅ Validate payload shape before using it
async handle(payload: unknown) {
  const { orderId, userId } = validateOrderPayload(payload); // throws on invalid
  const order = await Order.findOrFail(orderId);
  // ...
}
```

**Rules:**
- Never assume payload fields are present and correctly typed — they may come from an old version of the job or be corrupted
- Validate required fields and types at the start of the handler, before any side effects
- If validation fails: fail permanently (do not retry) — retrying a malformed payload wastes queue capacity

---

## Retry Strategy

| Failure type | Action |
|---|---|
| Transient (network timeout, 503) | Retry with exponential backoff |
| Business rule failure (record not found yet) | Retry with delay |
| Validation failure (malformed payload) | Fail permanently — do not retry |
| Unexpected exception | Retry up to max attempts, then DLQ |

**Exponential backoff formula**: `base_delay * 2^attempt` with jitter — prevents thundering herd when many jobs fail simultaneously.

**Minimum retry baseline** — unless the project defines otherwise: **3 attempts with exponential backoff starting at 2 s** (`2s → 4s → 8s`), then DLQ.

Configure per-job retry limits (not global defaults) — a payment job and an analytics job have different tolerance for retries. A payment job warrants fewer attempts and faster DLQ escalation than an analytics event.

---

## Dead Letter Queue (DLQ)

Every queue must have a DLQ configured. Jobs that exhaust retries move there instead of being silently dropped.

**After moving to DLQ:**
1. Alert the on-call team (do not let DLQ fill silently)
2. Inspect failed jobs to identify root cause
3. Fix the root cause, then replay or discard

**Never delete DLQ messages without inspecting them** — they contain the data of failed operations that may need manual resolution.

---

## Job Structure

```ts
// TypeScript / BullMQ example — same principles apply in any framework
class ProcessOrderJob {
  static queue = 'orders';
  static attempts = 3;
  static backoff = { type: 'exponential', delay: 2000 };

  async handle(payload: ProcessOrderPayload): Promise<void> {
    // 1. Validate payload
    const { orderId } = validateProcessOrderPayload(payload);

    // 2. Idempotency check
    const order = await Order.findOrFail(orderId);
    if (order.status !== OrderStatus.PENDING) return;

    // 3. Execute in a transaction if multiple writes are involved
    await db.transaction(async (trx) => {
      await order.process({ trx });
      await Inventory.reserve(order.items, { trx });
    });

    // 4. Trigger downstream jobs (don't chain synchronously)
    await SendOrderConfirmationJob.dispatch({ orderId });
  }
}
```

---

## Observability

- **Log on start and completion** — include job ID, entity ID, and duration; useful for tracing stuck jobs
- **Never log sensitive payload fields** — mask emails, card numbers, passwords in logs
- **Emit metrics** — queue depth, job duration p95, failure rate; connect to the monitoring dashboard
- **Unique job ID in logs** — every log line inside a job handler must carry the job ID for correlation

```python
# ✅ Structured log with context
logger.info("order.processed", extra={
    "job_id": self.request.id,
    "order_id": order_id,
    "duration_ms": elapsed,
})
```

---

## Framework-Specific Notes

| Framework | Key consideration |
|---|---|
| **Laravel Queue** | Use `ShouldBeUnique` for deduplication; `ShouldBeIdempotent` pattern via `uniqueId()`; Horizon for monitoring |
| **Sidekiq / ActiveJob** | Use `sidekiq-unique-jobs` for deduplication; set `retry` and `dead` thresholds per worker class |
| **Celery** | Set `acks_late=True` for at-least-once; use `task_id` for idempotency; Flower for monitoring |
| **BullMQ** | Use `jobId` option for deduplication; configure `removeOnComplete` to avoid memory bloat |
| **AWS SQS** | Visibility timeout must exceed max job duration; use `MessageDeduplicationId` on FIFO queues |
| **RabbitMQ** | Set `acks_late` / manual ack — never auto-ack before processing completes; configure a dead-letter exchange (`x-dead-letter-exchange`) per queue; use `x-message-ttl` to cap retry window; prefer quorum queues over classic for durability |
| **Apache Kafka** | Idempotency is consumer-side — commit offset only after successful processing (`enable.auto.commit=false`); use a unique `group.id` per consumer app; store processed offsets or business keys to detect replays; DLQ = a dedicated topic (e.g. `topic.DLT`); monitor consumer lag as the primary health signal |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.