Switch off endpoints while your server keeps running.

brownout is Go HTTP middleware for shedding some or all traffic to chosen routes at runtime. When a dependency is struggling, turn off an expensive endpoint or cut it to 10% of its traffic, then restore it, with no restart and no deploy.

Get started Try the playground
go get github.com/rossedman/brownout
⚡

Takes effect on the next request

Rules are swapped atomically. The request path takes no locks and doesn't allocate. With a handful of rules it adds well under a microsecond per request.

🎚️

Partial brownouts

Let a set percentage of traffic through, so you can reduce load without switching a feature off entirely.

⏱️

Rules that expire

Give a rule a TTL and it lifts by itself, so a forgotten brownout doesn't outlast the incident.

📦

Standard library only

Plain http.Handler middleware with no dependencies. It works with any router built on net/http.

Quick start

Wrap your app in the middleware and expose the admin API somewhere private. That's the whole setup.

1 · Wire it in
c := brownout.New()

// Wrap your app.
go http.ListenAndServe(":8080", c.Middleware(app))

// Admin API on an internal-only listener.
http.ListenAndServe("127.0.0.1:9090", c.AdminHandler())
2 · Brown out at runtime
# Shed /reports/* for 10 minutes
curl -X PUT localhost:9090/rules/reports \
  -d '{"path":"/reports/*","ttl":"10m"}'

# Let only 20% of searches through
curl -X PUT localhost:9090/rules/search \
  -d '{"path":"/search","allow_percent":20}'

# Restore
curl -X DELETE localhost:9090/rules/reports
The admin API has no authentication. Serve it on an internal port, or put your own auth in front of it as the admin-auth example does.

Rules

A rule matches requests by path and method and decides what share of them to let through. Rules are checked in order, and the first matching rule decides.

JSON fieldGo fieldMeaning
idIDUnique name. Setting a rule with an existing ID replaces it in place.
pathPathAn exact path, or a prefix if it ends in * (/api/*). * on its own matches everything.
methodsMethodsOptional. Only match these HTTP methods.
allow_percentAllowPercentPercentage of matching requests still let through, from 0 to 100. The default of 0 sheds everything.
status_codeStatusCodeStatus for shed requests. Default 503.
retry_after_secondsRetryAfterSecondsSets the Retry-After header.
messageMessageResponse body. Defaults to the status text.
expires_at / ttlExpiresAtWhen the rule stops applying. The admin API also accepts "ttl": "15m".

Three ways to change rules

Call these from a feature-flag callback, a health check that watches a dependency, or anywhere else in your code.

c.Set(rule)          // add or update one rule
c.Remove("reports")  // remove one rule
c.Replace(rules)     // swap the whole set; all rules must be valid or none are applied
c.Clear()            // remove everything
c.Rules()            // list active rules
c.Check(r)           // decide yourself: (rule, shed)

Playground

Edit the rules and the request to see what the middleware would do. This runs the library's matching logic in your browser.

Examples

Each example is a small runnable server. Run one with go run ./examples/<name>; the curl commands to try are at the top of each file.

Customizing

c := brownout.New(
  brownout.WithDefaultStatus(http.StatusTooManyRequests),
  brownout.WithOnShed(func(r *http.Request, rule brownout.Rule) {
    shedCounter.WithLabelValues(rule.ID).Inc()
  }),
  brownout.WithResponder(func(w http.ResponseWriter, r *http.Request, rule brownout.Rule) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(rule.StatusCode)
    json.NewEncoder(w).Encode(map[string]string{"error": "temporarily unavailable"})
  }),
)

Good to know

Running several replicas

Each process keeps its rules in memory. Share a watched file such as a ConfigMap, or call the admin API on every instance.

Mount the admin API outside the middleware

Otherwise a "*" rule will shed requests to the admin API too, and you can't remove the rule.

Exceptions

Because the first matching rule decides, put a rule with "allow_percent": 100 before a broader rule to exempt a path from it.