Skip to main content
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

1

Set a concurrency limit

Pass WithMaxConcurrentRequests when creating the client to cap how many requests run at the same time:
client := scraperapi.NewClient(
    scraperapi.WithAPIKey("YOUR_ZENROWS_API_KEY"),
    scraperapi.WithMaxConcurrentRequests(5),
)
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 for how plan limits work.
2

Scrape a list of URLs concurrently

Launch a goroutine per URL and use a sync.WaitGroup to track completion:
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.
3

Wait for everything to finish

wg.Wait()
fmt.Println("done")

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():
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)

How it works under the hood

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 for details.
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.

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:
ConfigurationCost multiplier vs. basic
Basic request1x
JSRender: true5x
UsePremiumProxies: true10x
JSRender: true + UsePremiumProxies: true25x
Running requests concurrently doesn’t add cost by itself. It only changes how fast the same requests complete. See ZenRows Pricing.

FAQ (Frequently Asked Questions)

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.
Yes. Every call from the same *scraperapi.Client, regardless of method, goes through one shared internal semaphore.
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.