---
# === IDENTITY ===
id: business/erp-integration/circuit-breaker-pattern-erp/2026
canonical_question: "How do you implement the circuit breaker pattern for ERP API integrations?"
aliases:
  - "Circuit breaker for ERP API calls"
  - "How to prevent cascading failures in ERP integrations"
  - "ERP API circuit breaker configuration — thresholds, states, fallbacks"
  - "When should I use circuit breaker vs retry vs bulkhead for ERP APIs?"
entity_type: erp_integration
domain: business > erp-integration > circuit-breaker-pattern-erp
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Cross-ERP (Pattern-level)"
    vendor: "N/A — architecture pattern"
    version: "N/A"
    edition: "All"
    deployment: "cloud"
    api_surface: "REST, SOAP, OData"
  - name: "Polly (.NET)"
    vendor: "App-vNext"
    version: "8.x"
    edition: "Open source"
    deployment: "cloud"
    api_surface: ".NET resilience library"
  - name: "Resilience4j (Java)"
    vendor: "Resilience4j"
    version: "2.x"
    edition: "Open source"
    deployment: "cloud"
    api_surface: "Java resilience library"
  - name: "Opossum (Node.js)"
    vendor: "Nodeshift"
    version: "8.x"
    edition: "Open source"
    deployment: "cloud"
    api_surface: "Node.js resilience library"

# === VERIFICATION ===
last_verified: 2026-03-07
confidence: 0.88
version: 1.0
first_published: 2026-03-07

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "Polly v8 removed legacy CircuitBreakerPolicy, replaced with ResiliencePipeline (2023)"
  next_review: 2026-09-03
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Circuit breaker does NOT replace retry — it prevents retries from hammering a failing service; combine both with retry inside breaker"
  - "A single circuit breaker per ERP endpoint is the correct granularity — one breaker per ERP system is too coarse, one per API call too fine"
  - "Circuit breaker state is per-process unless externalized to Redis/DB — horizontal scaling requires shared state or per-instance breakers"
  - "Open circuit must have a fallback strategy (queue, cache, error) — open circuit without fallback is just a faster failure"
  - "ERP maintenance windows (SAP, Oracle) will repeatedly trip breakers — schedule suppression windows or use health-check probes"
  - "Half-open state allows only limited probe requests — do not route production traffic through half-open circuits"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs retry with exponential backoff only, no cascading failure concern"
    use_instead: "business/erp-integration/error-handling-retry-comparison/2026"
  - condition: "User needs distributed transaction coordination across ERPs"
    use_instead: "business/erp-integration/saga-pattern-erp-transactions/2026"
  - condition: "User needs rate limiting or throttling, not failure detection"
    use_instead: "business/erp-integration/erp-rate-limits-comparison/2026"
  - condition: "User needs dead letter queue patterns for failed messages"
    use_instead: "business/erp-integration/error-handling-dead-letter-queues/2026"

# === AGENT HINTS ===
inputs_needed:
  - 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)"
      - "long-running multi-step business process"
  - key: language_platform
    question: "What language or platform are you using?"
    type: choice
    options:
      - "Python"
      - "Java / Spring Boot"
      - "C# / .NET"
      - "Node.js / TypeScript"
      - "iPaaS (MuleSoft, Boomi, Workato)"
  - key: failure_mode
    question: "What failure mode are you protecting against?"
    type: choice
    options:
      - "ERP API timeouts (>30s response)"
      - "Rate limit exhaustion (HTTP 429)"
      - "Intermittent 500 errors during maintenance"
      - "Complete ERP outage"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/circuit-breaker-pattern-erp/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-03-07)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: "business/erp-integration/error-handling-retry-comparison/2026"
      label: "Error Handling & Retry Patterns Comparison"
  related_to:
    - id: "business/erp-integration/error-handling-dead-letter-queues/2026"
      label: "Dead Letter Queue Patterns for ERP Integration"
    - id: "business/erp-integration/idempotency-erp-integrations/2026"
      label: "Idempotency in ERP Integrations"
    - id: "business/erp-integration/erp-rate-limits-comparison/2026"
      label: "ERP Rate Limits Comparison"
  solves:
    - id: "business/erp-integration/saga-pattern-erp-transactions/2026"
      label: "Saga Pattern — circuit breaker protects individual saga steps"
  alternative_to:
    - id: "business/erp-integration/batch-vs-realtime-integration/2026"
      label: "Batch vs Real-time — batch eliminates some circuit breaker needs"
  often_confused_with:
    - id: "business/erp-integration/erp-rate-limits-comparison/2026"
      label: "Rate limiting (server-side) vs circuit breaker (client-side)"

# === SOURCES ===
sources:
  - id: src1
    title: "Circuit Breaker Pattern — Azure Architecture Center"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker
    type: official_docs
    published: 2025-02-05
    reliability: authoritative
  - id: src2
    title: "Circuit Breaker Resilience Strategy — Polly Documentation"
    author: App-vNext
    url: https://www.pollydocs.org/strategies/circuit-breaker.html
    type: official_docs
    published: 2024-06-01
    reliability: authoritative
  - id: src3
    title: "Pattern: Circuit Breaker — Microservices.io"
    author: Chris Richardson
    url: https://microservices.io/patterns/reliability/circuit-breaker.html
    type: technical_blog
    published: 2024-01-15
    reliability: high
  - id: src4
    title: "Implementing the Circuit Breaker Pattern — .NET Microservices Architecture"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/implement-circuit-breaker-pattern
    type: official_docs
    published: 2022-03-09
    reliability: authoritative
  - id: src5
    title: "Opossum — Node.js Circuit Breaker"
    author: Nodeshift
    url: https://github.com/nodeshift/opossum
    type: community_resource
    published: 2025-01-10
    reliability: high
  - id: src6
    title: "Circuit Breaker Policy — MuleSoft Gateway Documentation"
    author: MuleSoft
    url: https://docs.mulesoft.com/gateway/latest/policies-outbound-circuit-breaker
    type: official_docs
    published: 2025-06-01
    reliability: authoritative
  - id: src7
    title: "Building Resilient Systems: Circuit Breakers and Retry Patterns"
    author: DasRoot.net
    url: https://dasroot.net/posts/2026/01/building-resilient-systems-circuit-breakers-retry-patterns/
    type: technical_blog
    published: 2026-01-15
    reliability: moderate_high
  - id: src8
    title: "Circuit Breaker and Retry with Resilience4j"
    author: Resilience4j Community
    url: https://resilience4j.readme.io/docs/getting-started-3
    type: official_docs
    published: 2024-09-01
    reliability: high
---

# Circuit Breaker Pattern for ERP API Integrations

## TL;DR

- **Bottom line**: Wrap every outbound ERP API call in a circuit breaker that trips after a configurable failure threshold, fails fast while the ERP recovers, and probes with limited half-open requests before restoring full traffic.
- **Key limit**: Circuit breaker is per-process state by default — horizontal scaling requires shared state (Redis) or per-instance breakers with coordinated thresholds.
- **Watch out for**: Setting thresholds too sensitive (trips on 2 failures) causes false opens during normal ERP latency spikes; too tolerant (50 failures) defeats the purpose. Start at 50% failure rate over 10-second windows with minimum 8 requests sampled.
- **Best for**: Real-time ERP API integrations where downstream unavailability (SAP maintenance, Salesforce governor limits, Oracle Cloud outages) would cascade into thread exhaustion, connection pool starvation, or saga timeout failures.
- **Libraries**: Python (custom or pybreaker), Java (Resilience4j 2.x), C# (Polly 8.x), Node.js (Opossum 8.x), iPaaS (MuleSoft gateway policy, Boomi custom scripting).

## System Profile

This card covers the circuit breaker pattern as applied to ERP API integrations across all major ERP systems. It is platform-agnostic but provides concrete implementations for the four major integration languages (Python, Java, C#, Node.js) and two leading iPaaS platforms (MuleSoft, Boomi). The pattern applies identically whether calling SAP S/4HANA OData, Salesforce REST, Oracle ERP Cloud REST, NetSuite SuiteTalk, or Dynamics 365 Web API. Per-ERP specifics (which error codes trip the breaker, typical recovery times) are covered in the error handling section.

| Property | Value |
|---|---|
| **Pattern** | Circuit Breaker (client-side resilience) |
| **Applies To** | All ERP REST/SOAP/OData API calls |
| **Granularity** | One breaker per ERP endpoint or API surface (not per ERP system) |
| **States** | Closed (normal) -> Open (failing fast) -> Half-Open (probing recovery) |
| **Implementations** | Polly 8.x (.NET), Resilience4j 2.x (Java), Opossum 8.x (Node.js), Custom (Python) |
| **iPaaS** | MuleSoft (gateway policy), Boomi (custom scripting), Workato (custom connector) |
| **Reference Architecture** | Microsoft Azure Architecture Center |

## API Surfaces & Capabilities

Circuit breakers protect calls to ERP API surfaces. Different API surfaces exhibit different failure modes and require different breaker configurations. [src1]

| ERP API Surface | Typical Failure Mode | Recommended Breaker Config | Recovery Time |
|---|---|---|---|
| SAP S/4HANA OData | 503 during planned downtime, timeouts on complex queries | 5 failures / 30s window, 120s break | 5-30 min (planned), 1-4h (incident) |
| Salesforce REST API | 429 rate limit, REQUEST_LIMIT_EXCEEDED, 503 | 3 consecutive 429s, 60s break aligned to rate limit reset | 60s (rate limit), 5-15 min (incident) |
| Oracle ERP Cloud REST | 500/503 during patching, FBDI timeouts | 5 failures / 60s window, 180s break | 15-60 min (patching), 1-2h (incident) |
| NetSuite SuiteTalk/REST | SSS_REQUEST_LIMIT_EXCEEDED, concurrency cap | 3 failures / 20s, 30s break | 30-60s (concurrency), 10-30 min (incident) |
| Dynamics 365 OData | 429 with Retry-After header, 503 during updates | Honor Retry-After header, 5 failures / 30s window | Per Retry-After value, 5-30 min (updates) |
| Workday REST/SOAP | 503 during tenant maintenance, auth token expiry | 5 failures / 60s, 120s break | 30-120 min (maintenance) |

## Rate Limits & Quotas

Circuit breakers interact with — but are distinct from — ERP rate limits. The breaker is a client-side pattern that protects the caller; rate limits are server-side enforcement. [src1, src3]

### Circuit Breaker Configuration Parameters

| Parameter | Description | Recommended Default | Notes |
|---|---|---|---|
| **Failure threshold** | Percentage or count of failures that trips the breaker | 50% failure rate OR 5 consecutive failures | Percentage-based (Polly, Resilience4j) is more robust than count-based |
| **Sampling window** | Time period over which failures are counted | 10-30 seconds | Too short = noise triggers opens; too long = slow detection |
| **Minimum throughput** | Minimum requests in window before threshold is evaluated | 8-10 requests | Prevents tripping on 1 failure out of 2 requests |
| **Break duration** | How long the circuit stays open before half-open probe | 30-120 seconds | Match to ERP typical recovery time |
| **Half-open probe count** | Number of test requests allowed in half-open state | 1-3 requests | Too many probes can re-overload a recovering service |
| **Success threshold** | Consecutive successes in half-open to close circuit | 3-5 successes | Ensures recovery is stable, not a single lucky request |
| **Timeout** | Per-request timeout that counts as failure | 30-60 seconds for ERP APIs | ERP APIs are slower than typical microservices — don't use 3s timeouts |

### Per-ERP Error Codes That Should Trip the Breaker

| ERP System | Trip On (Open Circuit) | Do NOT Trip On (Retry Instead) | Notes |
|---|---|---|---|
| **Salesforce** | 503, REQUEST_LIMIT_EXCEEDED, SERVER_UNAVAILABLE | 400 (bad request), INVALID_FIELD, DUPLICATE_VALUE | 429 — trip after 3 consecutive, not on first occurrence [src3] |
| **SAP S/4HANA** | 503, 504, CX_SY_RESOURCE_EXHAUSTION | 400, /IWBEP/CM_MGW_RT (OData validation) | Gateway timeout (504) indicates SAP app server overload [src1] |
| **Oracle ERP Cloud** | 503, 500 (repeated), FBDI import timeout | 400, ORA-00001 (unique constraint), validation errors | Distinguish transient 500 from persistent logic errors |
| **NetSuite** | SSS_REQUEST_LIMIT_EXCEEDED, SSS_CONCURRENT_LIMIT, 503 | USER_ERROR, INVALID_FLD_VALUE, SSS_MISSING_REQD_ARGUMENT | Governance limit errors are transient; validation errors are permanent |
| **Dynamics 365** | 429 (with Retry-After), 503, 502 | 400, 403, 404, -2147204784 (duplicate detection) | Always honor Retry-After header as minimum break duration [src1] |

## Authentication

Authentication failures interact with circuit breakers in specific ways. Token expiry should NOT trip the circuit — it should trigger a token refresh. [src1]

| Scenario | Should Trip Breaker? | Correct Handling |
|---|---|---|
| OAuth token expired (401) | No | Refresh token, retry once, then trip if refresh fails |
| API key invalid (403) | No | Alert immediately — this is a config error, not transient |
| Auth server unreachable | Yes | Trip breaker on auth endpoint separately from data endpoints |
| MFA challenge required | No | Alert — cannot be automated; indicates wrong auth flow |
| Rate limit on auth endpoint | Yes | Trip breaker; queue data requests until auth recovers |

## Constraints

<!-- Agents: read this section before recommending any circuit breaker implementation. -->

- **Circuit breaker does NOT replace retry** — Use retry for transient errors (first 2-3 attempts), then circuit breaker trips to prevent retry storms. Retry inside breaker, not breaker inside retry. [src1, src7]
- **Per-endpoint granularity** — One breaker per ERP API surface (e.g., separate breakers for Salesforce REST vs Salesforce Bulk). A single breaker for all Salesforce APIs means a Bulk API timeout opens the circuit for REST operations. [src3]
- **State isolation** — Circuit breaker state is in-memory by default. In Kubernetes with 10 pods, each pod has its own breaker. This means 10x the probe requests in half-open state. Externalize to Redis for shared state if needed. [src1]
- **Idempotency required for half-open probes** — Half-open probe requests may be duplicates of previously failed requests. Without idempotency keys, you risk duplicate records in the ERP. [src3]
- **Break duration must match ERP recovery** — SAP planned maintenance: 30-120 min. Salesforce rate limit reset: 60s. Oracle patching: 15-60 min. A 5-second break duration is useless for a 30-minute maintenance window. [src1]
- **Cannot circuit-break fire-and-forget** — If you use async message queues (Kafka, SQS), the queue acts as a natural buffer. Circuit breaker adds no value between queue consumer and queue — use it between consumer and ERP API. [src3]

## Integration Pattern Decision Tree

```
START — Should I use a circuit breaker for this ERP integration?
|
+-- Is the integration synchronous (real-time API call)?
|   +-- YES --> Circuit breaker is strongly recommended
|   |   +-- Is the ERP API call idempotent?
|   |   |   +-- YES --> Standard circuit breaker + retry
|   |   |   +-- NO --> Circuit breaker + idempotency key + DLQ
|   |   +-- Are you calling multiple ERP endpoints?
|   |       +-- YES --> Separate breaker per endpoint
|   |       +-- NO --> Single breaker sufficient
|   +-- NO (async / message-based) ↓
|
+-- Is there a synchronous ERP API call within the async flow?
|   +-- YES --> Circuit breaker on the API call, not the queue consumer
|   +-- NO --> Circuit breaker adds no value; use DLQ + retry instead
|
+-- Which resilience pattern do I need?
    +-- Transient errors (network blip, 1-2 failures) --> Retry with backoff
    +-- Sustained outage (ERP down for minutes) --> Circuit breaker
    +-- Protecting shared resources (thread pools) --> Bulkhead
    +-- Server-side request throttling --> Rate limiter
    +-- Combining all four --> Retry -> Circuit Breaker -> Bulkhead -> Timeout
        (Polly ResiliencePipeline / Resilience4j DecoratedSupplier)
```

## Quick Reference

| Pattern | Purpose | When to Use | Library |
|---|---|---|---|
| **Circuit Breaker** | Stop calling failing service, fail fast, auto-recover | ERP down or degraded for >30s | Polly, Resilience4j, Opossum |
| **Retry** | Retry transient failures with backoff | Network blip, single 500, rate limit | Polly, Resilience4j, tenacity |
| **Bulkhead** | Isolate resource pools per dependency | Multiple ERP integrations sharing thread pool | Polly, Resilience4j |
| **Rate Limiter** | Client-side request throttling | Stay under ERP API quota | Polly, Resilience4j, bottleneck |
| **Timeout** | Fail slow calls before thread exhaustion | ERP queries that can hang >60s | Built into all HTTP clients |
| **Fallback** | Return cached/default value when circuit open | Non-critical reads (e.g., product catalog) | Application logic |
| **Dead Letter Queue** | Capture failed writes for later replay | Write operations that must not be lost | Kafka DLQ, SQS DLQ, Azure Service Bus |

### Circuit Breaker States

| State | Behavior | Transitions To | Trigger |
|---|---|---|---|
| **Closed** | All requests pass through; failures counted | Open | Failure rate exceeds threshold in sampling window |
| **Open** | All requests fail immediately (BrokenCircuitException) | Half-Open | Break duration timer expires |
| **Half-Open** | Limited probe requests allowed | Closed OR Open | Probe succeeds (-> Closed) or fails (-> Open) |
| **Isolated** (Polly only) | Manually held open via API | Closed | Manual reset via ManualControl.CloseAsync() |

## Step-by-Step Integration Guide

### 1. Determine the correct granularity for your circuit breakers

Map your ERP API calls to circuit breaker instances. Each distinct endpoint or API surface gets its own breaker. [src1, src3]

```
ERP Integration Architecture:
  +-- Salesforce
  |   +-- CB: sf-rest      (REST API — accounts, contacts, opportunities)
  |   +-- CB: sf-bulk       (Bulk API 2.0 — data loads)
  |   +-- CB: sf-streaming  (Platform Events — webhooks)
  +-- SAP S/4HANA
  |   +-- CB: sap-odata     (OData v4 — business objects)
  |   +-- CB: sap-soap      (SOAP — legacy BAPIs)
  +-- Oracle ERP Cloud
      +-- CB: oracle-rest   (REST API — ERP modules)
      +-- CB: oracle-fbdi   (FBDI — file imports)
```

**Verify**: Each breaker should have independent state. Tripping `sf-bulk` should NOT affect `sf-rest`.

### 2. Configure thresholds based on ERP behavior

Start with conservative defaults, tune based on production telemetry. [src1, src2]

| Parameter | Conservative Start | Tuned After 30 Days |
|---|---|---|
| Failure rate threshold | 50% | Adjust based on baseline error rate |
| Sampling window | 30 seconds | Match to ERP API response time P99 |
| Minimum throughput | 10 | Match to actual request volume |
| Break duration | 60 seconds | Match to ERP typical recovery time |
| Half-open probes | 3 | Increase if recovery is gradual |

**Verify**: Monitor circuit state transitions for 7 days. If breaker trips >5x/day on a healthy ERP, thresholds are too sensitive.

### 3. Implement the circuit breaker in your language

See Code Examples section below for complete implementations in Python, Java, C#, and Node.js. [src2, src4, src5, src8]

### 4. Wire the fallback strategy for open circuit

When the circuit is open, the application must do something useful instead of throwing an exception. [src1]

```
Circuit Open — What to do with the request:
  +-- Is the operation a READ?
  |   +-- Return cached data (if fresh enough)
  |   +-- Return degraded response ("ERP data temporarily unavailable")
  |   +-- Route to secondary ERP instance (if available)
  +-- Is the operation a WRITE?
  |   +-- Queue to dead letter queue for later replay
  |   +-- Write to local staging table + reconcile later
  |   +-- Return 503 + Retry-After header to upstream caller
  +-- ALWAYS:
      +-- Log circuit state change with ERP endpoint, failure count, timestamp
      +-- Fire alert if circuit has been open >5 minutes
      +-- Increment circuit-open counter in metrics (Prometheus/Datadog/CloudWatch)
```

**Verify**: Simulate ERP downtime in staging. Confirm writes go to DLQ and reads return cached/degraded data.

## Code Examples

### Python: Circuit Breaker for ERP API Calls

```python
# Input:  ERP API endpoint URL, authentication headers
# Output: API response or fallback value when circuit is open
# Requires: requests>=2.31.0

import time
import threading
import requests
from enum import Enum
from dataclasses import dataclass, field

class CircuitState(Enum):
    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"

@dataclass
class CircuitBreaker:
    """Circuit breaker for ERP API calls. Thread-safe."""
    failure_threshold: int = 5       # failures before opening
    recovery_timeout: float = 60.0   # seconds in open state
    half_open_max_calls: int = 3     # probe requests allowed
    success_threshold: int = 3       # successes to close from half-open
    erp_timeout: float = 30.0        # per-request timeout (ERP APIs are slow)

    _state: CircuitState = field(default=CircuitState.CLOSED, init=False)
    _failure_count: int = field(default=0, init=False)
    _success_count: int = field(default=0, init=False)
    _last_failure_time: float = field(default=0.0, init=False)
    _half_open_calls: int = field(default=0, init=False)
    _lock: threading.Lock = field(default_factory=threading.Lock, init=False)

    # ERP-specific error codes that should trip the breaker
    TRIP_STATUS_CODES = {429, 500, 502, 503, 504}
    # Error codes that should NOT trip (permanent errors, not transient)
    SKIP_STATUS_CODES = {400, 401, 403, 404, 409, 422}

    def call(self, method, url, fallback=None, **kwargs):
        """Execute an ERP API call through the circuit breaker."""
        with self._lock:
            if self._state == CircuitState.OPEN:
                if time.time() - self._last_failure_time >= self.recovery_timeout:
                    self._state = CircuitState.HALF_OPEN
                    self._half_open_calls = 0
                    self._success_count = 0
                else:
                    if fallback is not None:
                        return fallback()
                    raise CircuitOpenError(
                        f"Circuit open — ERP unavailable. "
                        f"Retry after {self.recovery_timeout}s"
                    )
            if self._state == CircuitState.HALF_OPEN:
                if self._half_open_calls >= self.half_open_max_calls:
                    if fallback is not None:
                        return fallback()
                    raise CircuitOpenError("Half-open probe limit reached")
                self._half_open_calls += 1

        # Execute the actual ERP API call
        try:
            kwargs.setdefault("timeout", self.erp_timeout)
            response = requests.request(method, url, **kwargs)

            if response.status_code in self.TRIP_STATUS_CODES:
                self._record_failure()
                response.raise_for_status()

            if response.status_code not in self.SKIP_STATUS_CODES:
                self._record_success()

            return response

        except (requests.Timeout, requests.ConnectionError) as e:
            self._record_failure()
            if fallback is not None:
                return fallback()
            raise

    def _record_failure(self):
        with self._lock:
            self._failure_count += 1
            self._last_failure_time = time.time()
            if self._state == CircuitState.HALF_OPEN:
                self._state = CircuitState.OPEN  # probe failed, reopen
            elif self._failure_count >= self.failure_threshold:
                self._state = CircuitState.OPEN

    def _record_success(self):
        with self._lock:
            if self._state == CircuitState.HALF_OPEN:
                self._success_count += 1
                if self._success_count >= self.success_threshold:
                    self._state = CircuitState.CLOSED
                    self._failure_count = 0

    @property
    def state(self) -> CircuitState:
        return self._state

class CircuitOpenError(Exception):
    pass

# Usage: one breaker per ERP endpoint
sf_breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=60)
sap_breaker = CircuitBreaker(failure_threshold=5, recovery_timeout=120)

# Example: Salesforce REST API call with circuit breaker
response = sf_breaker.call(
    "GET",
    "https://myorg.my.salesforce.com/services/data/v62.0/query",
    params={"q": "SELECT Id, Name FROM Account LIMIT 10"},
    headers={"Authorization": "Bearer <token>"},
    fallback=lambda: {"records": [], "note": "Salesforce unavailable — cached data"}
)
```

### Java: Resilience4j Circuit Breaker for ERP APIs

```java
// Input:  ERP API endpoint, authentication config
// Output: API response or fallback when circuit is open
// Requires: io.github.resilience4j:resilience4j-circuitbreaker:2.2.0
//           io.github.resilience4j:resilience4j-retry:2.2.0

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.vavr.control.Try;
import java.time.Duration;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

// Configure per-ERP circuit breakers
CircuitBreakerConfig sapConfig = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)                           // 50% failure rate trips
    .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED)
    .slidingWindowSize(30)                              // 30-second window
    .minimumNumberOfCalls(8)                            // need 8 calls minimum
    .waitDurationInOpenState(Duration.ofSeconds(120))   // SAP: 2 min recovery
    .permittedNumberOfCallsInHalfOpenState(3)           // 3 probe requests
    .recordExceptions(java.net.ConnectException.class,
                      java.net.http.HttpTimeoutException.class)
    .recordResult(response ->                            // trip on 429/500/503
        response instanceof HttpResponse<?> hr &&
        (hr.statusCode() == 429 || hr.statusCode() >= 500))
    .ignoreResult(response ->                            // don't trip on 400/404
        response instanceof HttpResponse<?> hr &&
        hr.statusCode() >= 400 && hr.statusCode() < 500 &&
        hr.statusCode() != 429)
    .build();

CircuitBreakerConfig sfConfig = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)
    .slidingWindowSize(10)                              // Salesforce: 10s window
    .waitDurationInOpenState(Duration.ofSeconds(60))    // 60s = rate limit reset
    .permittedNumberOfCallsInHalfOpenState(3)
    .minimumNumberOfCalls(8)
    .build();

CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(sapConfig);
CircuitBreaker sapBreaker = registry.circuitBreaker("sap-odata", sapConfig);
CircuitBreaker sfBreaker = registry.circuitBreaker("sf-rest", sfConfig);

// Wire retry inside circuit breaker (retry first, then trip)
RetryConfig retryConfig = RetryConfig.custom()
    .maxAttempts(3)
    .waitDuration(Duration.ofSeconds(2))
    .retryExceptions(java.net.ConnectException.class)
    .build();
Retry retry = Retry.of("sap-retry", retryConfig);

// Compose: Retry -> CircuitBreaker -> API call
HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(10))
    .build();

var decoratedCall = CircuitBreaker.decorateCheckedSupplier(sapBreaker, () -> {
    HttpRequest request = HttpRequest.newBuilder()
        .uri(java.net.URI.create(
            "https://my-sap.s4hana.cloud/sap/opu/odata4/sap/api_business_partner/A_BusinessPartner"))
        .header("Authorization", "Bearer " + token)
        .timeout(Duration.ofSeconds(30))
        .build();
    return client.send(request, HttpResponse.BodyHandlers.ofString());
});

// Execute with fallback
Try.ofCheckedSupplier(Retry.decorateCheckedSupplier(retry, decoratedCall))
    .recover(throwable -> fallbackResponse());
```

### C#: Polly 8.x Circuit Breaker for ERP APIs

```csharp
// Input:  ERP API endpoint, HttpClient with auth
// Output: API response or fallback when circuit is open
// Requires: Microsoft.Extensions.Http.Resilience 8.x, Polly 8.x

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http.Resilience;
using Polly;
using Polly.CircuitBreaker;
using System.Net;

// Configure in DI container (Program.cs)
var services = new ServiceCollection();

services.AddHttpClient("SapOData", client =>
{
    client.BaseAddress = new Uri("https://my-sap.s4hana.cloud/sap/opu/odata4/");
    client.Timeout = TimeSpan.FromSeconds(60); // ERP APIs need longer timeouts
})
.AddResilienceHandler("sap-pipeline", builder =>
{
    // Retry first (inside circuit breaker)
    builder.AddRetry(new HttpRetryStrategyOptions
    {
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromSeconds(2),
        BackoffType = DelayBackoffType.Exponential,
        ShouldHandle = args => ValueTask.FromResult(
            args.Outcome.Result?.StatusCode is HttpStatusCode.ServiceUnavailable
            || args.Outcome.Exception is HttpRequestException)
    });

    // Circuit breaker wraps the retry
    builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
    {
        FailureRatio = 0.5,                                    // 50% failure rate
        SamplingDuration = TimeSpan.FromSeconds(30),           // 30s window
        MinimumThroughput = 8,                                 // need 8 requests minimum
        BreakDuration = TimeSpan.FromSeconds(120),             // SAP: 2 min recovery
        ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
            .HandleResult(r => r.StatusCode == HttpStatusCode.TooManyRequests
                            || r.StatusCode >= HttpStatusCode.InternalServerError)
            .Handle<HttpRequestException>()
            .Handle<TaskCanceledException>(),

        // Dynamic break duration based on Retry-After header (D365, Salesforce)
        BreakDurationGenerator = static args =>
        {
            var retryAfter = args.Outcome.Result?.Headers?.RetryAfter;
            if (retryAfter?.Delta is TimeSpan delta)
                return ValueTask.FromResult(delta);
            return ValueTask.FromResult(TimeSpan.FromSeconds(120));
        },

        OnOpened = args =>
        {
            Console.WriteLine($"[CIRCUIT OPEN] SAP OData — {args.Outcome.Exception?.Message}");
            // Fire alert, increment Prometheus counter
            return ValueTask.CompletedTask;
        },
        OnClosed = args =>
        {
            Console.WriteLine("[CIRCUIT CLOSED] SAP OData — recovered");
            return ValueTask.CompletedTask;
        }
    });

    // Timeout as outer wrapper
    builder.AddTimeout(TimeSpan.FromSeconds(60));
});
```

### Node.js: Opossum Circuit Breaker for ERP APIs

```javascript
// Input:  ERP API endpoint, fetch options with auth
// Output: API response or fallback when circuit is open
// Requires: opossum@8.1.3, node-fetch@3.3.2

import CircuitBreaker from "opossum";

// ERP API call function (the "action" opossum wraps)
async function callSalesforceApi(endpoint, options) {
  const response = await fetch(
    `https://myorg.my.salesforce.com/services/data/v62.0/${endpoint}`,
    { ...options, signal: AbortSignal.timeout(30000) } // 30s timeout for ERP
  );
  if (response.status === 429 || response.status >= 500) {
    throw new Error(`Salesforce API error: ${response.status}`);
  }
  return response.json();
}

// Create circuit breaker per ERP endpoint
const sfBreaker = new CircuitBreaker(callSalesforceApi, {
  timeout: 30000,                // 30s — ERP APIs are slow
  errorThresholdPercentage: 50,  // 50% failure rate trips the breaker
  resetTimeout: 60000,           // 60s open state (Salesforce rate limit reset)
  rollingCountTimeout: 10000,    // 10s sliding window
  rollingCountBuckets: 10,       // 10 buckets in the window
  volumeThreshold: 5,            // minimum 5 requests before evaluating
});

// Fallback when circuit is open
sfBreaker.fallback((endpoint, options) => ({
  records: [],
  _circuitOpen: true,
  _message: "Salesforce unavailable — returning empty result"
}));

// Monitor circuit state changes
sfBreaker.on("open", () =>
  console.warn("[CIRCUIT OPEN] Salesforce REST API — failing fast"));
sfBreaker.on("halfOpen", () =>
  console.info("[CIRCUIT HALF-OPEN] Salesforce — probing recovery"));
sfBreaker.on("close", () =>
  console.info("[CIRCUIT CLOSED] Salesforce — recovered"));

// Usage
const accounts = await sfBreaker.fire("query?q=SELECT+Id,Name+FROM+Account", {
  headers: { Authorization: `Bearer ${accessToken}` }
});
```

## Data Mapping

### Circuit Breaker Configuration Mapping Across Libraries

| Parameter | Polly 8.x (C#) | Resilience4j 2.x (Java) | Opossum 8.x (Node.js) | Python (Custom) |
|---|---|---|---|---|
| Failure rate | `FailureRatio` (0.0-1.0) | `failureRateThreshold` (0-100%) | `errorThresholdPercentage` (0-100) | `failure_threshold` (count) |
| Sampling window | `SamplingDuration` (TimeSpan) | `slidingWindowSize` (int seconds) | `rollingCountTimeout` (ms) | Custom (time.time()) |
| Minimum throughput | `MinimumThroughput` (int) | `minimumNumberOfCalls` (int) | `volumeThreshold` (int) | `failure_threshold` (implicit) |
| Break duration | `BreakDuration` (TimeSpan) | `waitDurationInOpenState` (Duration) | `resetTimeout` (ms) | `recovery_timeout` (float seconds) |
| Half-open probes | Not configurable (1 probe) | `permittedNumberOfCallsInHalfOpenState` | Not configurable (1 probe) | `half_open_max_calls` (int) |
| Dynamic break | `BreakDurationGenerator` (delegate) | Not built-in (custom) | Not built-in | Custom logic |
| Manual control | `ManualControl.IsolateAsync()` | `CircuitBreaker.transitionToForcedOpenState()` | `breaker.open()` / `breaker.close()` | Direct state mutation |

### Data Type Gotchas

- **Polly uses ratio (0.0-1.0), Resilience4j uses percentage (0-100)** — a `FailureRatio` of 0.5 in Polly equals `failureRateThreshold(50)` in Resilience4j. Mixing these up means 50x more or less sensitive than intended. [src2, src8]
- **Opossum rollingCountTimeout is in milliseconds, Resilience4j slidingWindowSize is in seconds** — `10000` in Opossum equals `10` in Resilience4j. Off by 1000x if you port config between libraries. [src5, src8]
- **Polly 8.x SamplingDuration replaced Polly 7.x durationOfBreak semantics** — if migrating from Polly v7, the entire configuration model changed. [src2]

## Error Handling & Failure Points

### Common Error Codes

| Code | Meaning | Should Trip Breaker? | Resolution |
|---|---|---|---|
| 429 | Rate limit exceeded | Yes (after 3 consecutive) | Backoff, respect Retry-After, align break duration to rate limit window |
| 500 | Internal Server Error | Yes (if repeated) | Trip after 3-5 occurrences in window; single 500 could be transient |
| 502 | Bad Gateway | Yes | Indicates proxy/load balancer failure; ERP app server likely down |
| 503 | Service Unavailable | Yes (immediately) | ERP is explicitly telling you to stop; trip immediately |
| 504 | Gateway Timeout | Yes | ERP app server overloaded; requests are queuing |
| 400 | Bad Request | No | Fix the request payload; this is a code bug, not an ERP outage |
| 401 | Unauthorized | No | Refresh auth token; trip only if refresh also fails |
| 403 | Forbidden | No | Permission issue; alert, don't trip |
| 404 | Not Found | No | Wrong endpoint; fix code |
| 409 | Conflict | No | Concurrent update; retry with merge strategy |

[src1, src3]

### Failure Points in Production

- **False opens during ERP maintenance windows**: SAP systems have known maintenance windows (e.g., 02:00-04:00 UTC Sundays). Circuit trips, alerts fire, on-call engineer wakes up. Fix: `Implement maintenance window suppression — skip alerting during scheduled windows but still protect the circuit.` [src1]
- **Token expiry cascade**: OAuth token expires, all requests get 401, breaker trips. Meanwhile, a single token refresh would fix everything. Fix: `Exclude 401 from circuit breaker; implement a separate token refresh circuit with its own breaker.` [src1, src3]
- **Half-open probe creates duplicate record**: Circuit opens during a write operation. Half-open probe retries the write without an idempotency key, creating a duplicate invoice in NetSuite. Fix: `Every ERP write operation must include an idempotency key (externalId in NetSuite, External_ID__c in Salesforce).` [src3]
- **Shared circuit breaker across microservices**: All 10 microservice instances share one Redis-backed circuit. One instance's local network issue trips the shared circuit, blocking all 10 instances. Fix: `Use a hybrid approach — local breaker per instance with shared metrics aggregation for alerting.` [src1]
- **Break duration too short for Oracle patching**: Oracle ERP Cloud patching takes 15-60 minutes. Circuit opens, half-open probe at 30 seconds fails, circuit re-opens. Cycle repeats 120 times during a 60-minute patch. Fix: `Implement exponential break duration — double break time on each re-trip: 30s → 60s → 120s → 240s, max 600s.` [src1]
- **Thread exhaustion before circuit trips**: Default timeout of 30 seconds with threshold of 5 failures means 5 threads blocked for 30 seconds each before circuit opens. With 10 concurrent requests, thread pool is exhausted. Fix: `Set aggressive per-request timeout (10-15s) separate from circuit break duration. Use bulkhead to limit concurrent ERP calls.` [src7]

## Anti-Patterns

### Wrong: Circuit breaker on idempotent writes without DLQ

```python
# BAD — writes are silently dropped when circuit opens
breaker = CircuitBreaker(failure_threshold=5)
try:
    breaker.call("POST", f"{erp_url}/invoices", json=invoice_data)
except CircuitOpenError:
    logger.warning("Circuit open — invoice not created")
    # Invoice is LOST. No retry. No queue. Gone forever.
```

### Correct: Circuit breaker with dead letter queue for writes

```python
# GOOD — failed writes are queued for later replay
breaker = CircuitBreaker(failure_threshold=5)
try:
    breaker.call("POST", f"{erp_url}/invoices", json=invoice_data)
except CircuitOpenError:
    logger.warning("Circuit open — queueing invoice to DLQ")
    dlq.send({
        "endpoint": f"{erp_url}/invoices",
        "method": "POST",
        "payload": invoice_data,
        "idempotency_key": invoice_data["externalId"],
        "queued_at": datetime.utcnow().isoformat()
    })
```

### Wrong: Single circuit breaker for all ERP endpoints

```javascript
// BAD — Bulk API timeout trips the breaker for REST API too
const erpBreaker = new CircuitBreaker(callAnyErpApi, {
  timeout: 30000,
  errorThresholdPercentage: 50,
});
// Bulk import hangs for 60s → breaker trips → REST queries fail too
await erpBreaker.fire("bulk/import", bulkPayload);
await erpBreaker.fire("query/accounts", {}); // BLOCKED — same breaker!
```

### Correct: Separate circuit breaker per API surface

```javascript
// GOOD — each API surface has its own breaker with tailored config
const sfRestBreaker = new CircuitBreaker(callSfRest, {
  timeout: 15000,                // REST is fast
  errorThresholdPercentage: 50,
  resetTimeout: 60000,
});
const sfBulkBreaker = new CircuitBreaker(callSfBulk, {
  timeout: 300000,               // Bulk can take 5 minutes
  errorThresholdPercentage: 50,
  resetTimeout: 120000,
});
// Bulk timeout does NOT affect REST operations
```

### Wrong: Too-sensitive threshold on high-latency ERP APIs

```yaml
# BAD — ERP APIs are NOT microservices; P99 latency is 5-10s, not 100ms
resilience4j:
  circuitbreaker:
    instances:
      sap-odata:
        failureRateThreshold: 20      # trips on 20% failures — too sensitive
        slidingWindowSize: 5           # only 5 seconds — way too short
        minimumNumberOfCalls: 2        # 2 calls! One slow response = open
        waitDurationInOpenState: 5s    # 5 seconds — SAP needs minutes
```

### Correct: Thresholds calibrated for ERP latency profiles

```yaml
# GOOD — tuned for real ERP behavior
resilience4j:
  circuitbreaker:
    instances:
      sap-odata:
        failureRateThreshold: 50      # 50% — tolerates occasional errors
        slidingWindowSize: 30          # 30 seconds — enough to see a pattern
        minimumNumberOfCalls: 8        # need real sample before judging
        waitDurationInOpenState: 120s  # SAP recovery takes minutes, not seconds
        permittedNumberOfCallsInHalfOpenState: 3
```

## Common Pitfalls

- **Treating 401 as a circuit-tripping failure**: Token expiry causes 401, breaker trips, all ERP calls blocked. Meanwhile, a single token refresh would restore service. Fix: `Exclude 401 from breaker; handle auth separately with its own refresh logic.` [src1]
- **Using microservice-scale timeouts for ERP APIs**: ERP APIs routinely take 5-15 seconds for complex queries. A 3-second timeout (common in microservice defaults) causes false timeouts that trip the breaker. Fix: `Set ERP-specific timeouts: 30s for REST, 60s for bulk operations, 120s for FBDI/file imports.` [src1, src7]
- **Not implementing exponential break duration**: Fixed 30-second break during a 2-hour SAP outage means the breaker trips and reopens every 30 seconds — 240 unnecessary probe requests. Fix: `Implement exponential backoff on break duration: 30s → 60s → 120s → 240s, capped at 600s.` [src1]
- **Circuit breaker without monitoring**: The breaker trips and nobody knows. Fix: `Log every state transition, expose circuit state as a health check endpoint, set alerts for circuits open >5 minutes.` [src1, src7]
- **Applying circuit breaker to queue consumers**: A Kafka consumer with a circuit breaker on the consume side stops consuming but messages keep arriving, eventually filling the partition. Fix: `Apply breaker between consumer and ERP API, not on the consumer itself. Let the consumer keep consuming and route to DLQ when circuit is open.` [src3]
- **Sharing state across microservice instances without coordination**: Redis-backed shared breaker means one instance's network blip can trip the circuit for all instances. Fix: `Use local breakers with shared metrics. If >50% of instances report failures, trip a global circuit via feature flag.` [src1]

## Diagnostic Commands

```bash
# Check Resilience4j circuit breaker state (Spring Boot Actuator)
curl -s http://localhost:8080/actuator/circuitbreakers | jq '.circuitBreakers'
# Expected: {"sap-odata":{"state":"CLOSED","failureRate":-1.0}}

# Check Resilience4j circuit breaker events
curl -s http://localhost:8080/actuator/circuitbreakerevents | jq '.circuitBreakerEvents[-5:]'

# Check Polly circuit state via health check endpoint (custom)
curl -s http://localhost:5000/health/circuits | jq
# Expected: {"sap-odata":"Closed","sf-rest":"Closed","oracle-rest":"HalfOpen"}

# Monitor Opossum circuit stats
# In Node.js: console.log(sfBreaker.stats)
# Exposes: {failures, successes, rejects, fires, timeouts, latencyMean}

# Test ERP API health directly (bypassing circuit breaker)
# Salesforce
curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer $SF_TOKEN" \
  "https://myorg.my.salesforce.com/services/data/v62.0/limits"

# SAP S/4HANA
curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer $SAP_TOKEN" \
  "https://my-sap.s4hana.cloud/sap/opu/odata4/sap/api_business_partner/\$metadata"

# Check if ERP is in maintenance (manual probe)
curl -s -w "\n%{http_code} %{time_total}s" \
  -H "Authorization: Bearer $TOKEN" \
  "$ERP_API_URL/health" 2>&1
# 503 = maintenance; 200 with >10s = degraded; 200 with <2s = healthy
```

## Version History & Compatibility

| Library | Version | Release | Breaking Changes | Notes |
|---|---|---|---|---|
| Polly | 8.x | 2023-07 | Complete API rewrite — `Policy` replaced by `ResiliencePipeline` | Cannot mix Polly 7 and 8 policies |
| Polly | 7.x | 2019-06 | Legacy — maintenance only | Still widely used; plan migration |
| Resilience4j | 2.2.0 | 2024-03 | Minor — added `TIME_BASED` sliding window improvements | Recommended for new Java projects |
| Resilience4j | 1.x | 2020-01 | EOL | Migrate to 2.x; API largely compatible |
| Opossum | 8.1.3 | 2025-01 | Minor — improved TypeScript types | Stable; primary Node.js option |
| Opossum | 7.x | 2023-01 | `rollingCountTimeout` default changed | Check config on upgrade |

### iPaaS Circuit Breaker Support

| Platform | Built-in Circuit Breaker? | Configuration Level | Notes |
|---|---|---|---|
| **MuleSoft** | Yes — gateway policy | API Gateway (Envoy-based) | Configures maxConnections, maxPendingRequests, maxRequests [src6] |
| **Boomi** | No — custom scripting | Process level (Groovy/JS) | Implement in Data Process shape with custom state management |
| **Workato** | No — custom connector | Connector SDK | Build circuit breaker logic in custom connector actions |
| **Celigo** | No — retry only | Flow level | Use external circuit breaker (e.g., Redis-backed) or implement in hook scripts |
| **SAP Integration Suite** | Partial — retry + timeout | iFlow level | No native circuit breaker; use Groovy script with JCache for state |

[src6]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Synchronous ERP API calls that can cascade failures | Asynchronous message-based integration (Kafka, SQS) | Dead letter queue + retry policy |
| ERP has known maintenance windows causing extended downtime | Single transient error that resolves on retry | Simple retry with exponential backoff |
| Multiple downstream ERP endpoints with independent failure modes | All ERP calls go through a single gateway that handles resilience | Gateway-level circuit breaker (MuleSoft policy) |
| Integration runs as a long-lived service processing continuous requests | One-time batch job that runs once and exits | Retry + error log; circuit breaker has no value for single execution |
| Thread pool or connection pool could be exhausted by slow ERP | Fire-and-forget writes where losing data is acceptable | Async fire-and-forget with logging |
| Saga pattern with multiple ERP steps — each step needs protection | Simple two-system point-to-point integration | Retry + DLQ (circuit breaker is overkill) |

## Cross-System Comparison

### iPaaS Circuit Breaker Configuration

| Capability | MuleSoft | Boomi | Workato | SAP Integration Suite |
|---|---|---|---|---|
| **Native circuit breaker** | Yes (gateway policy) | No | No | No |
| **Configuration** | YAML declarative | Groovy scripting | Custom connector SDK | Groovy in iFlow |
| **Granularity** | Per API / per upstream | Per process | Per connector | Per iFlow |
| **States** | Open / Closed (no half-open) | Custom (if implemented) | Custom | Custom |
| **Thresholds** | maxConnections, maxRequests, maxRetries | Custom logic | Custom logic | Custom logic |
| **Monitoring** | Anypoint Monitoring | Process Reporting | Activity Log | Cloud ALM |
| **Shared state** | Per Envoy instance | Per Atom | Per agent | Per tenant |

### Library Comparison for Custom Implementations

| Capability | Polly 8.x | Resilience4j 2.x | Opossum 8.x | pybreaker 1.x |
|---|---|---|---|---|
| **Language** | C# / .NET | Java / Kotlin | Node.js / TypeScript | Python |
| **Circuit states** | 4 (Closed, Open, HalfOpen, Isolated) | 3 (Closed, Open, HalfOpen) | 3 (Closed, Open, HalfOpen) | 3 (Closed, Open, HalfOpen) |
| **Sliding window** | Time-based | Time-based or Count-based | Time-based (rolling buckets) | Count-based |
| **Failure detection** | Predicate on result + exception | Predicate + record/ignore | Error threshold percentage | Exception counting |
| **Dynamic break** | BreakDurationGenerator delegate | Custom (extend) | Not built-in | Custom (extend) |
| **HttpClient integration** | Native (IHttpClientFactory) | Spring WebClient | Not built-in (manual wrap) | Not built-in |
| **Monitoring** | Built-in telemetry | Actuator + Micrometer | Events + Prometheus plugin | Custom events |
| **Bulkhead** | Yes (same pipeline) | Yes (separate decorator) | No (use separate library) | No |
| **Rate limiter** | Yes (same pipeline) | Yes (separate decorator) | No | No |
| **Maturity** | Very high | Very high | High | Moderate |

[src2, src5, src8]

## Important Caveats

- ERP APIs have fundamentally different latency profiles than microservices (5-15s P95 vs 100ms P95) — do not use microservice-default circuit breaker configurations, or the breaker will trip on normal ERP behavior [src1]
- Circuit breaker state is lost on process restart — if your integration service restarts while the ERP is still down, the circuit starts Closed and must re-learn the failure state. Consider persisting last-known state. [src1, src3]
- Different ERP editions have different rate limits (e.g., Salesforce Enterprise: 100K/24h vs Developer: 15K/24h) — breaker thresholds should account for edition-specific limits [src3]
- In multi-tenant iPaaS deployments, circuit breaker state for one tenant's ERP should not affect another tenant's circuit. Tenant isolation is critical. [src6]
- Library versions change configuration APIs significantly (Polly 7 vs 8 is a complete rewrite) — pin library versions and test circuit breaker behavior after any upgrade [src2, src4]
- This card covers the pattern and library implementations as of March 2026. ERP API error codes, rate limits, and maintenance window schedules are subject to change with each ERP release. Always verify against current ERP release notes.

## Related Units

- [Error Handling & Retry Patterns Comparison](/business/erp-integration/error-handling-retry-comparison/2026)
- [Dead Letter Queue Patterns for ERP Integration](/business/erp-integration/error-handling-dead-letter-queues/2026)
- [Idempotency in ERP Integrations](/business/erp-integration/idempotency-erp-integrations/2026)
- [ERP Rate Limits Comparison](/business/erp-integration/erp-rate-limits-comparison/2026)
- [Saga Pattern for Distributed Transactions Across ERPs](/business/erp-integration/saga-pattern-erp-transactions/2026)
- [Batch vs Real-time Integration](/business/erp-integration/batch-vs-realtime-integration/2026)
