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: 10sPrefer snake_case policy names — hyphens cannot be overridden via environment variables.
Running code through a policy
Section titled “Running code through a policy”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.
Fiber middleware
Section titled “Fiber middleware”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.
gRPC interceptors
Section titled “gRPC interceptors”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.
Code-only policies
Section titled “Code-only policies”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), ),)Configuration Reference
Section titled “Configuration Reference”Config path: modules.resilience.policy.<name>
policiespolicies defines the named policies this module manages. Preferpolicies.timeouttimeout bounds each execution attempt. Zero disables it
policies.retryretry retries failed executionspolicies.retry.max_attemptsmaxAttempts is the total number of attempts, including the first
policies.retry.delaydelay is the base delay between attempts. Zero means immediate retry
policies.retry.max_delaymaxDelay caps exponential backoff; requires Delay. Zero keeps the
policies.retry.jitterjitter randomizes each delay by up to this duration
policies.circuit_breakercircuitBreaker rejects executions while failures exceed a thresholdpolicies.circuit_breaker.failure_thresholdfailureThreshold is the number of failures that opens the breaker
policies.circuit_breaker.success_thresholdsuccessThreshold is the number of half-open successes that close it
policies.circuit_breaker.delaydelay is how long the breaker stays open before half-opening
policies.rate_limitrateLimit bounds the execution ratepolicies.rate_limit.maxmax is the number of executions allowed per period
policies.rate_limit.periodperiod is the window Max applies to. Defaults to one second
policies.rate_limit.burstybursty allows Max executions at once instead of smoothing them
policies.rate_limit.max_waitmaxWait is how long an execution may wait for a permit before being
policies.hedgehedge starts redundant attempts after a delay to trim tail latencypolicies.hedge.delaydelay before starting a hedged attempt. Required (> 0)
policies.hedge.max_hedgesmaxHedges is the max number of hedged attempts. 0 = library default (1)
policies.adaptive_limiteradaptiveLimiter sheds load by self-tuning a concurrency limitpolicies.adaptive_limiter.minmin is the minimum concurrency limit
policies.adaptive_limiter.maxmax is the maximum concurrency limit; must be >= 1
policies.adaptive_limiter.initialinitial is the starting limit; Min <= Initial <= Max
policies.adaptive_limiter.max_waitmaxWait is how long to wait for a permit before rejecting. Zero rejects
policies.adaptive_limiter.queueingqueueing enables absorbing short spikes before rejecting. Nil disables itpolicies.adaptive_limiter.queueing.initial_factorinitialFactor is the queue depth (times the limit) before rejections
policies.adaptive_limiter.queueing.max_factormaxFactor is the queue depth (times the limit) at which all excess is
policies.bulkheadbulkhead caps concurrency with a hard ceiling nearest the callpolicies.bulkhead.max_concurrentmaxConcurrent is the hard concurrency ceiling; must be >= 1
policies.bulkhead.max_waitmaxWait is how long to wait for a slot before rejecting. Zero rejects
Code-only options
Section titled “Code-only options”These options can only be set in Go code via With*() functions, not via config files or environment variables.
| Option | Type | Description |
|---|---|---|
WithPolicy(...) | map[string][]failsafe.Policy[interface {}] | registers a policy chain in code (code-only), ordered outermost |