---
# === IDENTITY ===
id: business/erp-integration/error-handling-dead-letter-queues/2026
canonical_question: "How do you implement error handling and dead letter queues for ERP integrations - retry strategies?"
aliases:
  - "ERP integration DLQ patterns and retry strategies"
  - "How to handle failed ERP API messages with dead letter queues"
  - "Exponential backoff and circuit breaker for ERP integrations"
  - "Retry and dead letter queue architecture for enterprise middleware"
entity_type: erp_integration
domain: business > erp-integration > error-handling-dead-letter-queues
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Cross-Platform (AWS SQS, Azure Service Bus, Apache Kafka, RabbitMQ)"
    vendor: "Multiple (AWS, Microsoft, Apache, VMware)"
    version: "Current GA versions as of 2026"
    edition: "All editions"
    deployment: cloud
    api_surface: "REST, AMQP, Kafka Protocol"
  - name: "iPaaS Middleware (MuleSoft, Boomi, Workato, Celigo)"
    vendor: "Multiple"
    version: "Current GA versions as of 2026"
    edition: "All editions"
    deployment: cloud
    api_surface: "REST, Proprietary"

# === VERIFICATION ===
last_verified: 2026-03-02
confidence: 0.92
version: 1.0
first_published: 2026-03-02

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: null
  next_review: 2026-08-29
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "DLQ retention varies by platform: AWS SQS max 14 days, Azure Service Bus unlimited (Premium), Kafka depends on topic config"
  - "Maximum delivery count before dead-lettering: AWS SQS configurable (1-1000), Azure Service Bus default 10, RabbitMQ configurable via x-delivery-limit"
  - "Circuit breaker state storage must be shared across all integration worker instances for consistency"
  - "Exponential backoff max delay should be capped at 30-60 seconds for user-facing flows and 5-15 minutes for batch flows"
  - "Idempotency keys are required for any retry strategy — without them, retries cause duplicate records in target ERP"
  - "DLQ messages have no automatic cleanup — unprocessed messages accumulate indefinitely unless TTL or purge policies are configured"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs vendor-specific API rate limit details"
    use_instead: "business/erp-integration/{vendor}-rest-api/2026"
  - condition: "User needs authentication flow implementation"
    use_instead: "business/erp-integration/{vendor}-authentication/2026"
  - condition: "User needs data mapping between specific ERP systems"
    use_instead: "business/erp-integration/{source}-to-{target}-mapping/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: message_broker
    question: "Which message broker or middleware are you using?"
    type: choice
    options:
      - "AWS SQS / SNS"
      - "Azure Service Bus"
      - "Apache Kafka"
      - "RabbitMQ"
      - "iPaaS (MuleSoft, Boomi, Workato)"
      - "Custom / other"
  - key: error_tolerance
    question: "What is your error tolerance?"
    type: choice
    options:
      - "zero-loss (every message must eventually succeed)"
      - "best-effort (occasional message loss acceptable)"
      - "at-least-once (duplicates acceptable, no loss)"
  - key: integration_pattern
    question: "What integration pattern do you need?"
    type: choice
    options:
      - "real-time sync (individual records, <1s latency)"
      - "batch/bulk (scheduled, high volume)"
      - "event-driven (webhook, CDC, platform events)"
      - "file-based (CSV/XML import/export)"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/error-handling-dead-letter-queues/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-03-02)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "business/erp-integration/salesforce-rest-api/2026"
      label: "Salesforce REST API capabilities and rate limits"
    - id: "business/erp-integration/sap-s4hana-odata-api/2026"
      label: "SAP S/4HANA OData API capabilities"
  solves:
    - id: "business/erp-integration/ap-automation-playbook/2026"
      label: "AP automation integration playbook"
    - id: "business/erp-integration/order-to-cash-playbook/2026"
      label: "Order-to-cash integration playbook"
  alternative_to:
    - id: "business/erp-integration/saga-pattern-distributed-transactions/2026"
      label: "Saga pattern for distributed ERP transactions"
  often_confused_with:
    - id: "business/erp-integration/idempotency-patterns/2026"
      label: "Idempotency patterns (complementary, not a substitute for DLQ)"

# === SOURCES ===
sources:
  - id: src1
    title: "Dead Letter Queue (DLQ): Architecture, Examples, and Best Practices (2026 Guide)"
    author: SRE School
    url: https://sreschool.com/blog/dead-letter-queue-dlq/
    type: technical_blog
    published: 2026-01-15
    reliability: high
  - id: src2
    title: "Integration Patterns IV: Retries and Dead-Letter Queues"
    author: LittleHorse
    url: https://littlehorse.io/blog/retries-and-dlq
    type: technical_blog
    published: 2025-09-10
    reliability: high
  - id: src3
    title: "Mastering Exponential Backoff in Distributed Systems"
    author: Better Stack
    url: https://betterstack.com/community/guides/monitoring/exponential-backoff/
    type: technical_blog
    published: 2025-11-20
    reliability: high
  - id: src4
    title: "Circuit Breaker Pattern — AWS Prescriptive Guidance"
    author: Amazon Web Services
    url: https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/circuit-breaker.html
    type: official_docs
    published: 2025-06-01
    reliability: authoritative
  - id: src5
    title: "Service Bus Dead-Letter Queues — Azure Service Bus"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dead-letter-queues
    type: official_docs
    published: 2025-05-15
    reliability: authoritative
  - id: src6
    title: "Resilience Design Patterns: Retry, Fallback, Timeout, Circuit Breaker"
    author: codecentric AG
    url: https://www.codecentric.de/en/knowledge-hub/blog/resilience-design-patterns-retry-fallback-timeout-circuit-breaker
    type: technical_blog
    published: 2025-03-01
    reliability: high
  - id: src7
    title: "Using Dead-Letter Queues in Amazon SQS"
    author: Amazon Web Services
    url: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html
    type: official_docs
    published: 2025-08-01
    reliability: authoritative
---

# Error Handling and Dead Letter Queues for ERP Integrations: Retry Strategies

## TL;DR

- **Bottom line**: Implement a three-layer resilience stack: exponential backoff with jitter for transient errors, circuit breakers for prolonged outages, and dead letter queues for poison messages that exhaust all retries. Every ERP integration needs all three.
- **Key limit**: DLQ retention varies by platform — AWS SQS max 14 days, Azure Service Bus unlimited on Premium tier, Kafka depends on topic retention config. Plan your reprocessing SLA around these limits.
- **Watch out for**: Retrying without idempotency keys causes duplicate records in the target ERP. Every retry-eligible operation must be idempotent — use unique transaction IDs, not auto-increment.
- **Best for**: Any ERP integration with zero-loss or at-least-once delivery requirements — order-to-cash, AP automation, inventory sync, and payroll feeds.
- **Authentication**: N/A (pattern-level card). See system-specific cards for authentication details per ERP vendor.

## System Profile

This card covers cross-platform error handling patterns applicable to all major ERP integrations, regardless of the specific ERP system (Salesforce, SAP, Oracle, NetSuite, Dynamics 365, Workday) or middleware platform. The patterns are implemented at the integration middleware layer — the message broker or iPaaS platform that sits between systems.

The three core resilience patterns (retry with backoff, circuit breaker, dead letter queue) are supported natively by all major message brokers and iPaaS platforms, though configuration syntax and defaults differ significantly. This card provides platform-agnostic architecture guidance with platform-specific configuration references.

| System | Role | DLQ Support | Retry Support | Circuit Breaker |
|---|---|---|---|---|
| AWS SQS/SNS | Message broker | Native (redrive policy) | maxReceiveCount (1-1000) | Manual (Step Functions / custom) |
| Azure Service Bus | Message broker | Native (subqueue per entity) | MaxDeliveryCount (default 10) | Manual (custom implementation) |
| Apache Kafka | Event streaming | Via error topic convention | Consumer-side retry logic | Manual (custom implementation) |
| RabbitMQ | Message broker | Native (dead-letter exchange) | x-delivery-limit header | Manual (custom implementation) |
| MuleSoft Anypoint | iPaaS | Built-in DLQ connector | Configurable retry policies | Built-in circuit breaker scope |
| Boomi | iPaaS | Error handling shapes | Configurable retry | Custom via process routes |
| Workato | iPaaS | Error monitoring recipes | Auto-retry with configurable count | Custom via error handlers |

## API Surfaces & Capabilities

| Pattern | Type | Best For | Complexity | Latency Impact | Data Loss Risk |
|---|---|---|---|---|---|
| Immediate retry | Retry | Transient network blips | Low | Minimal (ms) | Medium (no backoff) |
| Exponential backoff | Retry | Rate limits, temporary overload | Medium | Increasing (1s-60s) | Low |
| Exponential backoff + jitter | Retry | High-concurrency retries | Medium | Increasing (randomized) | Low |
| Circuit breaker | Protection | Prolonged service outages | Medium | Fast-fail when open | None (preserves messages) |
| Dead letter queue | Error isolation | Poison messages, schema errors | Medium | None (async) | Very low |
| Saga with compensation | Transaction | Multi-system writes | High | Variable | Very low |
| Outbox pattern | Delivery guarantee | Exactly-once publish | High | Minimal | Near zero |

## Rate Limits & Quotas

### Per-Platform DLQ Limits

| Platform | Max Retention | Max Message Size | Max DLQ Depth | Reprocessing Method |
|---|---|---|---|---|
| AWS SQS | 14 days | 256 KB (2 GB with S3) | Unlimited | Redrive to source queue |
| Azure Service Bus (Standard) | Unlimited | 256 KB | 5 GB per entity | Receive + resubmit |
| Azure Service Bus (Premium) | Unlimited | 100 MB | 80 GB per entity | Receive + resubmit |
| Apache Kafka (error topic) | Topic retention config | 1 MB default (configurable) | Partition-based | Consumer from error topic |
| RabbitMQ | TTL-based (configurable) | 128 MB default | Memory/disk-based | Consume from DLX queue |

[src5, src7]

### Retry Budget Guidelines

| Integration Type | Max Retries | Initial Delay | Max Delay | Backoff Factor | Jitter |
|---|---|---|---|---|---|
| Real-time API (user-facing) | 3-5 | 500ms | 30s | 2x | Full jitter |
| Batch/bulk processing | 5-10 | 1s | 5min | 2x | Equal jitter |
| Event-driven (CDC, webhooks) | 5-8 | 1s | 2min | 2x | Full jitter |
| File-based import (FBDI, EIB) | 3 | 30s | 10min | 3x | None |

[src3]

### Circuit Breaker Thresholds

| Parameter | Real-time Integration | Batch Integration | Event-driven |
|---|---|---|---|
| Failure threshold | 5 failures in 60s | 3 failures in 5min | 5 failures in 60s |
| Open state duration | 30s | 5min | 60s |
| Half-open probe count | 1 | 1-3 | 1 |
| Success threshold to close | 2 consecutive | 3 consecutive | 2 consecutive |

[src4, src6]

## Authentication

N/A — this is a pattern-level card. Authentication is handled at the ERP API layer. See system-specific cards for auth flows:

| ERP System | Recommended Auth | Relevant Card |
|---|---|---|
| Salesforce | OAuth 2.0 JWT Bearer | business/erp-integration/salesforce-rest-api/2026 |
| SAP S/4HANA | OAuth 2.0 + X.509 | business/erp-integration/sap-s4hana-odata-api/2026 |
| Oracle ERP Cloud | OAuth 2.0 | business/erp-integration/oracle-erp-cloud-rest-api/2026 |
| NetSuite | Token-Based Auth (TBA) | business/erp-integration/netsuite-suitetalk-api/2026 |
| Dynamics 365 | OAuth 2.0 (Azure AD) | business/erp-integration/dynamics-365-dataverse-api/2026 |

## Constraints
<!-- Agents: read this section before recommending any integration approach from this unit.
     These are hard constraints that must not be violated. -->

- **Idempotency is mandatory**: Every retryable operation must include an idempotency key (UUID, composite business key, or hash). Without it, retries create duplicate records in the target ERP — the #1 integration failure mode. [src1]
- **DLQ messages have no automatic cleanup**: AWS SQS, Azure Service Bus, Kafka error topics — none auto-purge DLQ messages. You must implement explicit retention policies or manual/automated reprocessing workflows. [src5]
- **Circuit breaker state must be shared**: If your integration runs on multiple worker instances (containers, Lambda functions, pods), circuit breaker state must be stored in a shared data store (Redis, DynamoDB, database) — local in-memory state causes split-brain behavior. [src4]
- **Retry amplification risk**: Retrying at multiple layers (application + middleware + infrastructure) causes exponential retry amplification. Pick ONE retry layer and disable retries at other layers. [src6]
- **DLQ depth = integration health**: A growing DLQ backlog indicates a systemic issue, not a transient failure. Alert on DLQ depth > 100 messages and investigate root cause before replaying. [src1]
- **Poison messages must be identified early**: Messages that will never succeed (schema validation failures, missing required fields, invalid data types) should be routed to DLQ immediately without retries. Only transient errors (429, 503, network timeout) deserve retries. [src2]

## Integration Pattern Decision Tree

```
START — ERP integration message fails processing
├── What type of error?
│   ├── Transient (429 rate limit, 503 unavailable, network timeout, connection reset)
│   │   ├── Is circuit breaker OPEN?
│   │   │   ├── YES → Fast-fail, queue for retry after circuit reset
│   │   │   └── NO ↓
│   │   ├── Retry count < max retries?
│   │   │   ├── YES → Retry with exponential backoff + jitter
│   │   │   │   ├── delay = min(initialDelay * 2^attempt, maxDelay)
│   │   │   │   └── actualDelay = random(0, delay) [full jitter]
│   │   │   └── NO → Move to Dead Letter Queue with full context
│   │   └── Did retry succeed?
│   │       ├── YES → Mark success, reset failure counter
│   │       └── NO → Increment failure counter, check circuit breaker threshold
│   │           ├── Threshold exceeded → OPEN circuit breaker
│   │           └── Below threshold → Continue retry loop
│   ├── Non-transient / Poison (400 bad request, 422 validation, schema mismatch)
│   │   └── Route IMMEDIATELY to DLQ — retries will never succeed
│   │       ├── Include: original message, error response, timestamp, attempt count
│   │       └── Alert operations team if message volume > threshold
│   ├── Partial success (bulk operation: some records succeed, some fail)
│   │   ├── Extract failed records from response
│   │   ├── Create new message with ONLY failed records
│   │   └── Route failed-record message through retry pipeline
│   └── Authentication error (401, 403)
│       ├── Token expired? → Refresh token, retry once
│       ├── Permissions changed? → Alert + DLQ (do not retry)
│       └── Credentials revoked? → OPEN circuit breaker + alert
├── DLQ message processing
│   ├── Automated triage
│   │   ├── Classify error type (schema, auth, data, system)
│   │   ├── Group by error pattern
│   │   └── Auto-resolve known patterns (e.g., re-run after token refresh)
│   └── Manual review
│       ├── Fix data issues in source system
│       ├── Replay individual or batch messages
│       └── Purge messages that are no longer relevant
└── Monitoring
    ├── DLQ depth: alert at > 100, page at > 1000
    ├── DLQ ingestion rate: alert if > 1% of total throughput
    ├── Circuit breaker state changes: log every transition
    └── Retry success rate: target > 95%
```

## Quick Reference

| Error Type | Retryable? | Strategy | Max Retries | DLQ Action |
|---|---|---|---|---|
| HTTP 429 (Rate Limit) | Yes | Backoff, respect Retry-After header | 5-10 | Reprocess after cooldown |
| HTTP 500 (Server Error) | Yes | Exponential backoff + jitter | 5 | Investigate server-side |
| HTTP 502/503/504 (Gateway) | Yes | Exponential backoff | 5 | Reprocess after service recovery |
| HTTP 400 (Bad Request) | No | Immediate DLQ | 0 | Fix payload, resubmit |
| HTTP 401 (Unauthorized) | Once | Refresh token, retry once | 1 | Rotate credentials |
| HTTP 403 (Forbidden) | No | Immediate DLQ | 0 | Fix permissions |
| HTTP 404 (Not Found) | No | Immediate DLQ | 0 | Fix resource reference |
| HTTP 409 (Conflict) | Conditional | Retry with conflict resolution | 3 | Manual merge |
| HTTP 422 (Validation) | No | Immediate DLQ | 0 | Fix data, resubmit |
| Connection timeout | Yes | Exponential backoff | 5 | Check network/firewall |
| Connection refused | Yes | Exponential backoff | 3 | Check service health |
| SSL/TLS handshake failure | No | Immediate DLQ | 0 | Fix certificates |
| DNS resolution failure | Yes | Exponential backoff | 3 | Check DNS config |
| Message schema mismatch | No | Immediate DLQ | 0 | Update schema, resubmit |

## Step-by-Step Integration Guide

### 1. Classify errors into retryable vs non-retryable

Before implementing retry logic, establish a clear error classification. Retrying non-retryable errors wastes resources and delays DLQ processing. [src2]

```python
# Error classification for ERP integrations
RETRYABLE_ERRORS = {
    429,  # Rate limited — always retry with Retry-After
    500,  # Internal server error — transient
    502,  # Bad gateway — transient
    503,  # Service unavailable — transient
    504,  # Gateway timeout — transient
}

NON_RETRYABLE_ERRORS = {
    400,  # Bad request — fix payload
    401,  # Unauthorized — refresh token once, then DLQ
    403,  # Forbidden — permissions issue
    404,  # Not found — resource doesn't exist
    405,  # Method not allowed — wrong endpoint
    409,  # Conflict — needs resolution logic
    422,  # Validation error — fix data
}

def classify_error(status_code, error_body):
    """Classify an ERP API error as retryable or non-retryable."""
    if status_code in RETRYABLE_ERRORS:
        return "transient"
    if status_code == 401:
        return "auth_expired"  # Special: retry once after token refresh
    if status_code in NON_RETRYABLE_ERRORS:
        return "poison"  # Route to DLQ immediately
    if status_code >= 500:
        return "transient"  # Unknown 5xx — assume transient
    return "poison"  # Unknown client error — don't retry
```

**Verify**: Run error classifier against last 30 days of integration logs -> expected: <5% misclassification rate.

### 2. Implement exponential backoff with jitter

The backoff formula prevents retry storms that overwhelm already-stressed ERP APIs. Full jitter is recommended — it distributes retries evenly across the delay window. [src3]

```python
import random
import time

def exponential_backoff_with_jitter(
    attempt: int,
    initial_delay: float = 1.0,
    max_delay: float = 60.0,
    factor: float = 2.0,
    jitter: str = "full"  # "full", "equal", or "decorrelated"
) -> float:
    """Calculate retry delay with exponential backoff and jitter.

    Full jitter:        random(0, min(max_delay, initial_delay * factor^attempt))
    Equal jitter:       half + random(0, half) where half = delay/2
    Decorrelated jitter: min(max_delay, random(initial_delay, 3 * previous_delay))
    """
    exponential_delay = initial_delay * (factor ** attempt)
    capped_delay = min(exponential_delay, max_delay)

    if jitter == "full":
        return random.uniform(0, capped_delay)
    elif jitter == "equal":
        half = capped_delay / 2
        return half + random.uniform(0, half)
    else:  # decorrelated
        return min(max_delay, random.uniform(initial_delay, 3 * capped_delay))
```

**Verify**: `exponential_backoff_with_jitter(0)` returns value between 0 and 1.0; `exponential_backoff_with_jitter(5)` returns value between 0 and 32.0.

### 3. Implement the circuit breaker

The circuit breaker prevents your integration from hammering an ERP API that is already down, which avoids wasting API quota and prevents cascading failures. [src4]

```python
import time
import threading

class CircuitBreaker:
    """Thread-safe circuit breaker for ERP API calls.

    States: CLOSED (normal) -> OPEN (failing) -> HALF_OPEN (testing) -> CLOSED
    """
    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"

    def __init__(self, failure_threshold=5, reset_timeout=30, success_threshold=2):
        self.failure_threshold = failure_threshold
        self.reset_timeout = reset_timeout  # seconds
        self.success_threshold = success_threshold
        self.state = self.CLOSED
        self.failure_count = 0
        self.success_count = 0
        self.last_failure_time = None
        self._lock = threading.Lock()

    def can_execute(self) -> bool:
        with self._lock:
            if self.state == self.CLOSED:
                return True
            if self.state == self.OPEN:
                if time.time() - self.last_failure_time >= self.reset_timeout:
                    self.state = self.HALF_OPEN
                    self.success_count = 0
                    return True  # Allow probe request
                return False  # Still in cooldown
            if self.state == self.HALF_OPEN:
                return True  # Allow probe requests
        return False

    def record_success(self):
        with self._lock:
            if self.state == self.HALF_OPEN:
                self.success_count += 1
                if self.success_count >= self.success_threshold:
                    self.state = self.CLOSED
                    self.failure_count = 0
            else:
                self.failure_count = 0

    def record_failure(self):
        with self._lock:
            self.failure_count += 1
            self.last_failure_time = time.time()
            if self.state == self.HALF_OPEN:
                self.state = self.OPEN
            elif self.failure_count >= self.failure_threshold:
                self.state = self.OPEN
```

**Verify**: Create breaker with `failure_threshold=3`, trigger 3 failures -> `can_execute()` returns `False`. Wait `reset_timeout` seconds -> `can_execute()` returns `True` (half-open).

### 4. Build the retry-with-DLQ pipeline

Combine error classification, backoff, and circuit breaker into a unified pipeline that routes failures appropriately. [src1, src2]

```python
import json
import uuid
from datetime import datetime, timezone

class ERPRetryPipeline:
    """Complete retry pipeline with DLQ for ERP integrations."""

    def __init__(self, erp_client, dlq_client, circuit_breaker, max_retries=5):
        self.erp = erp_client
        self.dlq = dlq_client
        self.cb = circuit_breaker
        self.max_retries = max_retries

    def process_message(self, message: dict) -> dict:
        """Process a single ERP integration message with full resilience."""
        idempotency_key = message.get("idempotency_key") or str(uuid.uuid4())
        message["idempotency_key"] = idempotency_key

        for attempt in range(self.max_retries + 1):
            # Check circuit breaker
            if not self.cb.can_execute():
                return self._send_to_dlq(message, attempt, "circuit_breaker_open",
                    "Circuit breaker is OPEN — target ERP unavailable")

            try:
                response = self.erp.send(message)
                self.cb.record_success()
                return {"status": "success", "attempt": attempt, "response": response}

            except ERPAPIError as e:
                error_type = classify_error(e.status_code, e.body)

                if error_type == "poison":
                    # Non-retryable — DLQ immediately
                    return self._send_to_dlq(message, attempt,
                        f"http_{e.status_code}", str(e.body))

                if error_type == "auth_expired" and attempt == 0:
                    self.erp.refresh_token()
                    continue  # Retry once after token refresh

                self.cb.record_failure()

                if attempt < self.max_retries:
                    delay = exponential_backoff_with_jitter(attempt)
                    # Respect Retry-After header if present
                    if e.status_code == 429 and e.retry_after:
                        delay = max(delay, float(e.retry_after))
                    time.sleep(delay)
                else:
                    return self._send_to_dlq(message, attempt,
                        f"http_{e.status_code}", f"Exhausted {self.max_retries} retries")

    def _send_to_dlq(self, message, attempt, error_code, error_detail):
        """Route failed message to DLQ with full diagnostic context."""
        dlq_envelope = {
            "original_message": message,
            "error_code": error_code,
            "error_detail": error_detail,
            "attempt_count": attempt + 1,
            "idempotency_key": message.get("idempotency_key"),
            "dead_lettered_at": datetime.now(timezone.utc).isoformat(),
            "source_queue": message.get("source_queue", "unknown"),
            "integration_id": message.get("integration_id", "unknown"),
        }
        self.dlq.send(json.dumps(dlq_envelope))
        return {"status": "dead_lettered", "attempt": attempt + 1,
                "error_code": error_code}
```

**Verify**: Send a message to a mock ERP that returns 503 three times then 200 -> pipeline returns `{"status": "success", "attempt": 3}`. Send a message that returns 400 -> pipeline returns `{"status": "dead_lettered", "attempt": 1}`.

### 5. Configure platform-specific DLQ

Set up the dead letter queue on your chosen message broker. [src5, src7]

```bash
# AWS SQS: Create DLQ and configure redrive policy
aws sqs create-queue --queue-name erp-integration-dlq \
  --attributes '{"MessageRetentionPeriod":"1209600"}'  # 14 days

DLQ_ARN=$(aws sqs get-queue-attributes --queue-url $DLQ_URL \
  --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)

aws sqs set-queue-attributes --queue-url $SOURCE_QUEUE_URL \
  --attributes "{\"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"5\\\"}\"}"

# Azure Service Bus: DLQ is automatic (subqueue per entity)
# Access via: <queue-name>/$deadletterqueue
# Configure max delivery count:
az servicebus queue update --name erp-integration \
  --namespace-name mybus --resource-group myrg \
  --max-delivery-count 5

# RabbitMQ: Configure dead-letter exchange
# Channel declaration:
# channel.queue_declare(queue='erp-integration',
#   arguments={
#     'x-dead-letter-exchange': 'erp-dlx',
#     'x-dead-letter-routing-key': 'erp-integration-dlq',
#     'x-delivery-limit': 5
#   })
```

**Verify**: `aws sqs get-queue-attributes --queue-url $SOURCE_QUEUE_URL --attribute-names RedrivePolicy` -> shows DLQ ARN and maxReceiveCount of 5.

### 6. Set up DLQ monitoring and alerting

DLQ without monitoring is a silent data loss risk. Configure alerts on DLQ depth and ingestion rate. [src1]

```python
# Prometheus metrics for DLQ monitoring
from prometheus_client import Counter, Gauge, Histogram

dlq_messages_total = Counter(
    'erp_dlq_messages_total',
    'Total messages routed to DLQ',
    ['integration_id', 'error_type']
)
dlq_depth = Gauge(
    'erp_dlq_depth',
    'Current DLQ message count',
    ['queue_name']
)
retry_duration_seconds = Histogram(
    'erp_retry_duration_seconds',
    'Time spent in retry loop before success or DLQ',
    ['integration_id', 'outcome'],
    buckets=[0.5, 1, 2, 5, 10, 30, 60, 120, 300]
)
circuit_breaker_state = Gauge(
    'erp_circuit_breaker_state',
    'Circuit breaker state (0=closed, 1=open, 2=half_open)',
    ['service_name']
)

# Alert rules (Prometheus/Alertmanager format):
# - alert: DLQDepthCritical
#   expr: erp_dlq_depth > 1000
#   for: 5m
#   labels: { severity: page }
#   annotations: { summary: "DLQ depth exceeds 1000 messages" }
#
# - alert: DLQIngestionRateHigh
#   expr: rate(erp_dlq_messages_total[5m]) > 0.01 * rate(erp_messages_processed_total[5m])
#   for: 10m
#   labels: { severity: warning }
#   annotations: { summary: "DLQ ingestion rate exceeds 1% of throughput" }
```

**Verify**: Query `/metrics` endpoint -> all four metric families visible. Trigger a DLQ message -> `erp_dlq_messages_total` increments by 1.

## Code Examples

### Python: Complete retry handler with DLQ for Salesforce bulk API

```python
# Input:  List of records to upsert via Salesforce Bulk API 2.0
# Output: Success/failure counts, DLQ message IDs for failed batches

import requests
import time
import random
import json
import uuid
from datetime import datetime, timezone

class SalesforceBulkRetryHandler:
    """Retry handler for Salesforce Bulk API 2.0 with DLQ support."""

    def __init__(self, instance_url, access_token, dlq_client,
                 max_retries=5, initial_delay=1.0, max_delay=60.0):
        self.base_url = f"{instance_url}/services/data/v62.0"
        self.headers = {
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json"
        }
        self.dlq = dlq_client
        self.max_retries = max_retries
        self.initial_delay = initial_delay
        self.max_delay = max_delay

    def upsert_with_retry(self, object_name, external_id_field,
                          records, idempotency_key=None):
        """Upsert records with exponential backoff and DLQ fallback."""
        idem_key = idempotency_key or str(uuid.uuid4())

        for attempt in range(self.max_retries + 1):
            try:
                # Create bulk job
                job = requests.post(
                    f"{self.base_url}/jobs/ingest",
                    headers=self.headers,
                    json={
                        "object": object_name,
                        "externalIdFieldName": external_id_field,
                        "operation": "upsert",
                        "contentType": "CSV"
                    }
                )

                if job.status_code == 429:
                    retry_after = int(job.headers.get("Retry-After", 0))
                    delay = max(
                        self._backoff(attempt),
                        retry_after
                    )
                    time.sleep(delay)
                    continue

                if job.status_code >= 500:
                    time.sleep(self._backoff(attempt))
                    continue

                if job.status_code >= 400:
                    # Non-retryable client error
                    return self._dead_letter(records, idem_key, attempt,
                        f"HTTP {job.status_code}", job.text)

                job_id = job.json()["id"]
                # Upload data and close job (simplified)
                return {"status": "success", "job_id": job_id,
                        "attempt": attempt}

            except requests.exceptions.ConnectionError:
                if attempt < self.max_retries:
                    time.sleep(self._backoff(attempt))
                else:
                    return self._dead_letter(records, idem_key, attempt,
                        "connection_error", "Connection refused after retries")

        return self._dead_letter(records, idem_key, self.max_retries,
            "max_retries_exceeded", "All retry attempts exhausted")

    def _backoff(self, attempt):
        delay = self.initial_delay * (2 ** attempt)
        capped = min(delay, self.max_delay)
        return random.uniform(0, capped)  # Full jitter

    def _dead_letter(self, records, idem_key, attempt, code, detail):
        envelope = {
            "idempotency_key": idem_key,
            "records": records,
            "error_code": code,
            "error_detail": detail,
            "attempts": attempt + 1,
            "dead_lettered_at": datetime.now(timezone.utc).isoformat()
        }
        msg_id = self.dlq.send(json.dumps(envelope))
        return {"status": "dead_lettered", "dlq_message_id": msg_id,
                "error_code": code}
```

### JavaScript/Node.js: Generic ERP retry middleware with circuit breaker

```javascript
// Input:  ERP API call function + message payload
// Output: Success response or DLQ envelope
// Requires: node-fetch@3.x

class ERPCircuitBreaker {
  constructor({
    failureThreshold = 5,
    resetTimeout = 30000,  // ms
    successThreshold = 2
  } = {}) {
    this.failureThreshold = failureThreshold;
    this.resetTimeout = resetTimeout;
    this.successThreshold = successThreshold;
    this.state = 'closed';
    this.failureCount = 0;
    this.successCount = 0;
    this.lastFailureTime = null;
  }

  canExecute() {
    if (this.state === 'closed') return true;
    if (this.state === 'open') {
      if (Date.now() - this.lastFailureTime >= this.resetTimeout) {
        this.state = 'half_open';
        this.successCount = 0;
        return true;
      }
      return false;
    }
    return true; // half_open
  }

  recordSuccess() {
    if (this.state === 'half_open') {
      this.successCount++;
      if (this.successCount >= this.successThreshold) {
        this.state = 'closed';
        this.failureCount = 0;
      }
    } else {
      this.failureCount = 0;
    }
  }

  recordFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    if (this.state === 'half_open' ||
        this.failureCount >= this.failureThreshold) {
      this.state = 'open';
    }
  }
}

async function retryWithDLQ(apiCall, message, {
  maxRetries = 5,
  initialDelay = 1000,
  maxDelay = 60000,
  factor = 2,
  circuitBreaker = new ERPCircuitBreaker(),
  dlqSend = async (envelope) => console.error('DLQ:', envelope)
} = {}) {
  const idempotencyKey = message.idempotencyKey || crypto.randomUUID();

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    if (!circuitBreaker.canExecute()) {
      return dlqSend({
        originalMessage: message,
        errorCode: 'circuit_breaker_open',
        attempts: attempt,
        idempotencyKey,
        deadLetteredAt: new Date().toISOString()
      });
    }

    try {
      const result = await apiCall(message, idempotencyKey);
      circuitBreaker.recordSuccess();
      return { status: 'success', attempt, result };
    } catch (err) {
      const statusCode = err.statusCode || err.status || 0;

      // Non-retryable errors — DLQ immediately
      if ([400, 403, 404, 405, 422].includes(statusCode)) {
        return dlqSend({
          originalMessage: message,
          errorCode: `http_${statusCode}`,
          errorDetail: err.message,
          attempts: attempt + 1,
          idempotencyKey,
          deadLetteredAt: new Date().toISOString()
        });
      }

      circuitBreaker.recordFailure();

      if (attempt < maxRetries) {
        const expDelay = initialDelay * Math.pow(factor, attempt);
        const capped = Math.min(expDelay, maxDelay);
        const jittered = Math.random() * capped;
        // Respect Retry-After header
        const retryAfter = (err.headers?.['retry-after'] || 0) * 1000;
        await new Promise(r => setTimeout(r, Math.max(jittered, retryAfter)));
      }
    }
  }

  return dlqSend({
    originalMessage: message,
    errorCode: 'max_retries_exceeded',
    attempts: maxRetries + 1,
    idempotencyKey,
    deadLetteredAt: new Date().toISOString()
  });
}
```

### cURL: DLQ monitoring and replay

```bash
# Input:  AWS SQS DLQ URL, Azure Service Bus connection
# Output: DLQ depth, sample messages, replay commands

# AWS SQS: Check DLQ depth
aws sqs get-queue-attributes \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789/erp-dlq \
  --attribute-names ApproximateNumberOfMessages \
  --query 'Attributes.ApproximateNumberOfMessages'
# Expected: "42" (number of messages in DLQ)

# AWS SQS: Peek at DLQ messages (without consuming)
aws sqs receive-message \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789/erp-dlq \
  --max-number-of-messages 5 \
  --visibility-timeout 0

# AWS SQS: Replay DLQ messages back to source queue
aws sqs start-message-move-task \
  --source-arn arn:aws:sqs:us-east-1:123456789:erp-dlq \
  --destination-arn arn:aws:sqs:us-east-1:123456789:erp-integration

# Azure Service Bus: Check DLQ count
az servicebus queue show \
  --name erp-integration \
  --namespace-name mybus \
  --resource-group myrg \
  --query 'countDetails.deadLetterMessageCount'
# Expected: 42

# Azure Service Bus: Peek DLQ messages
az servicebus queue peek \
  --name 'erp-integration/$deadletterqueue' \
  --namespace-name mybus \
  --resource-group myrg \
  --max-count 5
```

## Data Mapping

### DLQ Envelope Schema Reference

| Field | Type | Required | Description | Gotcha |
|---|---|---|---|---|
| original_message | Object | Yes | The complete original message payload | Must preserve all fields — partial messages break replay |
| error_code | String | Yes | Machine-readable error code (e.g., "http_429") | Standardize codes across all integrations for aggregated reporting |
| error_detail | String | Yes | Human-readable error description | Truncate to 4KB max — large stack traces bloat DLQ storage |
| attempt_count | Integer | Yes | Number of attempts before dead-lettering | Starts at 1, not 0 |
| idempotency_key | String | Yes | Unique key for replay deduplication | UUID or composite business key — never auto-increment |
| dead_lettered_at | ISO 8601 | Yes | When the message was dead-lettered | Always UTC — timezone mismatches cause replay ordering issues |
| source_queue | String | Yes | Origin queue/topic name | Required for routing replayed messages |
| integration_id | String | Yes | Integration flow identifier | Maps to monitoring dashboards |
| correlation_id | String | Recommended | End-to-end trace ID | Enables cross-system debugging |
| original_timestamp | ISO 8601 | Recommended | When the original message was produced | Helps identify stale messages during replay |

### Data Type Gotchas

- **Timestamp formats vary by ERP**: Salesforce uses ISO 8601 UTC, SAP uses `YYYYMMDD` + `HHMMSS` in separate fields, NetSuite depends on user timezone. Always normalize to UTC ISO 8601 in DLQ envelopes. [src1]
- **Partial success responses differ**: Salesforce Bulk API returns success/failure per record in a results CSV. SAP returns BAPI messages per record. Oracle FBDI returns an error report file. Parse all formats before determining which records to DLQ. [src2]
- **Message size limits**: AWS SQS max 256KB per message. If your DLQ envelope exceeds this, store the original payload in S3 and include only the S3 reference in the DLQ message. Azure Service Bus Premium supports up to 100MB. [src5, src7]

## Error Handling & Failure Points

### Common Error Codes

| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| HTTP 429 | Rate limit exceeded | Too many API calls in window | Exponential backoff, respect Retry-After header, reduce concurrency |
| HTTP 503 | Service unavailable | ERP maintenance or overload | Exponential backoff, check ERP status page |
| ETIMEDOUT | Connection timeout | Network issue or slow response | Increase timeout, check firewall rules, verify DNS |
| ECONNREFUSED | Connection refused | Service down or port blocked | Open circuit breaker, alert operations |
| MaxDeliveryCountExceeded | DLQ threshold reached | Message failed N times | Review error, fix root cause, replay from DLQ |
| TTLExpiredException | Message expired | Not processed within TTL | Increase TTL or processing capacity |
| UNABLE_TO_LOCK_ROW | Record lock conflict | Concurrent update to same record | Retry with jitter, implement record-level locking |
| INVALID_SESSION_ID | Token expired/invalid | OAuth token expired | Refresh token, retry once |

[src4, src5]

### Failure Points in Production

- **Retry amplification across layers**: Application retries 5x, middleware retries 3x, infrastructure retries 2x = 30 actual API calls for one message. Fix: `Disable retries at all layers except one. Application-layer retry is preferred for ERP integrations because you control error classification.` [src6]
- **DLQ messages accumulate silently**: Teams set up DLQ but no monitoring. Months later, thousands of unprocessed messages represent lost orders or duplicate payments. Fix: `Alert on DLQ depth > 100 within 24 hours of going live. Assign DLQ triage to the on-call rotation.` [src1]
- **Circuit breaker split-brain**: Each Lambda/container instance has its own circuit breaker state. Instance A opens its breaker while instances B-F continue hammering the failing ERP API. Fix: `Store circuit breaker state in Redis or DynamoDB. Check shared state before every API call.` [src4]
- **Replay without idempotency causes duplicates**: Team replays 500 DLQ messages. 200 had actually succeeded before timeout — now those 200 records are duplicated in the target ERP. Fix: `Every operation must use an idempotency key. Target ERP must reject duplicate keys (upsert by external ID, not insert).` [src2]
- **Poison message loops**: A schema validation error routes to DLQ, automated replay pushes it back, it fails again, routes to DLQ. Fix: `Track replay_count in DLQ envelope. Auto-quarantine messages with replay_count > 3.` [src1]
- **Rate limit backoff ignored during batch replay**: Team replays 1,000 DLQ messages simultaneously, immediately hitting rate limits again. Fix: `Rate-limit DLQ replay to 10-20% of normal throughput. Use a dedicated replay queue with throttling.` [src3]

## Anti-Patterns

### Wrong: Retrying all errors indiscriminately

```python
# BAD — retries non-retryable errors, wasting time and API quota
def send_to_erp(message, max_retries=5):
    for attempt in range(max_retries):
        try:
            response = erp_api.post(message)
            return response
        except Exception as e:
            time.sleep(2 ** attempt)  # Retries 400, 403, 422 etc.
    raise Exception("All retries failed")
```

### Correct: Classify errors before retrying

```python
# GOOD — only retries transient errors, DLQs poison messages immediately
def send_to_erp(message, max_retries=5):
    for attempt in range(max_retries):
        try:
            response = erp_api.post(message)
            return response
        except ERPError as e:
            if e.status_code in (400, 403, 404, 422):
                dlq.send(message, error=str(e))  # Don't retry
                return None
            if attempt < max_retries - 1:
                time.sleep(exponential_backoff_with_jitter(attempt))
    dlq.send(message, error="Retries exhausted")
```

### Wrong: Fixed-interval retry (no backoff)

```python
# BAD — hammers the ERP API at constant rate during outage
for attempt in range(5):
    try:
        response = erp_api.post(message)
        break
    except Exception:
        time.sleep(5)  # Same 5s delay every time
```

### Correct: Exponential backoff with jitter

```python
# GOOD — spreads retry load, respects rate limits
for attempt in range(5):
    try:
        response = erp_api.post(message)
        break
    except TransientError:
        delay = min(1.0 * (2 ** attempt), 60.0)
        time.sleep(random.uniform(0, delay))  # Full jitter
```

### Wrong: DLQ without envelope metadata

```python
# BAD — raw message in DLQ, no context for debugging or replay
dlq.send(original_message)
# Later: "Why is this in the DLQ? When did it fail? What error?"
```

### Correct: DLQ with rich diagnostic envelope

```python
# GOOD — complete context for debugging and safe replay
dlq.send({
    "original_message": original_message,
    "error_code": "http_429",
    "error_detail": "Rate limit exceeded on Salesforce Bulk API",
    "attempt_count": 6,
    "idempotency_key": "order-12345-upsert-v2",
    "dead_lettered_at": "2026-03-01T14:30:00Z",
    "source_queue": "salesforce-bulk-ingest",
    "correlation_id": "trace-abc-123"
})
```

## Common Pitfalls

- **No DLQ retention policy**: DLQ messages accumulate forever if retention is not configured. After 6 months, you have 50,000 stale messages — replaying them would create chaos. Fix: `Set TTL on DLQ messages (14 days for transient errors, 90 days for data issues). Auto-archive to cold storage after TTL.` [src5]
- **Retrying at multiple layers simultaneously**: Application retries + middleware retries + infrastructure retries = exponential retry amplification. Fix: `Choose one retry layer (application for ERP integrations). Disable retries at middleware and infrastructure layers.` [src6]
- **Missing idempotency on replay**: Replaying DLQ messages without idempotency keys creates duplicate records. Fix: `Include idempotency_key in every message. Use upsert (not insert) operations in target ERP. Verify duplicate detection before bulk replay.` [src2]
- **Circuit breaker too sensitive**: Opening the circuit on a single 503 error stops all integration traffic for the reset period. Fix: `Require 5+ failures in a rolling 60-second window before opening. Use percentage-based thresholds (>50% failure rate) for high-volume integrations.` [src4]
- **Circuit breaker too lenient**: Requiring 50 failures to open means you've already wasted 50 API calls against a dead endpoint. Fix: `Tune thresholds based on normal error rate. For ERP APIs with <1% natural error rate, 5 failures in 60s is appropriate.` [src4]
- **DLQ replay storms**: Replaying all accumulated DLQ messages at once overwhelms the ERP API and triggers more failures. Fix: `Rate-limit replays to 10-20% of normal throughput. Use a dedicated replay queue with configurable concurrency.` [src3]
- **Ignoring partial success in bulk operations**: Salesforce Bulk API, Oracle FBDI, and SAP IDocs can partially succeed — some records succeed while others fail. Treating the entire batch as failed creates duplicates. Fix: `Parse per-record results. Only retry the specific failed records, not the entire batch.` [src1]

## Diagnostic Commands

```bash
# Check DLQ depth (AWS SQS)
aws sqs get-queue-attributes \
  --queue-url $DLQ_URL \
  --attribute-names ApproximateNumberOfMessages
# Expected: "0" when healthy, investigate if > 100

# Check DLQ depth (Azure Service Bus)
az servicebus queue show --name erp-integration \
  --namespace-name $NAMESPACE --resource-group $RG \
  --query 'countDetails.deadLetterMessageCount'

# Peek at DLQ messages without consuming (AWS SQS)
aws sqs receive-message --queue-url $DLQ_URL \
  --max-number-of-messages 10 --visibility-timeout 0

# Check circuit breaker state (if stored in Redis)
redis-cli GET "circuit_breaker:salesforce_api:state"
# Expected: "closed" when healthy

# Check circuit breaker failure count
redis-cli GET "circuit_breaker:salesforce_api:failure_count"

# Monitor retry rate (Prometheus query)
# rate(erp_retry_attempts_total[5m]) / rate(erp_messages_processed_total[5m])
# Expected: < 0.05 (less than 5% retry rate)

# Check DLQ ingestion rate (Prometheus query)
# rate(erp_dlq_messages_total[1h])
# Expected: < 0.01 * rate(erp_messages_processed_total[1h])

# Replay DLQ messages (AWS SQS — available since 2023)
aws sqs start-message-move-task \
  --source-arn $DLQ_ARN \
  --destination-arn $SOURCE_QUEUE_ARN \
  --max-number-of-messages-per-second 10
```

## Version History & Compatibility

| Pattern/Platform | Version | Date | Status | Key Changes |
|---|---|---|---|---|
| AWS SQS DLQ Redrive | GA | 2023-07 | Current | Native message-move-task for DLQ replay |
| Azure Service Bus DLQ | GA (since 2014) | 2025-05 | Current | Enhanced dead-letter reason headers |
| Apache Kafka Error Topics | Convention | Ongoing | Current | No native DLQ — implemented via error topic pattern |
| RabbitMQ Dead Letter Exchange | GA (since 3.x) | 2024-09 | Current | x-delivery-limit added in 3.12+ (quorum queues) |
| MuleSoft Error Handling | 4.x | 2025-01 | Current | Enhanced DLQ connector with retry policies |
| AWS Step Functions Circuit Breaker | GA | 2024-11 | Current | Native circuit breaker pattern support |

### Deprecation Policy

These are architecture patterns, not versioned APIs — they do not have formal deprecation policies. However, platform-specific implementations evolve: AWS SQS redrive API replaced manual DLQ consumer patterns, and Azure Service Bus continues to add dead-letter reason codes. Always check your message broker's latest documentation for new native features that may simplify custom implementations. [src5, src7]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Zero-loss requirement (financial transactions, orders) | Fire-and-forget analytics events | Simple logging + batch reconciliation |
| ERP API has rate limits or intermittent outages | Target system has 99.99% availability SLA | Direct API call with basic try/catch |
| Multi-system integration (multiple failure points) | Single-system CRUD operations | Database transaction with rollback |
| Asynchronous processing (message queues, event streams) | Synchronous user-facing API calls | HTTP retry middleware (Polly, resilience4j) |
| Batch operations where partial failures are common | All-or-nothing transactional requirements | Saga pattern with compensation |
| Long-running integration jobs (>30s per operation) | Sub-second API calls | Inline retry with timeout |

## Cross-System Comparison

| Capability | AWS SQS | Azure Service Bus | Apache Kafka | RabbitMQ | MuleSoft |
|---|---|---|---|---|---|
| Native DLQ | Yes (redrive policy) | Yes (subqueue) | No (error topic convention) | Yes (dead-letter exchange) | Yes (DLQ connector) |
| Max retention | 14 days | Unlimited (Premium) | Topic retention config | TTL-based | Platform storage |
| DLQ replay | Native (message-move-task) | Manual (receive + resend) | Consumer from error topic | Manual (consume + republish) | UI-based replay |
| Max delivery count | 1-1000 (configurable) | 1-2000 (default 10) | Consumer-side logic | x-delivery-limit (quorum queues) | Configurable |
| Circuit breaker | Manual (Step Functions) | Manual | Manual | Manual | Built-in scope |
| Retry backoff | Application-side | Application-side | Application-side | Application-side + plugin | Built-in policies |
| Message ordering | FIFO queues (optional) | Sessions (optional) | Partition ordering | Per-queue ordering | Flow ordering |
| Dead-letter reason | Application-set | System headers + custom | Application-set | Application-set (headers) | Error type metadata |
| Cost model | Per-message | Per-operation + storage | Infrastructure (self-hosted) | Infrastructure (self-hosted) | License-based |
| Max message size | 256KB (2GB with S3) | 256KB (Std), 100MB (Premium) | 1MB default (configurable) | 128MB default | Platform limit |

## Important Caveats

- **DLQ is not error handling — it is error isolation**: A DLQ stores messages that have already failed all retry attempts. It does not fix errors. You still need automated triage, alerting, and manual review processes for DLQ messages.
- **Idempotency is not optional**: Any retry strategy without idempotency keys will create duplicate records in the target ERP. This is the single most common and expensive mistake in ERP integrations.
- **Platform DLQ features vary significantly**: AWS SQS, Azure Service Bus, and RabbitMQ have native DLQ support with different capabilities. Kafka requires custom error-topic implementation. Do not assume your broker's DLQ works like another's.
- **Circuit breaker thresholds need tuning**: Default thresholds rarely match production traffic patterns. Start conservative (5 failures in 60s) and adjust based on observed error rates and recovery times.
- **Retry strategies must account for rate limits**: ERP APIs like Salesforce (100K calls/24h) and SAP (per-dialog-step limits) have hard quotas. Retries consume from the same quota. Monitor remaining quota and back off proactively before hitting limits.
- **Information currency**: Message broker DLQ features evolve rapidly. AWS, Azure, and RabbitMQ added significant DLQ improvements in 2024-2025. Always verify against current platform documentation.

## Related Units

- [Salesforce REST API Capabilities](/business/erp-integration/salesforce-rest-api/2026)
- [SAP S/4HANA OData API Capabilities](/business/erp-integration/sap-s4hana-odata-api/2026)
- [AP Automation Integration Playbook](/business/erp-integration/ap-automation-playbook/2026)
- [Order-to-Cash Integration Playbook](/business/erp-integration/order-to-cash-playbook/2026)
- [Saga Pattern for Distributed ERP Transactions](/business/erp-integration/saga-pattern-distributed-transactions/2026)
- [Idempotency Patterns for ERP Integrations](/business/erp-integration/idempotency-patterns/2026)
