go 31 lines · 1 tab

Retry on 429 with Retry-After parsing

Leah Thompson Jan 2026
1 tab
package retry

import (
  "context"
  "net/http"
  "strconv"
  "time"
)

func SleepRetryAfter(ctx context.Context, resp *http.Response, max time.Duration) bool {
  v := resp.Header.Get("Retry-After")
  if v == "" {
    return false
  }
  secs, err := strconv.Atoi(v)
  if err != nil {
    return false
  }
  d := time.Duration(secs) * time.Second
  if d > max {
    d = max
  }
  t := time.NewTimer(d)
  defer t.Stop()
  select {
  case <-ctx.Done():
    return true
  case <-t.C:
    return true
  }
}
1 file · go Explain with highlit

Rate limits are normal in production; what matters is how clients behave when they hit them. Instead of hammering an upstream with immediate retries, I parse Retry-After (seconds) and sleep before retrying. I still keep an upper bound so one request doesn’t stall forever. The helper uses context.Context so cancellations stop the wait, which is important during shutdown or when the caller times out. I also treat 429 differently from 5xx: 429 means “you’re too fast,” so the right response is to slow down, not to fan out more retries. In practice I pair this with a token bucket limiter so the client stays under the rate most of the time. This is one of those small details that makes integrations stable.


Related snips

Share this code

Here's the card — post it anywhere.

Retry on 429 with Retry-After parsing — share card
Link copied