Webhook Circuit Breakers: Protecting Downstream Services From Cascading Failures
Webhook Circuit Breakers: Protecting Downstream Services From Cascading Failures In event-driven architectures, webhooks are the primary mechanism for real-time inter-system...

Webhook Circuit Breakers: Protecting Downstream Services From Cascading Failures
In event-driven architectures, webhooks are the primary mechanism for real-time inter-system communication. Whether you're accepting payment confirmation events from Stripe, pull request triggers from GitHub, or order state updates from Shopify, incoming webhooks deliver mission-critical payloads directly to your HTTP ingestion endpoints.
However, webhooks carry a hidden architectural vulnerability: they are unthrottled, external push requests.
Unlike client-facing REST APIs, where you control rate limits and can return 429 Too Many Requests to a single misbehaving client, incoming webhooks represent bursts of third-party traffic driven by external state changes you don't control. If your primary database or downstream microservices experience transient degradation — lock contention, CPU throttling, connection pool exhaustion — continuing to synchronously process incoming webhooks can turn a localized slowdown into a platform-wide outage.
To solve this, event-driven platforms implement the webhook circuit breaker pattern. By combining real-time telemetry with a fallback to queue-only ingestion, this pattern achieves graceful degradation: it protects backend infrastructure while aiming for zero event loss. This guide covers how to detect backend stress, implement backpressure for webhooks, and safely hold incoming payloads at the ingestion layer until downstream metrics recover — along with what actually happens when this goes wrong in production, based on recent postmortems.
1. The Anatomy of a Webhook Death Spiral
To understand why a dedicated webhook circuit breaker is necessary, consider a standard architecture: an HTTP gateway receives an incoming webhook, validates the HMAC signature, parses the payload, queries the primary database, updates a record, and returns 200 OK.
[ Incoming Webhook ] ──> [ API Gateway ] ──> [ App Worker ] ──> [ Primary DB ]
When everything is healthy, this flow takes tens of milliseconds. But when the primary database experiences a spike in lock contention — a maintenance job, a schema migration, a sudden traffic surge — here's what unfolds:
- Database latency spikes. Queries slow from single-digit milliseconds to seconds.
- Worker thread saturation. Incoming requests queue up inside the application server (Node.js event loop, Go goroutines, Puma threads) waiting on database connections.
- Load amplification. If the sender retries on failure, retries add to the load on an already-struggling system — the opposite of what you want during an incident.
- Cascading failure. Load balancer health checks start failing on saturated nodes, which get pulled from the target group. The remaining healthy nodes absorb 100% of traffic and fall over too.
+-------------------------------------------------------------------------+
| THE WEBHOOK DEATH SPIRAL |
| |
| 1. DB Lock Contention ──> Latency Spikes (5ms -> 3000ms) |
| 2. App Thread Pool Exhaustion (Waiting on DB sockets) |
| 3. Retries (where applicable) Amplify Load |
| 4. Health Checks Fail ──> Nodes Dropped from Load Balancer |
| 5. Total Cascading System Outage |
+-------------------------------------------------------------------------+
A necessary correction here: not every provider behaves the same way under timeout, and the differences matter a lot for how you design defenses. It's a common assumption that "the provider will just retry," but that's not universally true — see Section 9 for the real numbers, because the pattern you need to build depends on it.
The core issue is tight coupling between ingestion and execution. If your ingestion path directly touches a vulnerable downstream dependency, your entire ingestion pipeline is only as reliable as your weakest database table.
2. Standard Circuit Breakers vs. Webhook Circuit Breakers
The classic circuit breaker pattern — popularized by Martin Fowler's writing and Netflix's Hystrix library — acts as an automatic switch between services:
- Closed: Requests pass through normally.
- Open: When error rates cross a threshold, requests fail fast (e.g.,
503 Service Unavailable). - Half-Open: A small sample of probe traffic tests whether the downstream service has recovered.
A note on Hystrix specifically: Netflix put Hystrix into maintenance mode in November 2018 and has recommended alternatives like Resilience4j for new projects ever since; Spring Cloud dropped its Hystrix integration in favor of Resilience4j not long after. The three-state closed/open/half-open model Hystrix popularized is still exactly what's implemented in this pattern, but if you're picking a library today for standard RPC circuit breaking, reach for Resilience4j (JVM), or your language's equivalent, rather than Hystrix itself.
Why Standard Circuit Breakers Fail for Webhooks
If a standard circuit breaker trips on an incoming webhook endpoint and immediately returns 503 or 500, it can trigger two problems:
- Retry storms from providers that do retry. Providers like Stripe and Shopify treat non-2xx responses as transient failures and retry with backoff — so a tripped breaker that returns errors actually increases the volume of retry requests hitting your load balancer during the exact window you can least afford it.
- Silent, permanent data loss from providers that don't retry. GitHub, notably, does not automatically retry failed webhook deliveries at all. If your endpoint is down or times out even once, that event is simply gone unless you manually redeliver it from the dashboard or API within GitHub's retention window. A standard fail-fast breaker offers zero protection here — it doesn't reduce load, and it drops data.
The Solution: Store-and-Forward ("Queue-Only") Mode
A webhook circuit breaker doesn't reject requests with errors when it trips. Instead, it reroutes the processing path into Queue-Only (Store-and-Forward) Mode.
┌────────────────────────────────────────┐
│ Ingestion Layer (Stateless) │
└───────────────────┬────────────────────┘
│
Is Downstream Healthy?
/ \
YES / \ NO (Breaker Tripped!)
/ \
v v
┌──────────────────┐ ┌───────────────────────┐
│ Synchronous/Fast │ │ QUEUE-ONLY MODE │
│ Downstream Path │ │ (Buffer Payload to │
└──────────────────┘ │ Kafka / SQS / Redis) │
└───────────┬────────────┘
│
v
Acknowledge HTTP 202
(Protect Downstream!)
When downstream metrics indicate stress, the webhook ingestion server stops writing to the database or invoking downstream RPCs. Instead:
- The raw payload, headers, and metadata are written directly to a durable, high-throughput message store (Kafka, AWS SQS, Redis Streams).
- The ingestion server immediately responds with
202 Accepted. - Downstream workers throttle their queue consumption, giving the database room to recover.
3. High-Level Architecture Overview
Core layers of an event-driven system that implements ingestion-layer circuit breaking:
- Stateless Edge Ingestion Proxy: A low-footprint service (Go, Rust, Node.js) that performs only HMAC signature validation and raw-body persistence. Zero database connections.
- Circuit State Manager: A shared state store (typically Redis, via pub/sub) broadcasting the circuit's state (
CLOSED,OPEN_QUEUE_ONLY,HALF_OPEN) to every ingestion node. - Durable Buffer Queue: An event stream (Kafka topic, SQS queue, Redis Stream) built to absorb write bursts without backpressure.
- Asynchronous Worker Pool: Background consumers that pull from the queue and perform the actual database writes and business logic.
4. Detecting Backend Stress: Metric Signals That Trip the Breaker
A webhook circuit breaker is only as good as its telemetry. Trip too late and the database crashes anyway; trip too early and you add needless queuing latency for no benefit.
A reasonable starting point, monitored in real time:
| Metric Signal | Healthy Baseline | Warning / Caution | Breaker Trip Threshold (Open) |
|---|---|---|---|
| Database connection pool utilization | < 50% active | 70–84% active | ≥ 85% active for > 3 seconds |
| Downstream write latency (p99) | < 50 ms | 100–499 ms | ≥ 500 ms over a rolling 10s window |
| App node CPU throttling ratio | < 2% | 5–14% | ≥ 15% of container CPU quota throttled |
| Database lock wait timeout rate | 0 errors/min | 1–5 errors/min | > 5 lock wait timeouts/min |
These are illustrative starting points, not universal constants — the right thresholds depend entirely on your traffic shape and hardware, and that's exactly the weakness of fixed thresholds as a long-term strategy.
A More Modern Alternative: Adaptive Concurrency Limits
Fixed percentage thresholds require someone to guess the right numbers and re-tune them as the system scales. Netflix's own engineering team explicitly moved away from this in the years after Hystrix, publishing adaptive concurrency limits: instead of hand-tuned thresholds, the system continuously estimates its own safe concurrency ceiling from real-time latency, borrowing ideas from TCP congestion control (similar to how TCP Vegas and CUBIC estimate a safe sending rate from round-trip time).
The core idea, using Netflix's open-source concurrency-limits library as a reference implementation:
- Track a long-term "best observed" round-trip time (RTT) alongside a short-term moving average.
- Compute a gradient:
gradient ≈ best_RTT / current_RTT. - When latency is stable, the gradient stays near 1 and the concurrency limit holds steady.
- When latency climbs (queueing is building up downstream), the gradient drops below 1 and the limit shrinks automatically.
- When conditions improve, the limit grows back, with a small headroom term (often
sqrt(current_limit)) so it doesn't get stuck too low.
You don't have to build this from scratch: Netflix/concurrency-limits (Java), and gradient/AIMD-style limiters in libraries like Envoy's adaptive concurrency filter, implement this out of the box. For a webhook ingestion layer, you can run this alongside the fixed-threshold table above — use the static thresholds as a circuit-breaker-level kill switch for "downstream is clearly unhealthy," and use adaptive concurrency limiting as a finer-grained, self-tuning throttle on synchronous processing before you ever get there.
Implementing a Redis-Backed Telemetry Evaluator
Here's a Go implementation of a background monitor that evaluates system signals and updates the shared circuit breaker state:
package breaker
import (
"context"
"sync/atomic"
"time"
"github.com/redis/go-redis/v9"
)
type CircuitState int32
const (
StateClosed CircuitState = iota // 0: Normal Path
StateOpen // 1: Queue-Only Mode
StateHalfOpen // 2: Draining & Probing Mode
)
type WebhookCircuitBreaker struct {
rdb *redis.Client
state int32 // Atomic storage for fast in-memory lookups
dbPoolMax int
latencyLimitMs int64
}
func NewWebhookCircuitBreaker(rdb *redis.Client, maxDBConns int, maxLatencyMs int64) *WebhookCircuitBreaker {
return &WebhookCircuitBreaker{
rdb: rdb,
state: int32(StateClosed),
dbPoolMax: maxDBConns,
latencyLimitMs: maxLatencyMs,
}
}
// GetCurrentState returns local in-memory state with zero network latency
func (cb *WebhookCircuitBreaker) GetCurrentState() CircuitState {
return CircuitState(atomic.LoadInt32(&cb.state))
}
// MonitorDownstreamHealth runs as a continuous telemetry loop
func (cb *WebhookCircuitBreaker) MonitorDownstreamHealth(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
activeConns, p99Latency := cb.fetchDownstreamMetrics(ctx)
connUtilization := float64(activeConns) / float64(cb.dbPoolMax)
if connUtilization >= 0.85 || p99Latency >= cb.latencyLimitMs {
cb.tripToQueueOnly(ctx)
} else if connUtilization < 0.50 && p99Latency < (cb.latencyLimitMs/2) {
cb.attemptRecovery(ctx)
}
}
}
}
func (cb *WebhookCircuitBreaker) tripToQueueOnly(ctx context.Context) {
if atomic.CompareAndSwapInt32(&cb.state, int32(StateClosed), int32(StateOpen)) {
cb.rdb.Set(ctx, "circuit_breaker:webhook_state", "OPEN_QUEUE_ONLY", 0)
cb.rdb.Publish(ctx, "circuit_breaker_events", "TRIPPED_OPEN")
}
}
func (cb *WebhookCircuitBreaker) attemptRecovery(ctx context.Context) {
if atomic.CompareAndSwapInt32(&cb.state, int32(StateOpen), int32(StateHalfOpen)) {
cb.rdb.Set(ctx, "circuit_breaker:webhook_state", "HALF_OPEN", 0)
cb.rdb.Publish(ctx, "circuit_breaker_events", "PROBING_RECOVERY")
}
}
func (cb *WebhookCircuitBreaker) fetchDownstreamMetrics(ctx context.Context) (int, int64) {
// Query internal connection pool metrics and latency histogram samples.
// Returns (activeDBConnections, p99LatencyMilliseconds).
return 88, 620 // Example values showing database saturation
}
5. Circuit Breaker States & Execution Control
+-------------------------+
| CLOSED |
| (Normal Ingestion) |
+------------+------------+
|
Metrics Exceed Thresholds
|
v
+-------------------------+
| OPEN_QUEUE_ONLY | <--- Store payload to stream;
| (Graceful Degrade) | return HTTP 202 Accepted.
+------------+------------+
|
Cooldown & Health Normal
|
v
+-------------------------+
| HALF_OPEN | <--- Drain queue via dynamic
| (Probing Recovery) | token bucket rate limiting.
+------------+------------+
|
Metrics Stable | Metrics Degraded
v
Return to CLOSED
State 1: Closed (Normal Path)
Standard operation. The API worker validates headers, processes synchronously (or via a fast standard queue), and acknowledges with 200 OK or 201 Created. Target end-to-end latency: under 100 ms.
State 2: Open / Queue-Only Mode (Graceful Degradation) Trigger: DB pool utilization above 85%, or p99 write latency above 500 ms. The API layer detaches from the database entirely:
- Validate the HMAC signature in memory using pre-cached secrets.
- Append raw bytes directly to the durable message broker.
- Respond immediately with
202 Accepted, and aX-Execution-Mode: Queued-Asyncheader for observability.
Benefit: providers that respect 2xx responses (which is most of them) stop retrying, and database load drops to zero because ingestion nodes stop opening transactions.
State 3: Half-Open (Controlled Recovery & Probing)
Trigger: health metrics stay below warning levels for a continuous cooldown window (e.g., 30 seconds). Workers resume consuming queued webhooks using a token-bucket rate limiter (e.g., starting at 10 events/second). If downstream latency stays stable, the rate ramps up exponentially (10 → 50 → 200 events/sec) until the backlog drains and the system returns to Closed.
6. Implementation Deep-Dive: The Ingestion Endpoint
The HTTP handler must branch cleanly based on local circuit state so the ingestion layer stays fast even when the rest of your infrastructure is struggling. TypeScript/Node.js with Express and ioredis:
import { Request, Response } from 'express';
import Redis from 'ioredis';
import crypto from 'crypto';
import { cryptoVerifyHMAC } from './security';
const redis = new Redis(process.env.REDIS_URL!);
let localCircuitState: 'CLOSED' | 'OPEN_QUEUE_ONLY' | 'HALF_OPEN' = 'CLOSED';
// Subscribe to instant Redis Pub/Sub events for state changes
const redisSub = new Redis(process.env.REDIS_URL!);
redisSub.subscribe('circuit_breaker_events');
redisSub.on('message', (channel, message) => {
if (message === 'TRIPPED_OPEN') localCircuitState = 'OPEN_QUEUE_ONLY';
if (message === 'PROBING_RECOVERY') localCircuitState = 'HALF_OPEN';
if (message === 'RESET_CLOSED') localCircuitState = 'CLOSED';
});
export async function handleIncomingWebhook(req: Request, res: Response) {
const signature = req.headers['x-hub-signature-256'] as string;
const rawBody = req.body; // Buffer containing unparsed raw bytes
// 1. ALWAYS validate signatures at the edge (CPU-only, no DB query)
const isValid = cryptoVerifyHMAC(rawBody, signature, process.env.WEBHOOK_SECRET!);
if (!isValid) {
return res.status(401).json({ error: 'Invalid HMAC signature' });
}
const payload = {
eventId: req.headers['x-request-id'] || crypto.randomUUID(),
receivedAt: Date.now(),
headers: req.headers,
body: rawBody.toString('base64'),
};
// 2. CHECK CIRCUIT STATE
if (localCircuitState === 'OPEN_QUEUE_ONLY' || localCircuitState === 'HALF_OPEN') {
await redis.xadd('stream:webhook_ingestion', '*', 'payload', JSON.stringify(payload));
res.setHeader('X-System-Degraded', 'true');
res.setHeader('X-Execution-Path', 'Queue-Only');
return res.status(202).json({
status: 'accepted',
message: 'Event buffered safely for asynchronous processing.',
eventId: payload.eventId,
});
}
// 3. NORMAL PATH (CLOSED)
try {
await processWebhookSynchronously(payload);
return res.status(200).json({ status: 'success' });
} catch (err: any) {
// If inline execution fails due to DB pool timeout, push to queue and degrade
await redis.xadd('stream:webhook_ingestion', '*', 'payload', JSON.stringify(payload));
return res.status(202).json({
status: 'accepted',
message: 'Processing deferred to queue due to transient downstream delay.',
});
}
}
7. Recovering Without Secondary Outages (Dynamic Backpressure Draining)
The most common mistake in circuit breaker implementations is the recovery thundering herd. When the database recovers and the circuit transitions to HALF_OPEN, unleashing 50 workers on a 100,000-event backlog at full speed will re-trip the breaker within seconds.
+-----------------------------------------------------------------------+
| THE RECOVERY THUNDERING HERD DANGER |
| |
| Database Recovers ──> Circuit Half-Opens ──> 50 Workers Unthrottled |
| │ |
| Database Crashes Again <── Massive Concurrency Burst <┘ |
+-----------------------------------------------------------------------+
Solution: Dynamic Consumer Token Bucket
Workers draining the buffer queue must respect a dynamic concurrency ceiling tied to current database metrics:
import time
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
def worker_queue_loop():
"""
Worker process that drains webhooks at a rate dynamically
adjusted based on database connection health.
"""
while True:
allowed_rps = int(r.get("config:worker_max_rps") or 50)
delay = 1.0 / allowed_rps
messages = r.xreadgroup(
groupname="webhook_workers",
consumername="worker_node_1",
streams={"stream:webhook_ingestion": ">"},
count=1,
block=2000
)
if messages:
for stream_name, event_list in messages:
for event_id, data in event_list:
process_event(data)
r.xack("stream:webhook_ingestion", "webhook_workers", event_id)
time.sleep(delay)
def process_event(data):
# Execute heavy DB transactions here safely
pass
Dynamic Rate Allocation
During HALF_OPEN, worker throughput should scale proportionally to available headroom:
allowed_worker_rps = max_rps × (1 − current_db_pool_usage / max_db_pool_size)
If the connection pool is 70% full, worker throughput throttles to 30% of maximum. As the pool frees up to 20% usage, throughput expands to 80% of capacity. This is a simple linear version of the same idea behind the gradient-based adaptive limiters discussed in Section 4 — you can start here and graduate to a gradient/AIMD controller once you have enough production data to tune it.
8. Handling Edge Cases & Operational Considerations
1. What if the storage buffer fills up?
If a downstream outage persists for hours, your queue buffer (Kafka disk, SQS quota) can approach capacity. At that emergency threshold (e.g., >90% queue capacity), the ingestion layer should shed load with 429 Too Many Requests or 503 Service Unavailable, and include a Retry-After header:
HTTP/1.1 429 Too Many Requests
Retry-After: 300
X-Backpressure-Reason: Ingestion-Queue-Full
Providers that implement retry logic — Stripe and Shopify among them — generally respect Retry-After and pause redelivery accordingly. This doesn't help with GitHub, since GitHub doesn't retry regardless of the status code you return (see Section 9) — which is exactly why the queue-fill scenario should be a last resort, not a normal operating state.
2. Preserving event ordering across transitions.
Once a circuit trips to OPEN_QUEUE_ONLY, all subsequent webhooks should continue through the queue until backlog depth reaches zero, even if downstream health has already normalized — otherwise a newer event processed synchronously can overtake an older one still sitting in the queue.
[ Ingestion ] ──> Queue Depth > 0? ──(YES)──> Force via Queue (Preserve Order)
│
(NO)
│
v
Bypass Queue (Direct Path)
3. Idempotency keys are mandatory. Because events can be delayed in queues or redelivered by providers during circuit transitions, background workers must be idempotent. Store an idempotency key before executing business logic:
INSERT INTO processed_webhooks (event_id, processed_at)
VALUES ($1, NOW())
ON CONFLICT (event_id) DO NOTHING;
If the insert affects zero rows, skip processing and acknowledge the queue message as handled.
9. What Webhook Providers Actually Do on Failure
This is the part that's easy to get wrong by assumption, and it changes how paranoid your architecture needs to be about each integration. As of 2026:
| Provider | Retries on failure? | Window / attempts | What happens after retries are exhausted |
|---|---|---|---|
| Stripe | Yes, exponential backoff | Up to ~3 days, roughly 16 attempts in live mode (3 attempts over a few hours in test/sandbox mode) | Event marked failed in the dashboard; endpoint gets disabled after sustained failure, with an email notice. Manually resendable for up to 15 days. |
| Shopify | Yes, exponential backoff | Up to 8 attempts over a 4-hour window (changed from the older 19-attempts/48-hour policy in a September 2024 update) | Event dropped; a persistently failing subscription can be auto-removed and needs re-registration. |
| GitHub | No automatic retry at all | N/A | The delivery is simply recorded as failed. You (or an admin) must manually redeliver from "Recent Deliveries" or the REST API, within a retention window of a few days. |
A few implications for the circuit breaker design above:
- Don't assume the provider has your back. If you integrate with GitHub, a queue-only fallback isn't a nice-to-have during a DB incident — it's the only thing standing between you and silently losing events, since there's no second chance coming from GitHub's side.
- Shopify's retry window shrank significantly. Teams that built reliability logic around the old "19 retries over 48 hours" figure are now working with a much tighter 4-hour window, so an extended incident can outlast the provider's patience faster than older designs assumed.
- Your own retry/backlog window should be measured in days, not hours, precisely because provider-side retry windows are shorter and less uniform than people tend to assume. The queue-only mode in this pattern is what buys you that extra runway.
This isn't a hypothetical concern. In August 2026, GitHub published a postmortem attributing a multi-hour, multi-service outage (including Actions and Webhooks) partly to client-side retry loops amplifying load during recovery — the same "retry storm" failure mode this pattern is designed to prevent, just occurring inside GitHub's own infrastructure rather than downstream of it. Separately, observability vendor Firetiger published a postmortem describing an approximately 8-hour ingest degradation in March 2026 that specifically affected its ability to accept GitHub webhooks alongside telemetry data, caused by a cascading deployment issue rather than webhook volume itself — a reminder that ingestion outages come from many directions, not just traffic spikes, and a queue-first design helps regardless of the root cause.
10. Standard Webhooks and Managed Infrastructure
If you're building this from scratch, it's worth knowing the ecosystem has consolidated somewhat since this pattern was first popularized. The Standard Webhooks specification — an open effort originally driven by Svix along with Twilio, Kong, Supabase, and others — has become a common reference point for webhook signing and delivery conventions, and has seen adoption from a number of API platforms. Building your outbound webhook signing against that spec (if you're the one sending webhooks, not just receiving them) means integrators get a signature and retry model they've likely already implemented.
On the receiving side covered by this article, a few managed options exist if you'd rather not run the full ingestion, buffering, and circuit-breaking stack yourself: services like Hookdeck and Svix's self-hostable server both provide queueing, retries, and observability for webhook traffic, and can be a reasonable alternative to building and operating the pattern above in-house — particularly for smaller teams without dedicated infrastructure engineering capacity. The trade-off is the usual one: less operational burden, less control over the exact thresholds and recovery behavior described in this article.
11. Architectural Takeaways Checklist
- Decouple edge ingestion from database I/O. The HTTP handler validates signatures with CPU-only operations and writes raw buffers without querying the database.
- Track downstream telemetry in real time. Monitor p99 latency, lock contention, and connection pool saturation as primary trip signals — and consider layering in adaptive concurrency limiting rather than relying solely on fixed thresholds.
- Fall back to
202 Accepted. Under detected stress, switch to queue-only mode and acknowledge with202to avoid retry storms from providers that retry. - Don't rely on the provider's retry policy as your safety net. Some providers (GitHub) don't retry at all; others (Shopify) now retry over a much shorter window than older designs assumed. Your own buffer needs to outlast all of them.
- Enforce token-bucket draining during recovery. Ramp up consumer throughput gradually in
HALF_OPENto avoid a secondary thundering-herd outage. - Preserve FIFO ordering. Stay in queued mode until backlog depth reaches zero.
- Enforce idempotency everywhere. Retries and redeliveries mean duplicate events are a certainty, not an edge case.
Sources
- Netflix/Hystrix
README.md, GitHub — maintenance-mode announcement - Netflix Tech Blog, "Performance Under Load" (adaptive concurrency limits announcement)
- Netflix/concurrency-limits, GitHub repository and DeepWiki documentation
- Stripe Docs — "Receive Stripe events in your webhook endpoint"
- Shopify Dev Changelog — "Updates to webhook retry mechanism"
- GitHub Docs — "Handling failed webhook deliveries" and "Redelivering webhooks"
- Svix — "Announcing Standard Webhooks"; "Best Webhook Infrastructure Platforms (2026)"
- GitHub's August 20, 2026 postmortem on the August 17, 2026 outage
- Firetiger, "Incident postmortem... Firetiger ingest outage on March 1, 2026"