> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zenrows.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Concurrency with the Go SDK

> Scrape multiple URLs at once with the Go SDK's built-in concurrency limiter.

Go doesn't need a separate async API the way Python or JavaScript do, every SDK call is a regular blocking function you run inside a goroutine. The SDK adds one thing on top of that: an optional internal semaphore that caps how many requests are in flight at once, regardless of how many goroutines you launch.

## Basic usage

<Steps>
  <Step title="Set a concurrency limit">
    Pass `WithMaxConcurrentRequests` when creating the client to cap how many requests run at the same time:

    ```go theme={null} theme={null}
    client := scraperapi.NewClient(
        scraperapi.WithAPIKey("YOUR_ZENROWS_API_KEY"),
        scraperapi.WithMaxConcurrentRequests(5),
    )
    ```

    <Note>
      Set `WithMaxConcurrentRequests` to match your ZenRows plan's concurrent request limit. The default is 5. Each client instance enforces its own limit. Two scripts (or two client instances) don't share it, so running both at once can still trigger `429 Too Many Requests` errors. See [Concurrency](/universal-scraper-api/features/concurrency) for how plan limits work.
    </Note>
  </Step>

  <Step title="Scrape a list of URLs concurrently">
    Launch a goroutine per URL and use a `sync.WaitGroup` to track completion:

    ```go theme={null} theme={null}
    urls := []string{
        "https://www.scrapingcourse.com/ecommerce/",
        "https://www.scrapingcourse.com/ecommerce/page/2/",
        "https://www.scrapingcourse.com/ecommerce/page/3/",
    }

    var wg sync.WaitGroup
    for i, url := range urls {
        wg.Add(1)
        go func(i int, url string) {
            defer wg.Done()

            response, err := client.Get(context.Background(), url, nil)
            if err != nil {
                fmt.Println(i, err)
                return
            }

            fmt.Printf("[#%d]: %s\n", i, response.Status())
        }(i, url)
    }
    ```

    Each goroutine that acquires a slot from the internal semaphore runs its request, then releases the slot, so no more than `WithMaxConcurrentRequests` requests from this client are in flight at once, no matter how many goroutines you've launched.
  </Step>

  <Step title="Wait for everything to finish">
    ```go theme={null} theme={null}
    wg.Wait()
    fmt.Println("done")
    ```
  </Step>
</Steps>

## When to think about concurrency

You don't need to do anything special to run requests concurrently. Launching goroutines already benefits from the internal semaphore. The default limit is 5. Set `WithMaxConcurrentRequests` explicitly when you want to match a higher plan limit, or lower it if you're seeing `429` errors.

## POST and PUT in parallel

The same pattern works with `client.Post()` and `client.Put()`:

<Tabs>
  <Tab title="POST in parallel">
    ```go theme={null} theme={null}
    go func(i int, url string) {
        defer wg.Done()

        response, err := client.Post(context.Background(), url, nil, map[string]string{"key": "value"})
        if err != nil {
            fmt.Println(i, err)
            return
        }

        fmt.Printf("[#%d]: %s\n", i, response.Status())
    }(i, url)
    ```
  </Tab>

  <Tab title="PUT in parallel">
    ```go theme={null} theme={null}
    go func(i int, url string) {
        defer wg.Done()

        response, err := client.Put(context.Background(), url, nil, map[string]string{"key": "value"})
        if err != nil {
            fmt.Println(i, err)
            return
        }

        fmt.Printf("[#%d]: %s\n", i, response.Status())
    }(i, url)
    ```
  </Tab>
</Tabs>

## How it works under the hood

<Info>
  `WithMaxConcurrentRequests(n)` creates a buffered channel of size `n` inside the client. Every call to `Get`, `Post`, `Put`, or `Scrape` acquires a slot from that channel before sending the request, and releases it once the response comes back. This applies uniformly across methods, there's no separate limit for GET versus POST/PUT. Retry behavior configured via `WithMaxRetryCount` applies the same way regardless of how many requests run in parallel. See [Error Handling and Retries](/universal-scraper-api/sdk/go/error-handling-and-retries) for details.
</Info>

<Warning>
  The SDK's default concurrency is 5. If your plan allows more concurrent requests and you don't set `WithMaxConcurrentRequests` higher, you may be leaving throughput on the table. If you launch more goroutines than your plan allows, you'll get `429 Too Many Requests` errors.
</Warning>

## Troubleshooting

* **`429 Too Many Requests` even with a `WithMaxConcurrentRequests` value that matches your plan**: Another script or process is using the same API key at the same time. Concurrency limits are per plan, not per client instance, so simultaneous scripts share the same real ceiling even though each `*scraperapi.Client` only tracks its own local limit.
* **Goroutines seem to run one at a time**: Confirm you're not accidentally blocking inside the goroutine before calling `wg.Done()`, and that you're not reusing a single `context.Context` with an already-expired deadline.
* **Program exits before all goroutines finish**: Make sure `wg.Wait()` runs before `main()` returns; a goroutine that hasn't called `Done()` yet is silently dropped when the program exits.
* **Cancelling a request doesn't free a concurrency slot right away**: ZenRows keeps the slot occupied for up to a few minutes after a client-side cancellation while it finishes processing server-side.

## Pricing

Requests are billed identically whether they run one at a time or concurrently. Cost depends on the parameters you send per request, not on how many run in parallel:

| Configuration                                | Cost multiplier vs. basic |
| -------------------------------------------- | ------------------------- |
| Basic request                                | 1x                        |
| `JSRender: true`                             | 5x                        |
| `UsePremiumProxies: true`                    | 10x                       |
| `JSRender: true` + `UsePremiumProxies: true` | 25x                       |

Running requests concurrently doesn't add cost by itself. It only changes how fast the same requests complete. See [ZenRows Pricing](/first-steps/pricing).

## FAQ (Frequently Asked Questions)

<AccordionGroup>
  <Accordion title="What's the default concurrency limit if I don't set one?">
    The default is 5. Even without passing `WithMaxConcurrentRequests` explicitly, the SDK creates an internal semaphore capped at 5. Set it explicitly if your plan allows more, or lower it if you're seeing `429 Too Many Requests` errors.
  </Accordion>

  <Accordion title="Do GET, POST, and PUT share the same concurrency limit?">
    Yes. Every call from the same `*scraperapi.Client`, regardless of method, goes through one shared internal semaphore.
  </Accordion>

  <Accordion title="Do I need a worker pool library to do this?">
    No. A `sync.WaitGroup` plus goroutines, as shown above, is enough. Reach for a worker pool library only if you need more advanced scheduling, like rate limiting independent of the SDK's own semaphore.
  </Accordion>
</AccordionGroup>
