Resilience review
Skill tunahanaliozturk/secure-dotnet-skills/skills/resilience-review
Aegis — 12 judgment-style agent skills for secure, production-grade .NET on Azure (security, design, performance, concurrency, observability). Works with Claude Code, Codex, Cursor, Gemini.
npx -y skills add tunahanaliozturk/secure-dotnet-skills --skill resilience-reviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when reviewing how a .NET service handles transient faults and downstream failures — timeouts, retries, circuit breakers, bulkheads, and fallback — for calls to databases, queues, and HTTP dependencies.
SKILL.md
11.4 KB, as published. Nobody here has run it
Resilience Review
Directs the agent to audit every outbound dependency of a .NET service against concrete resilience patterns — timeouts, retry policies, circuit breakers, bulkheads, fallback, and cancellation propagation — and produce findings linked to real APIs and configuration options.
When to use
- A service has been reported as hanging or slow to fail when a downstream HTTP API, database, or queue is unavailable or degraded.
- A PR introduces or modifies calls to external HTTP endpoints, EF Core
DbContextoperations, or Azure Service Bus / Storage clients. - A service lacks
Microsoft.Extensions.Http.Resilienceor Polly v8 policies and needs them added before production hardening. - A post-incident review identified cascading failures caused by a missing circuit breaker or unbounded retry storm.
- A reliability or SLO review requires verifying that every outbound call has a bounded timeout and backoff.
Process
- Map the outbound dependencies. Enumerate every outbound call: HTTP endpoints (typed clients,
HttpClient), EF Core / SQL, Azure Service Bus / Storage / Key Vault, Redis /IDistributedCache, and any gRPC or message-queue producers. Record each dependency's latency profile and failure mode (transient vs permanent). - Confirm per-attempt and total timeouts. For each dependency, verify there is both a per-attempt timeout (bounding a single request) and an overall deadline / total timeout (bounding all retries combined). A call with only a per-attempt timeout can still block indefinitely if retries are unbounded.
- Review the retry policy. Check that retries target only idempotent operations and transient faults (
HttpRequestException, HTTP 5xx, 408 Request Timeout), honor429 Too Many Requestswith aRetry-Afterdelay, use exponential backoff with jitter to prevent thundering herd, and cap the attempt count. - Review circuit breaker and bulkhead isolation. Confirm a circuit breaker is configured on known-flaky dependencies to stop retry storms when the target is down. Confirm a bulkhead (concurrency limiter) isolates each slow dependency so it cannot exhaust the shared thread pool or connection pool.
- Review fallback and graceful degradation. For each dependency, confirm there is a defined fallback: a cached response, a default/empty result, a degraded mode, or a clear propagation of the
503upstream — not a swallowed exception that silently returns stale or corrupt data. - Check cancellation propagation. Verify
CancellationTokenis accepted and forwarded through every layer — controller action → service →HttpClientcall / EF Core query / queue operation. Deadlines set on an outerHttpContext.RequestAbortedorCancellationTokenSource.CreateLinkedTokenSourcemust flow to every I/O call. - Output findings. For each dependency with a gap, provide: the specific missing policy, the concrete API fix (
AddStandardResilienceHandler(),ResiliencePipelineBuilder,EnableRetryOnFailure), and the idempotency precondition that must be met before enabling retries.
.NET / Azure checks
Microsoft.Extensions.Http.Resilience—AddStandardResilienceHandler(). Register on typed clients withbuilder.Services.AddHttpClient<TClient>().AddStandardResilienceHandler(). The standard handler composes — in order — a total request timeout, a retry with exponential backoff + jitter, a circuit breaker, an attempt timeout, and a hedging option. Override defaults viaHttpStandardResilienceOptions:TotalRequestTimeout.Timeout,Retry.MaxRetryAttempts,Retry.BackoffType = DelayBackoffType.Exponential,CircuitBreaker.SamplingDuration,AttemptTimeout.Timeout. For custom pipelines useAddResilienceHandler("name", builder => { ... })with aResiliencePipelineBuilder<HttpResponseMessage>.- Polly v8
ResiliencePipelineBuilder. Polly v8 replacesPolicy.Handle<T>()(v7) withnew ResiliencePipelineBuilder().AddRetry(...).AddCircuitBreaker(...).AddTimeout(...).Build(). Register pipelines withbuilder.Services.AddResiliencePipeline("key", builder => { ... })and resolve viaResiliencePipelineProvider<string>. Strategies are composed in execution order: outermost strategy executes first. - Per-attempt timeout AND total timeout. A per-attempt timeout (
TimeoutStrategyOptions { Timeout = TimeSpan.FromSeconds(2) }) bounds a single attempt. A total timeout / deadline (TotalRequestTimeoutinHttpStandardResilienceOptions, or an outerAddTimeoutwrapping the retry strategy) bounds the entire retry sequence. Without a total timeout, three retries of a 2-second attempt timeout can still consume 6+ seconds; with jitter and backoff, indefinitely longer. - Retries only for idempotent operations and transient faults. Configure
ShouldHandleto matchHttpRequestException, HTTP 5xx, and 408. Honor429 Too Many Requests: inspectRetry-AfterviaRetryStrategyOptions.OnRetryand delay accordingly — useargs.Response?.Headers.RetryAfterto parse the value. Use exponential backoff with jitter:DelayBackoffType.ExponentialwithUseJitter = true. Cap attempts:MaxRetryAttempts = 3(or 4 total including the first attempt). Do not retry non-idempotent POSTs (payment creation, order submission) without an idempotency key that makes re-submission safe. - Circuit breaker. Configure
AddCircuitBreakerwithFailureRatio,SamplingDuration,MinimumThroughput, andBreakDuration. A half-open probe is automatic. Without a circuit breaker, a downed dependency receives full retry traffic from every in-flight request simultaneously, amplifying load on recovery and blocking threads/connections for the break duration. - Bulkhead / concurrency limiter. Add
AddConcurrencyLimiter(maxConcurrentCalls, queueDepth)to isolate a slow dependency. Without it, a dependency that starts taking 30 s per call will saturate the thread pool as requests queue up awaiting completion. Pair withIHttpClientFactory'sPooledConnectionLifetimeto prevent stale DNS entries. - Fallback strategy. Use
AddFallback(new FallbackStrategyOptions<HttpResponseMessage> { FallbackAction = ... })to return a cached or default response when all retries and the circuit breaker have been exhausted. Cache the last-known-good response inIMemoryCache/IDistributedCacheand serve it on fallback. For write paths, enqueue to a durable outbox or return a503with aRetry-Afterheader rather than silently dropping the operation. CancellationTokenend-to-end. Every async method in the call chain must accept and forwardCancellationToken. For HTTP calls, passcttoGetAsync/SendAsync. For EF Core, passcttoToListAsync,FirstOrDefaultAsync,SaveChangesAsync. For Azure SDK clients (BlobClient,ServiceBusClient), passcancellationToken. UseCancellationTokenSource.CreateLinkedTokenSource(requestAbortedToken, timeoutToken)to combine an HTTP request abort with a hard deadline.IHttpClientFactorytyped clients — notnew HttpClient(). Register withbuilder.Services.AddHttpClient<TPaymentClient, PaymentClient>(client => { client.BaseAddress = ...; }). The factory poolsSocketsHttpHandlerinstances, rotating them onHandlerLifetime(default 2 min) to respect DNS TTLs.new HttpClient()per call creates a new handler with no connection reuse; sockets accumulate inTIME_WAIT.- EF Core
EnableRetryOnFailure. Configure inDbContextOptionsBuilder:options.UseSqlServer(conn, sql => sql.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null)). The built-in strategy retries on SQL transient errors (connection failures, timeouts, deadlocks). For Azure SQL / SQL MI, the default error list covers transient connectivity faults. Do not callEnableRetryOnFailureand then also wrap EF calls in a Polly retry — double-retry can cause excessive attempts. - Idempotency as a retry precondition. Before enabling retry on any operation, confirm the operation is idempotent or made idempotent with an idempotency key. GET, HEAD, PUT, and DELETE are semantically idempotent. POST is not — wrap payment-creation, order-submission, or any side-effectful POST in an
Idempotency-Keythat the server deduplicates before enabling retry on the client.
Red flags
| Signal | Why it matters |
|---|---|
HttpClient call with no Timeout and no CancellationToken | The call can block indefinitely if the server stops responding; threads accumulate and the thread pool starves. Always set client.Timeout or pass a CancellationToken with a deadline. |
| Retry applied to a non-idempotent POST without an idempotency key | Retrying payment creation or order submission can cause duplicate charges or duplicate records. Confirm idempotency first. |
while (retries-- > 0) hand-rolled retry loop with Task.Delay | No jitter, no backoff calibration, no circuit-breaker integration, no CancellationToken support, and no standard observability hooks. Replace with Polly v8 ResiliencePipelineBuilder or AddStandardResilienceHandler(). |
Infinite or unbounded MaxRetryAttempts | A loop retrying indefinitely against a downed dependency holds connections and threads for the service's entire uptime. Cap at 3–5 attempts with a total timeout. |
| Retries with no backoff (fixed or zero delay) | Synchronized retry waves from many concurrent callers hit the recovering dependency simultaneously — thundering herd. Use DelayBackoffType.Exponential with UseJitter = true. |
| No circuit breaker on a known-flaky dependency | When the target is down, every in-flight request retries to exhaustion before failing; the circuit breaker stops this within one SamplingDuration window and allows the dependency to recover. |
catch (Exception) { return null; } swallowing all errors | Turns dependency failures into silent data corruption — callers receive a null or default response with no indication that the call failed. Propagate or convert to a structured fallback with logging. |
new HttpClient() per request or per method call | No handler pooling; TCP sockets linger in TIME_WAIT, exhausting ephemeral ports under moderate traffic. Register via IHttpClientFactory. |
Missing EnableRetryOnFailure on EF Core with Azure SQL | Transient SQL connectivity errors (error 40613, 40197, 49918) are common on Azure SQL; without retry-on-failure, a transient fault surfaces as an unhandled exception. |
Polly v7 Policy.Handle<T>().WaitAndRetry(...) in a new .NET 8+ project | Polly v7 policies are not pipeline-composable with IHttpClientFactory's resilience extension; v8 ResiliencePipelineBuilder / Microsoft.Extensions.Http.Resilience is the current standard and integrates with IServiceCollection. |
Example
See examples/resilience-review/.
Related skills
- dotnet-performance-review — overlapping concern:
IHttpClientFactory, connection pooling, and async/IO patterns affect both performance and resilience. - async-concurrency-review —
CancellationTokenpropagation,Task.WhenAllfan-out, and thread-pool health are reviewed in depth there.