Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run Scan×
Skip to content
concurrency

Making Concurrent Requests in Go: Context, Errors, and Limits

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Fail fast: use errgroup.WithContext when 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

errgroup

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.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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: SetLimit blocks 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: Do does not turn non-2xx status codes into Go errors. Check StatusCode and 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) or errors.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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Does SetLimit restrict requests per second?

No. It caps active group goroutines. A time-based rate limiter is a separate control.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read next

Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.