Utopia Circuit Breaker is a simple and lite library for protecting PHP applications from cascading failures when a downstream dependency misbehaves. The breaker tracks failures, short-circuits calls when a service is unhealthy, and gradually probes recovery — with optional shared state (Redis / Swoole Table) and native telemetry via utopia-php/telemetry. This library is aiming to be as simple and easy to learn and use. This library is maintained by the Appwrite team.
Although this library is part of the Utopia Framework project it is dependency free and can be used as standalone with any other PHP project or framework.
Install using composer:
composer require utopia-php/circuit-breakerInit in your PHP code:
require_once __DIR__ . '/vendor/autoload.php';
use Utopia\CircuitBreaker\CircuitBreaker;
$breaker = new CircuitBreaker(
threshold: 3, // Open circuit after 3 failures
timeout: 30, // Try half-open after 30 seconds
successThreshold: 2 // Require 2 successes to close circuit
);
$result = $breaker->call(
open: fn () => 'Service unavailable - circuit is open',
close: fn () => makeExternalApiCall(),
halfOpen: fn () => makeExternalApiCall() // Optional: called during recovery testing
);The circuit breaker operates in three states:
- CLOSED (normal operation) — calls pass through to the protected service. Failures are counted; once they reach
threshold, the circuit transitions to OPEN. - OPEN (blocking) — calls are immediately short-circuited to the
opencallback (your fallback). Aftertimeoutseconds the circuit transitions to HALF_OPEN. - HALF_OPEN (probing recovery) — the next calls execute the
halfOpencallback (orcloseif not provided). AftersuccessThresholdconsecutive successes the circuit transitions back to CLOSED; any failure immediately re-opens it.
The optional halfOpen callback lets you apply different behaviour while probing (shorter timeouts, smaller payloads, extra logging).
use Utopia\CircuitBreaker\CircuitBreaker;
$breaker = new CircuitBreaker(threshold: 3, timeout: 30, successThreshold: 2);
$result = $breaker->call(
open: function () {
// Circuit is OPEN — service is down
logger()->warning('Circuit breaker is OPEN - using fallback');
return getCachedData() ?? ['error' => 'Service unavailable'];
},
close: function () {
// Circuit is CLOSED — normal operation
return apiClient()->fetchData();
},
halfOpen: function () {
// Circuit is HALF_OPEN — testing recovery
logger()->info('Circuit breaker testing recovery...');
return apiClient()->fetchData(['timeout' => 5]);
}
);use Utopia\CircuitBreaker\CircuitBreaker;
$breaker = new CircuitBreaker(threshold: 5, timeout: 60, successThreshold: 2);
$data = $breaker->call(
open: fn () => cache()->get('user_data') ?? ['error' => 'Service temporarily unavailable'],
close: function () {
$response = Http::get('https://api.example.com/users');
if (!$response->successful()) {
throw new \Exception('API request failed');
}
return $response->json();
}
);By default, each CircuitBreaker instance keeps state in memory. To share circuit state between PHP workers, pass a cache adapter and a stable cacheKey.
use Utopia\CircuitBreaker\Adapter\Redis as RedisAdapter;
use Utopia\CircuitBreaker\CircuitBreaker;
$redis = new \Redis();
$redis->connect('127.0.0.1');
$breaker = new CircuitBreaker(
threshold: 5,
timeout: 60,
successThreshold: 2,
cache: new RedisAdapter($redis),
cacheKey: 'users-api'
);Use the Swoole adapter when workers need to share state through Swoole shared memory.
use Utopia\CircuitBreaker\Adapter\SwooleTable;
use Utopia\CircuitBreaker\CircuitBreaker;
$table = SwooleTable::createTable(size: 1024);
$cache = new SwooleTable($table);
$breaker = new CircuitBreaker(
threshold: 5,
timeout: 60,
successThreshold: 2,
cache: $cache,
cacheKey: 'users-api'
);Telemetry is opt-in. The telemetry constructor argument defaults to null, which emits no metrics and does not require utopia-php/telemetry at runtime. Install utopia-php/telemetry and pass any adapter to emit counters and gauges for calls, fallbacks, callback failures, transitions, state, failure counts, success counts, active calls, and transition/probe events.
composer require utopia-php/telemetryuse Utopia\CircuitBreaker\CircuitBreaker;
use Utopia\Telemetry\Adapter\OpenTelemetry;
$telemetry = new OpenTelemetry(
'http://otel-collector:4318/v1/metrics',
'backend',
'orders',
gethostname() ?: 'local'
);
$breaker = new CircuitBreaker(
threshold: 5,
timeout: 60,
successThreshold: 2,
cacheKey: 'orders-api',
telemetry: $telemetry,
metricPrefix: 'backend'
);
$result = $breaker->call(
open: fn () => ['fallback' => true],
close: fn () => $client->request('/orders')
);
$telemetry->collect();By default, metrics are emitted as breaker.*. Pass metricPrefix to namespace those metric names for a host application; for example metricPrefix: 'backend' emits backend.breaker.calls.
You can also attach or replace the adapter after construction:
$breaker = new CircuitBreaker(metricPrefix: 'backend');
$breaker->setTelemetry($telemetry);threshold(int, default3) — failures tolerated before opening the circuittimeout(int, default30) — seconds to wait before transitioning to half-opensuccessThreshold(int, default2) — consecutive half-open successes required to closecache(?Utopia\CircuitBreaker\Adapter, defaultnull) — optional shared cache adaptercacheKey(string, defaultdefault) — cache namespace for one circuit's statetelemetry(?Utopia\Telemetry\Adapter, defaultnull) — optional telemetry adaptermetricPrefix(string, default'') — optional prefix for telemetry metric names (e.g.edge)
$breaker->call(
open: callable, // Required: Called when circuit is OPEN
close: callable, // Required: Called when circuit is CLOSED (or HALF_OPEN if no halfOpen callback)
halfOpen: ?callable // Optional: Called when circuit is HALF_OPEN
);$state = $breaker->getState(); // Utopia\CircuitBreaker\CircuitState enum
$breaker->isOpen();
$breaker->isClosed();
$breaker->isHalfOpen();
$breaker->getFailureCount();
$breaker->getSuccessCount();- PHP 8.2 or later
- Optional:
utopia-php/telemetry,ext-opentelemetry, andext-protobuffor OpenTelemetry metrics and the local telemetry demo - Optional:
ext-redisforUtopia\CircuitBreaker\Adapter\Redis - Optional:
ext-swooleforUtopia\CircuitBreaker\Adapter\SwooleTable
Unit tests avoid Redis and Swoole runtime dependencies:
composer testE2E tests run Redis and a PHP runtime with the Redis/Swoole extensions through Docker:
composer test:e2e:dockerStart Redis, an instrumented PHP demo server, OpenTelemetry Collector, Prometheus, and Grafana:
composer telemetry:up- Demo UI: http://localhost:8080
- Grafana: http://localhost:3030/d/circuit-breaker/circuit-breaker-telemetry
- Prometheus: http://localhost:9090
Preview from a five-minute checkout-api scenario:
Populate the dashboard with the same scenario:
composer telemetry:scenarioStop the stack and remove local volumes:
composer telemetry:downThe MIT License (MIT) http://www.opensource.org/licenses/mit-license.php
