Skip to main content
Have a list of URLs? Submit them to Batch as one job. Zenrows runs them in parallel with Adaptive Stealth Mode, tracks every task, and stores each page for you to download. You get no concurrency to tune, no retry loop to write, and nothing to keep running while it works. For a handful of URLs, a concurrent loop over Fetch is just as good, and the last section shows it. Uses: Batch (POST /jobs, GET /jobs/{id}/results) with zenrows_params, and Fetch for small lists

Ask your AI agent

With the Zenrows MCP server connected to Claude, Cursor or any MCP client, paste this prompt. Your agent picks the right Zenrows tools and settings on its own.
For a long list, point the agent at a file instead: “Use Zenrows to scrape every URL in urls.txt in one batch.” It runs the same way for 10 URLs or 100,000.

Submit the list as one Batch job

Send every URL in tasks, with the scrape settings for all of them in zenrows_params. Give each task an external_id: Zenrows returns it on every result, so you can match results to your own records. Then wait for the run to finish and download each result from its result_url.
The Node.js tab uses the Zenrows SDK, which submits, waits and pages through results for you; the Python tab shows the same flow over the raw REST API. Every task runs with Adaptive Stealth Mode (mode=auto), which picks the cheapest configuration that works for each page. The CLI and MCP send mode=auto by default. Any Fetch parameter fits in zenrows_params, and a task’s own zenrows_params override the job’s.

Example output

The Python run:
One row of GET /jobs/{job_id}/results, with the presigned link shortened:
The CLI run:

Use the Python SDK

ZenRowsBatchClient submits the job, waits for it and downloads every result in a few lines:
Python (SDK)
Output
Pass dictionaries such as {"url": ..., "external_id": "abominable-hoodie"} to set per-task IDs, and use_external_id=True on download_to_dir to name files by them. More in the Python and Node.js SDK guides.

Small lists: send requests concurrently

For a few dozen URLs that you want back in seconds, call Fetch directly and let the SDK run requests in parallel. Set concurrency to your plan’s concurrent request count, which every response reports in the Concurrency-Limit header.
Output
Both SDKs queue the requests and keep concurrency of them in flight. Move to Batch once the list grows, or when you’d rather not keep a process running while it works. More in Run many requests concurrently.

Customize it

Troubleshooting

  • Some tasks failed: read them with status=failed on the results endpoint; each row has an error with a code. Rerun just those with POST /jobs/{job_id}/rerun?status=failed, retry_failed() in Python, or zenrows batch retry-failed <id>. Successful results carry over. See Retry the tasks that failed.
  • A result_url returns 403: the presigned link is stale. List the results again for a fresh link.
  • The run ends failed with a failure_reason: the account or key needs credits or a raised credit cap. Fix it, then rerun the tasks that didn’t complete.
  • 429 when submitting: your account’s Crawl and Batch jobs are all running. Wait for one to finish, then submit.
  • Concurrent requests return 429: lower concurrency to the Concurrency-Limit header value, or move the list to Batch.

Get notified when a Batch job finishes

Receive a signed webhook instead of polling.

Run a Batch job on a schedule

Scrape the same list hourly, daily or weekly.

Map every URL on a website

Build the URL list with Crawl first.

Retry failed requests

Handle errors when you call Fetch directly.