PollyElasticsearch

Polly v8 resilience for Elastic.Clients.Elasticsearch 8+ — add retry, timeout, and circuit-breaker to any Elasticsearch operation in two lines.
var client = new ElasticsearchClient(settings);
var resilient = client.WithPolly(pipeline => pipeline
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = ElasticTransientErrors.IsTransient,
})
.AddTimeout(TimeSpan.FromSeconds(10)));
var result = await resilient.ExecuteAsync((c, ct) => c.SearchAsync<Product>(s => s
.Index("products")
.Query(q => q.Match(m => m.Field(f => f.Name).Query("laptop"))), ct));
Why PollyElasticsearch?
Elastic.Clients.Elasticsearch does not natively integrate with Polly. This library bridges the gap:
| Problem |
Solution |
TransportException on connection failure |
Caught by ElasticTransientErrors.IsTransient |
| HTTP 429 rate-limit from Elasticsearch / proxy |
Auto-thrown as ElasticTransientException and retried |
| HTTP 503 cluster maintenance / rolling restart |
Auto-thrown as ElasticTransientException and retried |
| HTTP 504 gateway timeout behind a load balancer |
Auto-thrown as ElasticTransientException and retried |
| Slow queries blocking thread pool |
Wrap with AddTimeout |
| Cascading failures during outage |
Wrap with AddCircuitBreaker |
Installation
dotnet add package PollyElasticsearch
dotnet add package Polly.Core
Quick-start
1. Manual wiring
using Polly;
using Polly.Retry;
var client = new ElasticsearchClient(
new ElasticsearchClientSettings(new Uri("https://my-cluster:9200")));
var resilient = client.WithPolly(p => p
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(2),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = ElasticTransientErrors.IsTransient,
}));
// Every call is now wrapped in the Polly pipeline.
var response = await resilient.ExecuteAsync(
(c, ct) => c.GetAsync<Product>("products", "doc-id", ct));
2. Dependency injection
// Program.cs / Startup.cs
builder.Services.AddSingleton(new ElasticsearchClient(
new ElasticsearchClientSettings(new Uri("https://my-cluster:9200"))));
builder.Services.AddPollyElasticsearch(pipeline => pipeline
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = ElasticTransientErrors.IsTransient,
})
.AddTimeout(TimeSpan.FromSeconds(10)));
// Inject ResilientElasticsearchClient into your services
public class ProductService(ResilientElasticsearchClient client)
{
public Task<SearchResponse<Product>> SearchAsync(string q, CancellationToken ct) =>
client.ExecuteAsync((c, ct2) => c.SearchAsync<Product>(s => s
.Index("products")
.Query(q2 => q2.Match(m => m.Field(f => f.Name).Query(q))), ct2), ct);
}
3. With a URI shortcut
builder.Services.AddPollyElasticsearch(
new Uri("https://my-cluster:9200"),
pipeline => pipeline
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 5,
ShouldHandle = ElasticTransientErrors.IsTransient,
}));
Transient error reference
// Use in any Polly strategy:
ShouldHandle = ElasticTransientErrors.IsTransient
| Condition |
Why it's transient |
ElasticTransientException (HTTP 429) |
Rate limited — back off and retry |
ElasticTransientException (HTTP 503) |
Cluster down / maintenance — retry later |
ElasticTransientException (HTTP 504) |
Proxy/load-balancer timeout — retry |
TransportException |
Network failure or connection refused |
Note: ElasticTransientException is thrown automatically by ResilientElasticsearchClient when the response HTTP status code is in ElasticTransientErrors.StatusCodes (429, 503, 504). You do not need to throw it yourself.
Checking the status code
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder()
.Handle<ElasticTransientException>(ex => ex.StatusCode == 429),
MaxRetryAttempts = 5,
Delay = TimeSpan.FromSeconds(10), // respect 429 back-off window
})
Advanced pipelines
Full production pipeline
client.WithPolly(p => p
.AddTimeout(TimeSpan.FromSeconds(30)) // total call timeout
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 4,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = ElasticTransientErrors.IsTransient,
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(15),
ShouldHandle = ElasticTransientErrors.IsTransient,
}));
Observability via Polly events
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = ElasticTransientErrors.IsTransient,
OnRetry = args =>
{
logger.LogWarning("Elasticsearch retry {Attempt} after {Delay}ms — {Exception}",
args.AttemptNumber, args.RetryDelay.TotalMilliseconds, args.Outcome.Exception?.Message);
return ValueTask.CompletedTask;
},
})
API reference
ResilientElasticsearchClient
| Member |
Description |
Inner |
The underlying ElasticsearchClient |
ExecuteAsync<TResponse>(operation, ct) |
Executes operation through the pipeline; throws ElasticTransientException for 429/503/504 |
ElasticTransientErrors
| Member |
Description |
IsTransient |
PredicateBuilder for ElasticTransientException + TransportException |
StatusCodes |
IReadOnlySet<int> — {429, 503, 504} |
ElasticTransientException
| Member |
Description |
StatusCode |
The HTTP status code that triggered the exception |
Message |
Human-readable description including the status code |
Extension methods
| Method |
Description |
client.WithPolly(pipeline) |
Wraps an ElasticsearchClient with a pre-built ResiliencePipeline |
client.WithPolly(configure) |
Builds a pipeline inline and wraps the client |
DI extensions
| Method |
Description |
services.AddPollyElasticsearch(configure) |
Registers ResiliencePipeline + ResilientElasticsearchClient (requires ElasticsearchClient already in DI) |
services.AddPollyElasticsearch(uri, configure) |
Registers ElasticsearchClient for uri, then pipeline + resilient client |
Target frameworks
| Framework |
Supported |
| .NET 6 |
✅ |
| .NET 8 |
✅ |
| .NET 9 |
✅ |
License
MIT © Justin Bannister