What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Use one reusable http.Client, create a separate context-aware request for each independent call, and coordinate the goroutines so errors, cancellation, response bodies, and shared results are all handled deliberately. For a small fixed batch, plain goroutines can be enough; for fail-fast cancellation or bounded fan-out, errgroup is a useful standard pattern. A goroutine limit controls simultaneous work, not requests per second.
What concurrent HTTP requests do—and do not—guarantee
Concurrent requests let a Go program make progress on multiple independent network operations at once rather than waiting for each operation to finish before starting the next. They are useful for fan-out in an HTTP handler, parallel calls to separate services, and batch jobs. They do not make dependent calls safe to run in parallel: if request B needs data returned by request A, preserve that dependency.
Concurrency also does not automatically make a program faster. The result depends on the remote services, connection behavior, workload, and available resources. The Go documentation specifies API behavior, but does not establish a universal speedup or an ideal concurrency limit. Choose a limit from your own service capacity and workload, and measure under representative conditions.
Reuse one client and give each request a context
Go documents that clients and transports are safe for concurrent use and recommends reusing them. Create a client once, configure the policy your application needs, and share it across workers. A new client or transport per request defeats transport reuse and connection caching without a specific reason to do so.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problems#1 Best Overall
An outgoing request’s context controls the request lifecycle: obtaining a connection, sending the request, and reading response headers and the body. Use http.NewRequestWithContext so a caller’s cancellation or deadline can reach the network operation. In an HTTP handler, derive work from r.Context() so work can stop when the incoming request is canceled. In a batch job, use a context with an appropriate deadline or cancellation policy.
Contexts are cooperative cancellation signals, not a promise that arbitrary code will stop instantly. Ensure work uses APIs that observe the context, and do not discard a context error as though it were a successful response. The Go Blog’s overview by Sameer Ajmani explains propagating cancellation, deadlines, and request-scoped values.
A bounded, fail-fast batch with errgroup
For a batch in which any failed request should cancel sibling work, errgroup.WithContext provides a derived context and coordinates completion. Add the dependency with go get golang.org/x/sync/errgroup. This example requires Go 1.18 or later for its use of generics. It stores each result in its own slice slot, checks HTTP status separately from transport errors, reads and closes every successful response body, and caps active group goroutines. Replace the example URLs and concurrency limit with values appropriate to your application.
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
"golang.org/x/sync/errgroup"
)
type result struct {
URL string
Body []byte
}
func fetchAll(ctx context.Context, client *http.Client, urls []string, limit int) ([]result, error) {
if limit < 1 {
return nil, fmt.Errorf("limit must be at least 1")
}
g, groupCtx := errgroup.WithContext(ctx)
g.SetLimit(limit)
results := make([]result, len(urls))
for i, url := range urls {
i, url := i, url // Keep each task's index and URL explicit.
g.Go(func() error {
req, err := http.NewRequestWithContext(groupCtx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("build request for %q: %w", url, err)
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("GET %q: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("GET %q: unexpected HTTP status %s", url, resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read %q: %w", url, err)
}
results[i] = result{URL: url, Body: body}
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
func main() {
client := &http.Client{Timeout: 20 * time.Second}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
urls := []string{
"https://example.com/",
"https://go.dev/",
}
results, err := fetchAll(ctx, client, urls, 4)
if err != nil {
fmt.Println("batch failed:", err)
return
}
for _, r := range results {
fmt.Printf("%s: %d bytesn", r.URL, len(r.Body))
}
}
Save it as main.go, run go mod init example.com/concurrent-fetch, then go get golang.org/x/sync/errgroup and go run .. The http.Client timeout bounds the overall client operation; the context deadline additionally expresses the caller’s work budget and lets related operations share cancellation. Set these values to fit the actual service and operation rather than treating the sample durations as universal defaults.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →What the group behavior means
errgroup.WithContext cancels its derived context when a task returns a non-nil error, and also when Wait returns. The first error returned by a task is the error reported from Wait; other tasks may observe cancellation and return their own context-related errors. Always call Wait before using results written by the workers. The group documentation describes group lifecycle, cancellation, error handling, and limits.
SetLimit caps active goroutines in the group. When the limit is reached, a later call to Go blocks until a slot is available; it is not a separately configurable buffered queue. Do not change the limit while group functions are active. A limit of four in the sample is merely an example, not a recommended value for all services.
When all requests should finish even if one fails
Fail-fast behavior is not always the right policy. For an independent report or health check, you may want every request to finish and then inspect each result, including failures. In that case, avoid canceling sibling work on the first task error. Use a worker pool or semaphore, retain a result or error per input, and return from each worker after recording its outcome. Continue to pass the parent context to requests so caller cancellation and deadlines still work.
Decide the error policy before choosing the coordination pattern:
- Fail fast: use
errgroup.WithContextwhen one failure makes remaining work unnecessary. - Collect all outcomes: capture errors by input index and let other requests continue when each result is independently useful.
- Partial success: return successful results together with an explicit error collection or per-item status rather than silently dropping failures.
Do not write multiple goroutines into the same map, append to a shared slice, or update shared counters without synchronization. The concurrent-safety guarantee for http.Client and Transport does not extend to application-owned state. Distinct preallocated result slots, as in the example, avoid concurrent structural changes; wait for workers before reading them.
Choose a concurrency limit for the workload
Launching one goroutine per URL is straightforward for a small, known batch. For a large or user-controlled list, unbounded fan-out can consume resources and put unnecessary load on remote services. Bound active work with errgroup.SetLimit or a fixed worker pool. A worker pool is useful when you want an explicit jobs channel, a fixed number of long-lived workers, or a queueing policy; a group limit is simpler when task submission can block at the cap.
Choose the cap by considering remote-service capacity, expected latency, local memory and connection use, and whether several callers can run batches simultaneously. A per-batch limit does not necessarily cap aggregate work across many simultaneous batches. If aggregate pressure matters, enforce a shared process-wide limit as well.
A concurrency cap is not a rate limiter. It limits how many operations are in flight; when requests complete quickly, a limited pool can still send many requests per second. If an API specifies requests per second, use a time-based rate limiter in addition to concurrency control, and respect its documented quota and retry guidance.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Handle response bodies and HTTP status correctly
A successful Client.Do call means a response was received, not that the server returned a successful HTTP status. A 404 or 500 is not, by itself, a Do error. Check resp.StatusCode against the success policy your application needs. The example accepts any 2xx response; an API may require one exact status or treat particular non-2xx responses as meaningful results.
After a non-error return from Do, close the response body, including on status failures. The net/http package documentation says the caller must close it. If the body is not consumed to EOF and closed, connection reuse may be unavailable; do not read an arbitrarily large body into memory merely to enable reuse. For large responses, stream or impose an application-appropriate size bound, then close the body.
Keep transport errors, context cancellation, and HTTP statuses distinct in logs and return values. This makes it easier to tell whether a request could not be completed, was canceled, or completed with an application-level response the code rejects.
Rank #4
Other implementation choices and trade-offs
Plain goroutines and a WaitGroup
For a small fixed set where you need every request attempted, sync.WaitGroup plus a results slice can be enough. Add synchronization for shared error collection, or assign each worker an exclusive result slot. A WaitGroup only waits; it does not choose an error policy, cancel sibling requests, or limit goroutines for you.
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 minuteerrgroup
Use errgroup when first-error reporting and optional context cancellation make the code clearer, or when its limit matches your submission pattern. Its derived context is for the group work; after Wait returns it is canceled, so do not reuse it for subsequent operations.
Package convenience functions
Package-level HTTP helpers can be convenient for a one-off operation, but an explicit shared client makes timeout and transport policy visible in a concurrent workload. Configure and reuse the client rather than constructing a transport for each task.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Performance, reliability, and cost considerations
Concurrency can reduce time spent waiting across independent network calls, but it can also amplify remote load, memory use, open connections, and retry traffic. A client timeout, request context deadline, and bounded concurrency address different risks: total operation duration, caller-controlled cancellation, and simultaneous work. They should be selected together with the service’s own limits.
Retries need care. Retrying every failure immediately can multiply traffic during an outage, and a canceled request should generally not be retried as though it were an ordinary transient failure. Follow the target API’s retry and backoff guidance; do not assume that concurrent calls are idempotent or safe to repeat.
Best Value
Troubleshooting concurrent request batches
- The batch hangs: ensure every launched path eventually returns; apply context deadlines or a client timeout, and inspect any task that is not observing cancellation.
- Some tasks never start promptly:
SetLimitblocks submission when the active count is at its cap. This is expected; reduce input volume, adjust a considered limit, or use explicit queueing if submission behavior must differ. - A server error appears as success:
Dodoes not turn non-2xx status codes into Go errors. CheckStatusCodeand apply the endpoint’s success policy. - Connections are not being reused or resources grow: verify every non-nil response body is closed and that clients/transports are reused.
- Results are inconsistent or the race detector reports a race: give workers distinct result slots or protect shared maps, slices, and counters with synchronization. Run
go test -race ./...against tests that exercise the concurrent path. - Cancellation is reported as a transport failure: inspect the error chain with
errors.Is(err, context.Canceled)orerrors.Is(err, context.DeadlineExceeded)where appropriate, and preserve the underlying error with%w.
Or skip the browser setup
If the independent HTTP task you need is capturing a web page, ScreenshotNeo provides a screenshot API: one GET request can return an image or PDF. Its clean-shot options can accept consent banners and remove 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 are not billed, with response headers identifying the page verdict and billing status. It also offers an MCP server with screenshot, page-info, and PDF tools for AI agents. The API call below is one request; for a batch, issue separate calls under the same context and concurrency controls described above. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Free includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Learn about ScreenshotNeo, or sign up for the free plan.
Further Go learning
For broader language background, the Go project maintains official learning resources and a book list. Those resources are general Go learning material rather than a substitute for checking the current package documentation for the APIs used in a particular program.
Frequently Asked Questions
Can I share one http.Client among goroutines?
Yes. The net/http documentation states that clients and transports are safe for concurrent use; reuse the client rather than creating one per request.
Recommended Free Tools
Does SetLimit restrict requests per second?
No. It caps active group goroutines. A time-based rate limiter is a separate control.
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.




