Golang HTTP client retry: what net/http retries on POST
Go's http.Transport retries only network errors on reused connections, and a POST only with an Idempotency-Key header. Write the 429 and 5xx loop yourself.

Go's http.Client does not retry failed responses. Its Transport retries a request only after a network error on a connection that was already used successfully, and only if the request is idempotent: GET, HEAD, OPTIONS or TRACE, or any method whose headers carry "Idempotency-Key" or "X-Idempotency-Key", with a body it can replay. A 429 or a 5xx comes back to your code, so you write the retry loop yourself or use a wrapper such as hashicorp's go-retryablehttp.
The Go facts come from the net/http and context package docs and the go-retryablehttp README and package docs. The paid-API example is Sume's video API, from Errors and rate limits, Video Generation and Jobs and results. All were read on 2026-09-29.
When does Go's Transport retry a POST?
All of these must hold. The last two are in your control:
| Condition | What it means for a POST |
|---|---|
| A network error occurred | A 429 or 5xx response is not a network error, so the Transport returns it |
| The connection was already used successfully | A failure on a fresh connection is returned to you |
| The request is idempotent | Set an Idempotency-Key or X-Idempotency-Key header |
No body, or Request.GetBody is defined | NewRequestWithContext fills GetBody for *bytes.Buffer, *bytes.Reader and *strings.Reader |
How do I write a retry loop around http.Client?
Loop, build a fresh request each attempt (a body reader is spent after one send), keep the same key, and honor Retry-After. This version creates a Sume video job and retries only transport errors and 429 rate_limited until the context you pass expires, so call it with a context.WithTimeout context:
var client = &http.Client{Timeout: 30 * time.Second}
func createVideo(ctx context.Context, body []byte, key string) (*http.Response, error) {
for attempt := 0; ; attempt++ { // ends when ctx expires
req, _ := http.NewRequestWithContext(ctx, "POST", "https://api.sume.com/v1/videos", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SUME_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key) // same key, same body, every attempt
wait := time.Duration(1<<attempt) * time.Second
resp, err := client.Do(req)
if err == nil && resp.StatusCode != http.StatusTooManyRequests {
return resp, nil // 202, or an error to read and stop on
}
if err == nil {
var e struct{ Error struct{ Code string } }
json.NewDecoder(resp.Body).Decode(&e)
resp.Body.Close()
if e.Error.Code != "rate_limited" {
return nil, fmt.Errorf("429 %s", e.Error.Code) // queue_full: stop
}
if s, perr := strconv.Atoi(resp.Header.Get("Retry-After")); perr == nil {
wait = time.Duration(s) * time.Second
}
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(wait):
}
}
}Should I use go-retryablehttp?
It saves the loop. Its README says it retries when the client returns an error, such as a connection error, or when a 500-range code other than 501 comes back, with exponential backoff, and that it can rewind a POST body so the full request is sent again. StandardClient() turns its client into a plain *http.Client. The README doesn't list 429, but the package docs say DefaultBackoff reads Retry-After on a 429, and the CheckRetry field lets you set your own retry policy in place of DefaultRetryPolicy.
For a paid create, a blanket 5xx retry needs care. Sume's docs say a 503 provider_not_configured should not be retried aggressively, and 429 queue_full means no new paid job until an existing one finishes or is canceled. Write a CheckRetry that stops on those codes, and whatever retries, set the Idempotency-Key header on the request so every attempt carries it.
How do I stop a retry from paying twice?
Send an Idempotency-Key built from the thing being made, such as an order id, and keep the body byte for byte. On /v1/videos, a replay with the key returns the original job, and Sume's docs say not to retry unsafe submit requests without one. Reuse a key only for the same operation and payload.
Bound the whole loop with context.WithTimeout, and call cancel when done, as the context docs say. For an outgoing request, the context controls its entire lifetime. A client-side timeout doesn't cancel a job that was created: it keeps running and still bills. Once you hold a job id, poll GET /v1/videos/{id} rather than submitting again; if you never got one, resend later with the same key. Golang HTTP POST JSON with headers covers the request itself.
Sources
Related posts
More in Developers
- GPT-6 Astra tool calling needs the Responses API: meaning for Sume
OpenAI says GPT-6 Astra supports Chat Completions but its tool calling requires Responses. Use the Responses mcp tool for Sume, not a chat.completions loop.
- How long does GPT Image 2.5 take? Time your own jobs on Sume
Sume publishes no per-model image time for GPT Image 2.5, only a 30-second wait budget. A short curl script times your own prompts by quality and size.
- GPT Image 2.5 arena: run your own blind test through the API
Arena leaderboards rank images on other people's prompts. Here is a blind three-way test of Flare, Sunburst and GPT Image 2 on yours, with one script on Sume.
- GPT Image 2.5 background: transparent vs opaque in the API
Sume accepts background auto, transparent or opaque on the two GPT Image 2.5 ids only. What opaque does, what a bad value returns, and which models reject it.
Written by Sume