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.
- GPT Image 2.5 4K: how to request a 3840x2160 image by API
To get a 4K image from GPT Image 2.5 on Sume, send image_size 3840x2160 to POST /v1/images and be ready for a 202 job response. Request, cost, and polling.
- GPT Image 2.5 image editing API: edit a photo with a prompt
Edit a photo with GPT Image 2.5 on Sume: send the image in input_references, describe the change, and set aspect_ratio to auto. Up to 16 references per call.
- GPT Image 2.5 request returned 202: how to get the image from the job
When GPT Image 2.5 takes longer than 30 seconds on Sume, POST /v1/images returns 202 with a job. Poll the status URL, then read the result, or use a webhook.
Written by Sume