A second chance.
Not ten more.
One retry for an eligible, replay-safe read. Diagnosis and budgets decide whether it can help.
Budgeted retriesA little care for every API call
Budgeted retries, dependency breakers, and adaptive timeouts for Go's outbound HTTP calls. One transport. No extra service to run.
A replay-safe read is sent.
An eligible read gets at most one extra attempt, only when the diagnosis and retry budgets allow it.
Responding to failure automatically, within safe limits. Curo adapts how your service calls an API, not the API itself. It can't fix an upstream outage. It can help your service handle one.
01 / The approach
Not just when to stop. When to try again, how long to wait, and when to let traffic return.
One retry for an eligible, replay-safe read. Diagnosis and budgets decide whether it can help.
Budgeted retriesReads get a response-header timeout learned from recent latency, within bounds you set. Earlier caller deadlines always win.
Adaptive timeoutsOnce a dependency is diagnosed as down, calls fail fast. After each cooldown, a probe tests recovery, and traffic returns when one succeeds.
Dependency breakersDeterministic rules, not AI. Decisions come with evidence and reasons.
02 / Start small
Wrap your existing transport. Curo starts in Observe, showing its decisions without changing request behavior.
go get github.com/raj1kshtz/curo@latesttransport, err := curo.New(
http.DefaultTransport,
)
if err != nil {
return err
}
client := &http.Client{
Transport: transport,
Timeout: time.Minute,
}
// Review Stats and Report, then:
// transport.SetMode(curo.Enforce)
// Close transport on shutdown.03 / A little clarity
The short version, before
you dive into the docs.
Go developers whose services call HTTP APIs. Curo wraps outbound net/http traffic, not incoming handlers. It is not a gRPC interceptor, a proxy, or a separate service.
Only in Enforce mode, which you choose with WithMode at startup or SetMode at runtime. Each control still needs its own evidence and safety checks. Observe is the default, and switching back to it stops every action. Off passes requests straight through.
Your HTTP client, the operating mode, and the timeout bounds, which default to 2 and 30 seconds. Keep your client's timeout above that ceiling. Every other threshold is fixed. Curo never extends a caller's deadline or retries a write.
Stats counts requests, retries, breaker actions, timeouts, and Curo's own failures. Report shows each dependency's readiness, diagnosis, reasons, evidence, and timeout. Both encode to JSON, so a private debug endpoint can serve them.
In Enforce mode, ErrBreakerOpen means Curo did not send the request, and ErrTimeout means the dependency may have received it. Match both with errors.Is, checking ErrTimeout before context.DeadlineExceeded, which it also matches.
A failure inside Curo never fails a request. After three internal failures within a minute, the transport passes every request straight through for the rest of its life. Stats reports this, and WithLogger logs the failures through log/slog.
State belongs to one transport in one process, so replicas learn separately and restarts start cold. A transport tracks up to 128 targets, each a scheme, host, port, and method class, and requests beyond them get no actions. Retries and breakers wait for about 30 seconds of steady traffic.
v0.1.0 is the first release. CI tests every change on Linux, macOS, and Windows with Go 1.23 and the two latest Go releases, under the race detector, and requires every statement to run. Until v1, a minor release may still change the public API, so pin the version you adopt.
Small integration. Thoughtful safeguards.