Skip to content

Resilience

pkg/resilience/policy builds named failsafe-go policy chains and provides a *policy.Registry via DI. pkg/resilience/fiber and pkg/resilience/grpc adapt those named policies into fiber v3 middleware and gRPC interceptors.

Each named policy composes primitives in a fixed order, outermost to innermost: hedge, retry, circuit breaker, rate limit, adaptive limiter, bulkhead, timeout. Policies are static after Init — config hot-reload is deliberately unsupported because circuit breakers hold runtime state a reload would silently reset.

Add the policy module to your runtime and define policies in config.

lakta.NewRuntime(
config.NewModule(
config.WithConfigDirs(".", "./config"),
config.WithArgs(os.Args[1:]),
),
tint.NewModule(),
slog.NewModule(),
otel.NewModule(),
policy.NewModule(),
)
modules:
resilience:
policy:
default:
policies:
api:
timeout: 2s
rate_limit:
max: 100
period: 1s
upstream:
retry:
max_attempts: 3
delay: 50ms
max_delay: 1s
circuit_breaker:
failure_threshold: 5
success_threshold: 2
delay: 10s

Prefer snake_case policy names — hyphens cannot be overridden via environment variables.

Resolve the *policy.Registry from DI and run work through a named policy. Run handles error-only functions; the free generic policy.Get returns a typed result.

reg, err := lakta.Invoke[*policy.Registry](ctx)
if err != nil {
return err
}
err = reg.Run(ctx, "upstream", func(ctx context.Context) error {
return callDownstream(ctx)
})
user, err := policy.Get(ctx, reg, "upstream", func(ctx context.Context) (*User, error) {
return fetchUser(ctx)
})

The context passed to the callback carries policy-driven cancellation, such as timeouts. Policy and handler errors pass through unwrapped, so callers can match sentinels like circuitbreaker.ErrOpen.

resfiber.New runs the handler chain through a named policy and maps rejections to HTTP status codes: rate limit to 429, bulkhead/adaptive-limiter overload to 503 with Retry-After, open circuit breaker to 503, timeout to 504. Import it aliased to avoid clashing with gofiber/fiber.

import resfiber "github.com/Vilsol/lakta/pkg/resilience/fiber"
fiberserver.NewModule(
fiberserver.WithRouterCtx(func(ctx context.Context, app *fiber.App) {
reg, _ := lakta.Invoke[*policy.Registry](ctx)
mw, _ := resfiber.New(reg, "api")
app.Use(mw)
}),
)

Retry policies re-run the handler chain, which is rarely safe server-side; prefer rate_limit, circuit_breaker, and timeout policies in middleware.

resgrpc adapts a named policy into unary interceptors and a tap handler, translating overload sentinels to gRPC status codes (bulkhead.ErrFull/adaptivelimiter.ErrExceeded to Unavailable, ratelimiter.ErrExceeded to ResourceExhausted). Import it aliased to avoid clashing with google.golang.org/grpc.

import resgrpc "github.com/Vilsol/lakta/pkg/resilience/grpc"
// Client-side: retry/hedge outbound calls.
interceptor, err := resgrpc.NewUnaryClientInterceptor(reg, "upstream")
// Server-side: guard handlers, or guard before message allocation.
serverInterceptor, err := resgrpc.NewUnaryServerInterceptor(reg, "api")
inHandle, err := resgrpc.NewServerInHandle(reg, "api")

Prefer NewServerInHandle over the server interceptor for load limiting — it rejects overloaded requests before gRPC allocates the request message.

WithPolicy registers a policy chain in code, ordered outermost first. Use it for primitives not expressible in config (such as a fallback), or hand-built prioritized limiters. A config entry with the same name replaces it wholesale.

policy.NewModule(
policy.WithPolicy("api",
ratelimiter.NewBursty[any](100, time.Second),
),
)

Config path: modules.resilience.policy.<name>

policiespolicies defines the named policies this module manages. Prefer
policies.timeouttime.Duration

timeout bounds each execution attempt. Zero disables it

env
policies.retryretry retries failed executions
policies.retry.max_attemptsint

maxAttempts is the total number of attempts, including the first

env
policies.retry.delaytime.Duration

delay is the base delay between attempts. Zero means immediate retry

env
policies.retry.max_delaytime.Duration

maxDelay caps exponential backoff; requires Delay. Zero keeps the

env
policies.retry.jittertime.Duration

jitter randomizes each delay by up to this duration

env
policies.circuit_breakercircuitBreaker rejects executions while failures exceed a threshold
policies.circuit_breaker.failure_thresholdint

failureThreshold is the number of failures that opens the breaker

env
policies.circuit_breaker.success_thresholdint

successThreshold is the number of half-open successes that close it

env
policies.circuit_breaker.delaytime.Duration

delay is how long the breaker stays open before half-opening

env
policies.rate_limitrateLimit bounds the execution rate
policies.rate_limit.maxint

max is the number of executions allowed per period

env
policies.rate_limit.periodtime.Duration

period is the window Max applies to. Defaults to one second

env
policies.rate_limit.burstybool

bursty allows Max executions at once instead of smoothing them

env
policies.rate_limit.max_waittime.Duration

maxWait is how long an execution may wait for a permit before being

env
policies.hedgehedge starts redundant attempts after a delay to trim tail latency
policies.hedge.delaytime.Duration

delay before starting a hedged attempt. Required (> 0)

env
policies.hedge.max_hedgesint

maxHedges is the max number of hedged attempts. 0 = library default (1)

env
policies.adaptive_limiteradaptiveLimiter sheds load by self-tuning a concurrency limit
policies.adaptive_limiter.minuint

min is the minimum concurrency limit

env
policies.adaptive_limiter.maxuint

max is the maximum concurrency limit; must be >= 1

env
policies.adaptive_limiter.initialuint

initial is the starting limit; Min <= Initial <= Max

env
policies.adaptive_limiter.max_waittime.Duration

maxWait is how long to wait for a permit before rejecting. Zero rejects

env
policies.adaptive_limiter.queueingqueueing enables absorbing short spikes before rejecting. Nil disables it
policies.adaptive_limiter.queueing.initial_factorfloat64

initialFactor is the queue depth (times the limit) before rejections

env
policies.adaptive_limiter.queueing.max_factorfloat64

maxFactor is the queue depth (times the limit) at which all excess is

env
policies.bulkheadbulkhead caps concurrency with a hard ceiling nearest the call
policies.bulkhead.max_concurrentuint

maxConcurrent is the hard concurrency ceiling; must be >= 1

env
policies.bulkhead.max_waittime.Duration

maxWait is how long to wait for a slot before rejecting. Zero rejects

env

These options can only be set in Go code via With*() functions, not via config files or environment variables.

OptionTypeDescription
WithPolicy(...)map[string][]failsafe.Policy[interface {}]registers a policy chain in code (code-only), ordered outermost