client.get() in a loop wastes time waiting for each request to finish before starting the next one. The SDK’s async methods let you fire off requests in parallel instead.
Basic usage
Set a concurrency limit
Pass
concurrency when creating the client to cap how many requests run at the same time:Set
concurrency to match your ZenRows plan’s concurrent request limit. 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.Scrape a list of URLs concurrently
Use
get_async with asyncio.gather to run requests in parallel and wait for all of them to finish:asyncio.gather waits for every call to finish, even if some of them fail. It won’t raise until all results (or exceptions) are collected. Each item in responses is a full requests.Response, so you can inspect status_code, text, and headers exactly as you would with client.get().When to use async and concurrency
Useget_async/post_async/put_async with asyncio.gather when scraping more than a handful of URLs and you want them processed in parallel instead of one at a time. For a single URL, client.get() (see Quickstart) is simpler and does the same thing without the asyncio boilerplate.
Async POST and PUT
post_async and put_async work the same way, accepting the same data, params, and headers arguments as their synchronous counterparts:
- POST async
- PUT async
How it works under the hood
The SDK runs each async call on a thread pool sized to your
concurrency value, then awaits the results, giving you the ergonomics of asyncio without needing an async HTTP client. This means the same retry behavior configured via retries applies equally to synchronous and async calls. See Error Handling and Retries for details.Troubleshooting
RuntimeWarning: coroutine 'main' was never awaited: You calledmain()directly instead ofasyncio.run(main()).429 Too Many Requestseven thoughconcurrencyis set correctly: 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 eachZenRowsClientonly tracks its own local limit.- Some responses in the list are much slower than others: This is expected; total throughput depends on the slowest requests in each concurrent batch. See the Concurrency guide for the underlying math.
- 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
Async requests are billed identically to synchronous ones. Cost depends on theparams you send per request, not on how many run in parallel:
| Configuration | Cost multiplier vs. basic |
|---|---|
| Basic request | 1x |
js_render=True | 5x |
premium_proxy=True | 10x |
js_render=True + premium_proxy=True | 25x |
FAQ (Frequently Asked Questions)
What's the default concurrency if I don't set one?
What's the default concurrency if I don't set one?
The default concurrency is 5. You can set it to any number you want according to your ZenRows plan.
Can I use requests' own concurrency tools instead?
Can I use requests' own concurrency tools instead?
Yes.
multiprocessing.pool.ThreadPool with plain requests calls works too. See the Concurrency guide for that approach if you’d rather not use the SDK’s async methods.