Free tools Windows power users keep installed
One-click scans. No signup required.
Go’s net/http transport may retry a narrow class of network failures, but it does not provide a general application retry policy. For application retries, decide first whether repeating the operation is safe; then classify retryable failures, cap attempts and delay, honor the caller’s context, and recreate the request body for each attempt. These safeguards matter most for writes: a lost response does not tell you whether the server completed the operation.
What Go retries automatically—and what it does not
http.Client handles request execution, redirects, cookies, and timeout behavior. It is not a general retry loop for server responses such as HTTP 500, nor does every error returned from Client.Do trigger another attempt.
The standard library documents limited network-error retries in http.Transport. They depend on conditions including whether the connection has already been used successfully, whether the request is idempotent, and whether its body can be replayed. The transport recognizes GET, HEAD, OPTIONS, and TRACE as idempotent methods; it also recognizes an Idempotency-Key or X-Idempotency-Key header. A request with a body needs no body or a defined Request.GetBody for this retry path. See the Go Transport documentation.
These transport rules are not a substitute for an application policy. Your code must decide whether an HTTP response or other failure is worth retrying, how many attempts are allowed, how long to wait, and what to do when the caller cancels.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →#1 Best Overall
Decide whether repeating the operation is safe
HTTP method semantics are a useful starting point, not a complete safety check. RFC 9110 describes idempotent methods as methods whose intended effect is the same when the request is repeated. Your endpoint’s actual side effects and contract still matter. A client may receive a network error after a server has committed a write but before the response reaches the client.
- Read or idempotent operation: A retry may be reasonable if the specific endpoint has no additional non-idempotent effects and the failure is transient.
- Non-idempotent write: Do not retry merely because the response was lost. Retry only if the server supports and honors an idempotency key or another deduplication contract.
- One logical write across attempts: Reuse the same idempotency key for every attempt of that operation. A new key per attempt defeats deduplication.
Check the remote service’s documentation: a header has no protective effect unless the server defines and implements its meaning, retention period, and scope. The HTTP semantics standard explains idempotent methods and the Retry-After header at RFC 9110.
Choose failures worth retrying
Retry classification should reflect the service contract. Transient transport failures and selected overload or server responses may merit another attempt. Invalid input, authentication failures, and other permanent client errors usually will not improve unless something changes. Do not convert every error into a retry.
- Choose explicitly which status codes your service treats as temporary, such as a documented overload response.
- Check whether an error is caused by cancellation or a deadline; those mean the caller has stopped waiting, not that another attempt should begin.
- When a response includes
Retry-After, parse its delay or HTTP date and honor it when appropriate, subject to the caller’s remaining time and your maximum-wait policy. - Decide what to do with malformed or excessive
Retry-Aftervalues; do not let a response create an unbounded wait.
The HashiCorp go-retryablehttp documentation includes an implementation example for handling 429 responses. Treat the example as a library-specific pattern, not as a rule that every API uses the same status policy.
Recommended Free Tools
Bound attempts, delay, and the total time budget
A retry policy should make its limits visible. Set a finite maximum number of total attempts, a delay schedule with a cap, and an overall deadline. Exponential backoff increases the time between attempts; jitter randomizes that delay so clients affected by the same outage are less likely to retry in sync. There is no universally correct attempt count or delay: choose values against the endpoint’s latency, rate limits, and caller’s time budget.
Fixed delays can keep request load high when many clients retry together, as discussed in Cloud Native Go. The AWS SDK for Go v2 retry guide also warns that unlimited retries can cause runaway workloads and inflated billing. Those sources explain the risks; their example values are not universal settings for your application.
Use the caller’s context as the total operation budget. A request created with http.NewRequestWithContext is controlled by its context during connection acquisition, sending, and response handling. A retry wait must also be cancelable. http.Client.Timeout spans connection time, redirects, and reading the response body; the Go documentation notes that a value of zero means no timeout for this field. Avoid stacking timeouts and retry loops without calculating the resulting maximum duration.
A bounded retry loop with replayable requests
The following Go example retries selected temporary statuses and transport errors for an idempotent GET. It uses a finite attempt limit, capped exponential backoff with full jitter, an interruptible wait, and a fresh request for each attempt. Replace the status classification and limits with the endpoint’s documented policy. It does not implement Retry-After; add that parsing if the service’s contract requires it.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →package retry
import (
"context"
"errors"
"fmt"
"io"
"math/rand/v2"
"net"
"net/http"
"time"
)
func Get(ctx context.Context, client *http.Client, url string) ([]byte, error) {
const maxAttempts = 4 // total attempts, including the first
const baseDelay = 200 * time.Millisecond
const maxDelay = 2 * time.Second
for attempt := 1; attempt <= maxAttempts; attempt++ {
if err := ctx.Err(); err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("build request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
if ctx.Err() != nil {
return nil, ctx.Err()
}
if !retryableNetworkError(err) || attempt == maxAttempts {
return nil, fmt.Errorf("GET failed after %d attempt(s): %w", attempt, err)
}
} else {
body, readErr := io.ReadAll(resp.Body)
closeErr := resp.Body.Close()
if readErr != nil {
if ctx.Err() != nil {
return nil, ctx.Err()
}
if attempt == maxAttempts {
return nil, fmt.Errorf("read response after %d attempt(s): %w", attempt, readErr)
}
} else if retryableStatus(resp.StatusCode) && attempt < maxAttempts {
// Discard this response and retry below.
} else if closeErr != nil {
return nil, fmt.Errorf("close response body: %w", closeErr)
} else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("GET returned HTTP %s: %s", resp.Status, body)
} else {
return body, nil
}
}
if err := wait(ctx, backoff(attempt, baseDelay, maxDelay)); err != nil {
return nil, err
}
}
return nil, errors.New("unreachable retry loop exit")
}
func retryableStatus(code int) bool {
return code == http.StatusTooManyRequests || code == http.StatusBadGateway ||
code == http.StatusServiceUnavailable || code == http.StatusGatewayTimeout
}
func retryableNetworkError(err error) bool {
var netErr net.Error
return errors.As(err, &netErr) && netErr.Timeout()
}
func backoff(attempt int, base, cap time.Duration) time.Duration {
d := base
for i := 1; i < attempt && d < cap; i++ {
d *= 2
}
if d > cap {
d = cap
}
// Full jitter: choose a delay from zero through the capped backoff.
return time.Duration(rand.Int64N(int64(d) + 1))
}
func wait(ctx context.Context, d time.Duration) error {
t := time.NewTimer(d)
defer t.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-t.C:
return nil
}
}
The example requires a Go version that provides the imported math/rand/v2 package. For an earlier Go version, substitute a suitable random-number source. The sample reads the full response into memory, which is appropriate only when response sizes are bounded for your use case. For large responses, impose a size limit and design a streaming strategy that preserves the retry policy without unbounded buffering.
The error classifier intentionally retries only timeout-classified network errors. Production services may define other temporary transport failures, but classify them deliberately. The status list is also an example policy, not a universal list. Preserve the response body long enough to report useful final errors, while avoiding unbounded diagnostic output.
Retrying writes and request bodies
An io.Reader is a stream, not reusable request data. Once a request has consumed it, constructing another request around the same reader may send an empty or partial body. Build the body again for each attempt, or provide a replay mechanism such as Request.GetBody when the data and request semantics allow it.
For a write, generate the logical operation’s idempotency key once, before the retry loop. Attach the same key to every newly constructed request, and make sure the endpoint supports it. Without a server-side deduplication contract, a timeout after sending a write is ambiguous: retrying can duplicate the effect.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteRank #4
payload := []byte(`{"amount":1250}`)
operationKey := "payment-operation-unique-id" // stable for this logical operation
for attempt := 1; attempt <= maxAttempts; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint,
bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", operationKey)
// Send, classify the result, and stop or wait before the next attempt.
}
This snippet shows body recreation and key reuse, not a complete write policy. Do not assume that setting the header alone makes a POST safe. Check the API’s deduplication behavior and decide how to handle a final ambiguous outcome.
Response bodies, deadlines, and connection reuse
Close every response body, including responses you discard before retrying. When connection reuse matters, you may drain an appropriate bounded amount before closing; do not read an unbounded error response just to preserve a connection. The correct drain limit depends on your response-size expectations and Go version. The Go client documentation describes response-body ownership and closing at Client.Do.
Before a retry wait, compare the proposed delay—including any server-provided delay—to the remaining context deadline. If there is not enough time for another attempt, stop instead of sleeping past the caller’s budget. Context cancellation during either the request or wait must end the operation promptly.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Hand-written loop or retry library?
| Approach | Policy control | Body replay | Cancellation and time budget | Integration considerations |
|---|---|---|---|---|
| Hand-written loop | You define status and error classification, attempt limits, delays, jitter, and service-specific behavior. | You must recreate bodies or configure replay explicitly. | You must use the caller context for requests and interruptible waits. | No retry-library dependency; suitable when the policy is small and explicit. |
| HashiCorp go-retryablehttp | Provides retry checks and exponential backoff that you can assess and customize. | Its documentation describes request-body rewind support. | Review the current module behavior and ensure it fits the operation’s whole deadline. | Check current release behavior and how its policy matches the remote API’s idempotency contract. |
| AWS SDK for Go v2 retryer | The SDK guide describes configurable maximum attempts and rate limiting. | Depends on the SDK operation and its request semantics. | Account for SDK retries within the operation’s time budget. | When using the SDK, understand its retryer before adding an outer loop; stacked policies can multiply attempts and delay. |
Package and SDK behavior can change across releases. Check the documentation for the version in your module, and calculate combined attempts if a caller retries around a client or SDK that already retries internally.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Best Value
Common retry failures and fixes
- A POST happens twice: The response may have been lost after the server completed the first write. Stop retrying without a server-supported deduplication contract; otherwise reuse one idempotency key across attempts.
- A retried request has an empty body: The original reader was consumed. Recreate the reader from saved data or supply a suitable replay mechanism.
- Retries continue after the caller gives up: The request or sleep is not using the caller’s context. Use
NewRequestWithContextand a timer selected againstctx.Done(). - Many clients retry together: A fixed delay synchronizes attempts. Add jitter, retain a finite attempt limit, and cap the delay.
- A 401, 403, or invalid-input response repeats: The classifier is too broad. Retry only failures that could plausibly resolve without changing the request or credentials.
- The operation exceeds its expected duration: Client timeout, per-attempt timeouts, retry waits, and nested SDK policies may combine. Set an overall deadline and account for every layer.
- Connections are not reused after discarded responses: Bodies may be closed without suitable bounded draining. Consider a bounded drain only where response size and connection reuse make it appropriate.
Or skip the browser setup
For website screenshots, ScreenshotNeo provides a one-request API rather than requiring you to run a browser. A GET request can return a PNG, JPEG, WebP, or PDF. The example below uses curl; see the ScreenshotNeo API documentation for options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, with X-Page-Verdict and X-Billed response headers identifying the result. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots.
Sign up free for 1,000 screenshots a month, with no card required.
Frequently Asked Questions
Does Go retry failed HTTP requests automatically?
Only in limited transport-level network-failure cases documented for http.Transport; application-level status and error retry policy is up to your code.
Can I safely retry a POST after a timeout?
Only if the endpoint provides a deduplication contract, such as a server-supported idempotency key, and you reuse the same key for that logical operation.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




