# gojek/heimdall: retries and a hystrix-style circuit breaker for Go HTTP clients

> Heimdall wraps net/http with an in-memory retrier, configurable backoff and a hystrix-like circuit breaker. It suits Go services that call flaky upstreams; it is not a general-purpose networking toolkit.

**gojek/heimdall** — An enhanced HTTP client for Go

- Repository: https://github.com/gojek/heimdall
- Website: http://gojek.tech
- Stars: 2,778 · Forks: 208
- Language: Go
- License: Apache-2.0
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/gojek-heimdall

## What gojek/heimdall adds on top of net/http

The standard library's http.Client gives you connection pooling and timeouts. It does not retry a failed request, and it does not stop sending traffic to an upstream that is failing. Heimdall's README frames the project as an HTTP client for applications that "make a large number of requests, at scale", and the three capabilities it lists are a hystrix-like circuit breaker, synchronous in-memory retries with a pluggable retrier strategy, and per-request timeouts.

The audience is Go backend engineers calling remote services where transient failures are normal: a payment gateway that times out under load, an internal API that returns 5xx during a deploy. Heimdall is a library, not a proxy or a sidecar. It lives inside your process, so the retry budget and the breaker state are per-instance. That is the central design fact to hold onto, because it determines both what the library does well and where it stops being the right tool.

## How the retrier, backoff and hystrix packages fit together

The repository splits into a root package plus httpclient/ and hystrix/ subpackages. The root package holds the retry and backoff primitives: backoff.go, retry.go and their tests sit at the top level, alongside client.go and plugin.go. The httpclient package exposes the plain client, and hystrix wraps the same client in a breaker.

Data flow is straightforward. You build a client with functional options. A request goes through the client, and if a retrier is configured, the client re-issues the request according to the backoff schedule until the retry count is exhausted. With the hystrix client, the call is additionally routed through a command identified by a name you supply. That command tracks failures against the configured thresholds, and when the error rate crosses the threshold the breaker opens, at which point calls fail fast or hand off to your fallback function.

Two timeout values exist in the hystrix client. The README states that the hystrix timeout determines when hystrix registers an error, while the HTTP timeout determines when the client itself returns a timeout error, and that unless you have special requirements both should carry the same value. That is a real footgun: set the HTTP timeout higher than the hystrix timeout and the breaker will record errors for requests that eventually succeed.

## Installing gojek/heimdall and making a first request

Installation is a single Go module fetch. The README gives this command for the current major version:

```bash
go get -u github.com/gojek/heimdall/v8
```

The module path in go.mod is github.com/gojek/heimdall/v8, so the import path must carry the /v8 suffix. A first request needs the httpclient subpackage and a timeout option:

```go
import "github.com/gojek/heimdall/v8/httpclient"

timeout := 1000 * time.Millisecond
client := httpclient.NewClient(httpclient.WithHTTPTimeout(timeout))
res, err := client.Get("http://google.com", nil)
if err != nil {
	panic(err)
}
body, err := io.ReadAll(res.Body)
```

According to the README, this prints the contents of the Google home page. The response is a standard *http.Response, so existing body-reading code works unchanged. If you already build *http.Request values, the client also exposes a Do method with an interface similar to http.Do, which the README shows as an alternative entry point.

Adding retries means constructing a backoff, wrapping it in a retrier, and passing both to the client:

```go
backoffInterval := 500 * time.Millisecond
maximumJitterInterval := 5 * time.Millisecond
backoff := heimdall.NewConstantBackoff(backoffInterval, maximumJitterInterval)
retrier := heimdall.NewRetrier(backoff)

client := httpclient.NewClient(
	httpclient.WithHTTPTimeout(1000*time.Millisecond),
	httpclient.WithRetrier(retrier),
	httpclient.WithRetryCount(4),
)
```

The README notes that the maximum jitter interval must be more than 1*time.Millisecond. There is also an exponential variant, heimdall.NewExponentialBackoff, which takes an initial timeout, a max timeout, an exponent factor and a jitter interval. For the breaker, switch to the hystrix package and supply a command name, a hystrix timeout, a concurrency cap and an error percentage threshold:

```go
import "github.com/gojek/heimdall/v8/hystrix"

client := hystrix.NewClient(
	hystrix.WithHTTPTimeout(10 * time.Millisecond),
	hystrix.WithCommandName("google_get_request"),
	hystrix.WithHystrixTimeout(1000 * time.Millisecond),
	hystrix.WithMaxConcurrentRequests(30),
	hystrix.WithErrorPercentThreshold(20),
)
```

Fallbacks are registered with hystrix.WithFallbackFunc and take a function with the signature func(err error) error. The README says the fallback triggers when your code returns an error or when it cannot complete based on hystrix health checks, and gives the example of posting to a second channel when the first fails. Note that WithSleepWindow and WithRequestVolumeThreshold appear in the fallback example but are not described in the README text.

## Retries are synchronous and in-memory, and that shapes everything

The README is explicit that retries are synchronous and in-memory. That means the calling goroutine blocks for the full backoff schedule. With a constant backoff of 500ms and a retry count of 4, a permanently failing upstream holds the caller for roughly two seconds before returning an error, and every concurrent caller holds its own goroutine for the same duration. Under a broad upstream outage this converts a fast failure into a queue of blocked goroutines, which is the opposite of what a breaker is meant to achieve.

The circuit breaker mitigates this only if the error rate crosses the threshold before the goroutines pile up. The error percentage threshold is configurable, but the README does not document how the breaker counts retried attempts, so whether a single logical request with four retries counts as one failure or five is not stated. That matters for the threshold you pick.

A second boundary: the breaker state is per-process. If you run ten replicas, you get ten independent breakers, each learning about the outage separately. Heimdall offers no shared state, no Redis or gossip backend, and no way to coordinate trip decisions across a fleet. Teams that need fleet-wide circuit breaking have to look at a service mesh or a dedicated breaker service instead.

## How gojek/heimdall differs from plain net/http and from a service mesh

The honest alternative for many teams is the standard library plus a few lines of retry code. net/http already handles connection reuse and context cancellation, and a retry loop around client.Do is not hard to write. Heimdall's value is in the parts that are tedious to get right: jittered backoff schedules, a retrier interface you can swap, and a breaker with fallback hooks. If you only need a timeout and a single retry, the dependency is not paying for itself.

The other alternative is pushing this concern out of the process entirely, into a sidecar or service mesh. That approach shares breaker state across replicas and applies uniformly to every language in your stack. Heimdall's difference is the opposite trade: no infrastructure, no sidecar, configuration expressed as Go functional options next to the call site, and behaviour that is testable in a unit test. The cost is that the policy exists only in Go processes and only per replica. Neither approach is strictly better; they fail in different directions when an upstream degrades.

## Version 8, the Go 1.25 requirement and the Apache-2.0 licence

The latest release listed is v8.0.0, published on 2026-07-01, with v7.3.1 and v7.3.0 shortly before it. The last push to the repository was on 2026-07-01. The repository is not archived.

The go.mod file declares go 1.25 and requires github.com/gojek/hystrix-go/hystrix v1.0.0 plus github.com/stretchr/testify v1.11.1 for tests. That Go directive is the practical upgrade constraint: a team on an older toolchain cannot simply bump the import path to /v8 without also moving the toolchain. The v7 line exists for those who cannot.

The licence is Apache-2.0, which permits commercial and closed-source use and includes an explicit patent grant. This is a permissive licence, not a copyleft one, so it does not oblige you to publish your own source. That is a factual description of the licence text, not legal advice; if your organisation has a policy on third-party dependencies, route it through the usual review.

The Makefile shows the maintenance surface you inherit if you fork or contribute: a setup target that installs golangci-lint v2.8.0 and goveralls v0.0.12, a compile target running go build -race ./..., and a test target that runs go test -race with an atomic coverage profile under ENVIRONMENT=test. The CI workflow lives in .github/workflows/ci.yml. There is a CHANGELOG.md at the repository root, so version-to-version changes are recorded there rather than only in release notes.

## Where gojek/heimdall is the wrong choice

Skip it if you need per-attempt retry decisions. Heimdall's retrier decides the schedule, but the README does not describe a hook that inspects the response status or body before deciding whether to retry. Retrying a 400 or a 409 wastes the upstream's time and yours.

Skip it if the upstream is non-idempotent and you have no idempotency key. The README does not document any deduplication or request-identity mechanism, so a retried POST can create two resources.

Skip it if you need a breaker that reflects the health of an upstream as seen by the whole fleet. Per-process state means a rolling deploy resets every breaker at once, and a single hot replica can trip while the rest stay closed.

Finally, skip it if you are not writing Go. The library is Go-only, and the hystrix subpackage depends on gojek's fork of hystrix-go rather than the original afex/hystrix-go referenced in the README's description link, which is a detail worth knowing before you assume API parity with that project.

## Conclusion

Adopt gojek/heimdall if your Go service calls upstreams that fail intermittently and you want retries plus a circuit breaker without writing that plumbing yourself. Do not adopt it if you need per-attempt retry decisions, a distributed breaker shared across processes, or support for a Go toolchain older than 1.25. Before committing, confirm that your build runs Go 1.25, that a synchronous in-memory retry fits your latency budget, and that the default retrier's error classification matches how your upstream signals failure.

## FAQ

### What is gojek/heimdall used for?

It is a Go HTTP client for applications that make many requests at scale. The README lists three capabilities: a hystrix-like circuit breaker, synchronous in-memory retries with a custom retrier option, and clients with different timeouts per request.

### How do I install gojek/heimdall?

The README gives a single command, go get -u github.com/gojek/heimdall/v8, and the import path must include the /v8 suffix because that is the module path in go.mod.

### Does gojek/heimdall retry requests automatically?

Only if you configure it. You create a backoff with heimdall.NewConstantBackoff or heimdall.NewExponentialBackoff, wrap it with heimdall.NewRetrier, and pass it via httpclient.WithRetrier along with httpclient.WithRetryCount.

### Is gojek/heimdall's circuit breaker shared between service replicas?

No. The breaker tracks failures inside the process that created the client, so each replica maintains its own state. The README does not describe any shared or distributed breaker backend.

### What Go version does gojek/heimdall v8 need?

The go.mod file declares go 1.25, so the v8 module path requires that toolchain or newer. Teams on older toolchains would need the v7 line instead.

## Sources

- [gojek/heimdall on GitHub](https://github.com/gojek/heimdall)
- [License: Apache-2.0](https://github.com/gojek/heimdall/blob/master/LICENSE)
- [Project website](http://gojek.tech)
- [README](https://github.com/gojek/heimdall/blob/master/README.md)
- [Releases](https://github.com/gojek/heimdall/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/gojek-heimdall
