Resile is a resilience and retry library for Go, inspired by Python's stamina. It provides a type-safe, ergonomic, and highly observable way to handle transient failures in distributed systems.
Subscribe to our YouTube channel for deep dives and tutorials.
Resile allows you to compose complex resilience strategies with a single, type-safe call. No interface{} casting, no reflection—just clean Go.
user, err := resile.Do(ctx, func(ctx context.Context) (*User, error) {
return client.GetUser(ctx, id)
},
resile.WithBulkhead(10), // Max 10 concurrent requests
resile.WithCircuitBreaker(cb), // Stop if failure rate > 50%
resile.WithRetry(3), // 3 attempts with AWS Full Jitter
resile.WithTimeout(1*time.Second), // Each attempt max 1s
resile.WithFallback(func(ctx context.Context, err error) (*User, error) {
return cache.Get(id), nil // Return stale data on failure
}),
)
go get github.com/cinar/resile
Requires Go 1.24+ (uses generics and errors.Join).
In distributed systems, transient failures are a mathematical certainty. Resile simplifies the "Correct Way" to retry:
- AWS Full Jitter: Uses the industry-standard algorithm to prevent "thundering herd" synchronization.
- Adaptive Retries: Built-in token bucket rate limiting to prevent "retry storms" across a cluster.
- Generic-First: No interface{} or reflection. Full compile-time type safety.
- Context-Aware: Strictly respects context.Context cancellation and deadlines.
- Zero-Dependency Core: The core resile, circuit, and chaos packages depend only on the Go standard library. Telemetry adapters (telemetry/) are opt-in.
- Opinionated Defaults: Sensible defaults (5 attempts, exponential backoff).
- Chaos-Ready: Built-in support for fault and latency injection to test your resilience policies.
Want to learn more about the philosophy behind Resile and advanced resilience patterns in Go? Check out these deep dives:
Also, check out our Dev.to space for more articles and discussions.
The examples/ directory contains standalone programs showing how to use Resile in various scenarios:
Do and DoErr calls.Retry-After headers and using slog.RetryState.CancelAllRetries.gen_statem.database/sql.| Function | Signature | Description |
|---|---|---|
Do[T] |
(ctx, action, ...Option) (T, error) |
Retry an action that returns a value. |
DoErr |
(ctx, action, ...Option) error |
Retry an action that returns only an error. |
DoState[T] |
(ctx, action, ...Option) (T, error) |
Like Do, with access to RetryState. |
DoErrState |
(ctx, action, ...Option) error |
Like DoErr, with access to RetryState. |
DoHedged[T] |
(ctx, action, ...Option) (T, error) |
Speculative retries for tail latency. |
DoErrHedged |
(ctx, action, ...Option) error |
Error-only hedging variant. |
DoStateHedged[T] |
(ctx, action, ...Option) (T, error) |
Stateful hedging (e.g., endpoint rotation). |
DoErrStateHedged |
(ctx, action, ...Option) error |
Error-only stateful hedging. |
New |
(...Option) Retryer |
Create a reusable Retryer instance. |
NewPolicy |
(...Option) *Policy |
Create a composed resilience policy. |
NewStateMachine[S,D,E] |
(S, D, TransitionFunc, ...Option) *StateMachine |
Create a resilient state machine. |
FatalError |
(error) error |
Mark an error as non-retryable. |
WithTestingBypass |
(context.Context) context.Context |
Skip all backoff delays in tests. |
InjectDeadlineHeader |
(ctx, Header, string) |
Propagate deadline to outgoing requests. |
The Problem: A database connection or network request might fail intermittently due to transient blips.
The Recipe: Retry a simple operation that only returns an error. If all retries fail, Resile returns an aggregated error containing the failures from every attempt.
err := resile.DoErr(ctx, func(ctx context.Context) error {
return db.PingContext(ctx)
})
The Problem: Fetching data from a microservice needs to be resilient and type-safe without boilerplate casting.
The Recipe: Fetch data with full type safety. The return type is inferred from your closure.
// val is automatically inferred as *User
user, err := resile.Do(ctx, func(ctx context.Context) (*User, error) {
return apiClient.GetUser(ctx, userID)
}, resile.WithMaxAttempts(3))
The Problem: Long-tail latency (the 99th percentile) slows down your entire system even when most requests are fast.
The Recipe:
Speculative retries reduce tail latency by starting a second request if the first one doesn't finish within a configured HedgingDelay. The first successful result is used, and the other is cancelled.
// For value-yielding operations
data, err := resile.DoHedged(ctx, action,
resile.WithMaxAttempts(3),
resile.WithHedgingDelay(100 * time.Millisecond),
)
// For error-only operations
err := resile.DoErrHedged(ctx, action,
resile.WithMaxAttempts(2),
resile.WithHedgingDelay(50 * time.Millisecond),
)
Stateful hedging variants are also available for endpoint rotation with speculative retries:
data, err := resile.DoStateHedged(ctx, func(ctx context.Context, state resile.RetryState) (string, error) {
url := endpoints[state.Attempt % uint(len(endpoints))]
return client.Get(ctx, url)
}, resile.WithHedgingDelay(100 * time.Millisecond))
Read more: Beating Tail Latency: A Guide to Request Hedging in Go Microservices
The Problem: Retrying against the same failing endpoint is futile; you need to cycle through a list of healthy hosts.
The Recipe:
Use DoState (or DoErrState) to access the RetryState, allowing you to rotate endpoints or fallback logic based on the failure history.
endpoints := []string{"api-v1.example.com", "api-v2.example.com"}
data, err := resile.DoState(ctx, func(ctx context.Context, state resile.RetryState) (string, error) {
// Rotate endpoint based on attempt number
url := endpoints[state.Attempt % uint(len(endpoints))]
return client.Get(ctx, url)
})
Read more: Self-Healing State Machines: Resilient State Transitions in Go
The Problem: Downstream services may
$ claude mcp add resile \
-- python -m otcore.mcp_server <graph>