> ## 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.

> ## Agent Instructions
> The Zenrows API (Fetch, Extract via extract=auto, and Batch) is described in OpenAPI 3.1 at https://www.zenrows.com/openapi.json and listed in the RFC 9727 API catalog at https://www.zenrows.com/.well-known/api-catalog. Use the spec for exact parameter names and types.

# Retry the tasks that failed

> See which URLs in a Zenrows Batch job failed and why, then retry only those, as they are or with stronger settings.

When a Batch run ends with some failed tasks, you don't scrape the whole list again. Read the failures and their error codes, then either rerun just those tasks as they are or send them again with stronger settings. Successful results carry over, so you only pay for what you retry.

**Uses:** [Batch results](/batch/developer-guide-restapi#download-content) (`GET /jobs/{id}/results?status=failed`), [reruns](/batch/developer-guide-restapi#5-reruns-and-retrying-failures) (`POST /jobs/{id}/rerun?status=failed`) and [error codes](/api-error-codes)

## Ask your AI agent

Paste this prompt into a coding agent such as Claude Code or Cursor, with the [Zenrows docs connected](/academy/connect-your-ai-agent#give-your-agent-the-zenrows-docs). The agent looks up the details and writes the calls.

```text theme={"dark"}
Using the Zenrows docs, write a script that takes a Zenrows Batch job ID, lists the tasks
that failed with their error codes, skips pages that no longer exist, and retries the rest
with settings strong enough for protected sites. Print what succeeded on the retry.
It should work the same for a job with 50 failures or 50,000.
```

## See which tasks failed and why

Call `GET /jobs/{job_id}/results` with `status=failed`. Every row has an `error` object with a `code` you can look up in [API error codes](/api-error-codes) and the HTTP `status` of the failure. Page through with `cursor` until there is no `next_cursor`.

```python Python (Requests) theme={"dark"}
import requests

BASE = "https://async.api.zenrows.com/v1"
headers = {"X-API-Key": "YOUR_ZENROWS_API_KEY"}
job_id = "YOUR_JOB_ID"

# Page through the failed tasks of the latest run
failed, cursor = [], None
while True:
    page = requests.get(
        f"{BASE}/jobs/{job_id}/results",
        headers=headers,
        params={"status": "failed", "cursor": cursor},
        timeout=60,
    ).json()
    failed += page["results"]
    cursor = page.get("next_cursor")
    if not cursor:
        break

for row in failed:
    error = row["error"]
    print(row["external_id"], error["code"], error["status"], row["url"])
```

This job ran four URLs with plain requests (no `mode=auto`), so the protected page failed along with a product page that doesn't exist:

```text Output theme={"dark"}
discontinued-item RESP002 404 https://www.scrapingcourse.com/ecommerce/product/discontinued-item/
antibot-challenge REQS002 400 https://www.scrapingcourse.com/antibot-challenge
```

The run's `stats` also count failures by reason in `failure_reasons`, for example `{"bad_target": 2}`, so you see the shape of a large run before you read every row.

Decide per code:

| Code | Meaning | Retry with |
| - | - | - |
| `REQS002`, `RESP001` | The site needs rendering or premium proxies | Stronger settings, see [Retry with stronger settings](#retry-with-stronger-settings) |
| `REQS004` | The URL is invalid, or the settings conflict | Fix the URL or the settings, then submit again |
| `RESP002`, `RESP007` | The page or the domain doesn't exist | Drop it |
| Timeouts, `429`, `5xx` | Temporary | The same settings, see [Rerun the failures as they are](#rerun-the-failures-as-they-are) |

## Rerun the failures as they are

`POST /jobs/{job_id}/rerun?status=failed` starts a new run of the same job. Failed tasks run again with their original settings, and successful tasks carry over with their results. Use it for temporary failures. Run it once the previous run has finished.

<CodeGroup>
  ```python Python (SDK) theme={"dark"}
  # pip install zenrows
  from zenrows import ZenRowsBatchClient

  client = ZenRowsBatchClient(api_key="YOUR_ZENROWS_API_KEY")
  job = client.job("YOUR_JOB_ID")

  job.run.wait(timeout=900)  # the previous run must be finished

  run = job.retry_failed()  # new run: failed tasks re-run, successful ones carry over
  run = run.wait(timeout=900)
  print(run.status.value, f"{run.stats.successful}/{run.stats.total} successful")

  for row in run.results(status="failed"):
      print("still failing:", row.external_id, row.error.code)
  ```

  ```javascript Node.js (SDK) theme={"dark"}
  // npm install zenrows
  import { ZenRowsBatchClient } from "zenrows";

  const client = new ZenRowsBatchClient("YOUR_ZENROWS_API_KEY");
  const job = client.job("YOUR_JOB_ID");

  await job.run.wait(); // the previous run must be finished

  const retry = await job.retryFailed(); // failed tasks re-run, successful ones carry over
  const run = await retry.wait();
  console.log(run.status, `${run.stats.successful}/${run.stats.total} successful`);

  for await (const row of run.results({ status: "failed" })) {
    console.log("still failing:", row.external_id, row.error.code);
  }
  ```

  ```bash cURL theme={"dark"}
  curl -X POST "https://async.api.zenrows.com/v1/jobs/YOUR_JOB_ID/rerun?status=failed" \
    -H "X-API-Key: YOUR_ZENROWS_API_KEY"
  ```

  ```bash CLI theme={"dark"}
  zenrows batch retry-failed YOUR_JOB_ID
  zenrows batch wait YOUR_JOB_ID
  zenrows batch results YOUR_JOB_ID --status failed
  ```
</CodeGroup>

The cURL response reports how many tasks run again and how many carry over:

```json theme={"dark"}
{
  "job_id": "01M4YME6MHTGS74XFKQ1X9MMTR",
  "latest_run": {
    "run_id": "01M4YNN47ZBV58RKP61TNMQBPK",
    "run_sequence": 2,
    "status": "running",
    "stats": { "total": 4, "completed": 2, "successful": 2, "failed": 0 }
  },
  "rerun_of": "01M4YME6MHKZDFVF16E9WMQ71S",
  "retried_tasks": 2,
  "inherited_tasks": 2
}
```

The CLI run:

```text theme={"dark"}
✓ Reran failed tasks for job 01M4YME6MHTGS74XFKQ1X9MMTR · status: running
  2/4 completed · 2 successful · 0 failed
✓ Job 01M4YME6MHTGS74XFKQ1X9MMTR finished · status: completed
  4/4 completed · 2 successful · 2 failed
```

Here both tasks fail again, as their codes predicted: the `404` page is gone and the protected page needs stronger settings. Add `,pending` to the status (`include_pending=True` in Python, `includePending: true` in Node.js) to also run tasks that never started, for example after you stopped a run.

## Retry with stronger settings

A rerun keeps each task's settings. To retry with different ones, collect the failed URLs worth retrying and submit them as a new job with stronger `zenrows_params`. Adaptive Stealth Mode (`mode=auto`) is the usual choice: it escalates to JavaScript rendering and premium proxies only where a page needs them.

```python Python (Requests) theme={"dark"}
import time
import requests

BASE = "https://async.api.zenrows.com/v1"
headers = {"X-API-Key": "YOUR_ZENROWS_API_KEY"}
job_id = "YOUR_JOB_ID"

# Codes for pages that are gone or URLs that are wrong: skip these
SKIP = {"RESP002", "RESP007", "REQS004"}

# 1. Collect the failed tasks worth retrying
retry, cursor = [], None
while True:
    page = requests.get(f"{BASE}/jobs/{job_id}/results", headers=headers,
                        params={"status": "failed", "cursor": cursor}, timeout=60).json()
    for row in page["results"]:
        if row["error"]["code"] not in SKIP:
            retry.append({"url": row["url"], "external_id": row["external_id"]})
    cursor = page.get("next_cursor")
    if not cursor:
        break
print("Retrying", [t["external_id"] for t in retry])

# 2. Submit them as a new job with Adaptive Stealth Mode
new_job = requests.post(f"{BASE}/jobs", headers=headers, json={
    "type": "regular",
    "status": "closed",
    "zenrows_params": {"mode": "auto"},
    "tasks": retry,
}, timeout=60).json()
new_id = new_job["job_id"]

while True:
    run = requests.get(f"{BASE}/jobs/{new_id}", headers=headers, timeout=60).json()["latest_run"]
    if run["status"] in {"completed", "failed", "stopped"}:
        break
    time.sleep(5)
print(new_id, run["status"], f"{run['stats']['successful']}/{run['stats']['total']} successful")
```

## Example output

The stronger-settings script on the same job skips the `404` page and resubmits the protected one:

```text theme={"dark"}
Retrying ['antibot-challenge']
01M4YPWBTA64T4A5VZNJ4Q1W66 completed 1/1 successful
```

The protected page now succeeds. Keeping the `external_id` on each task lets you merge the retried results back with the first job's results.

## Customize it

| Change | How |
| - | - |
| Force specific settings on the retry | Use `js_render`, `premium_proxy` and `proxy_country` instead of `mode=auto` in `zenrows_params`, see [Scrape pages protected by anti-bot systems](/academy/fetch/protected-sites) |
| Stronger settings for some tasks only | Put `zenrows_params` on those tasks, see [Use different settings for each URL in one job](/academy/batch/per-task-settings) |
| Rerun every task, not only failures | `POST /jobs/{job_id}/rerun` with no `status`, `job.rerun()` in the SDKs |
| Retry automatically when a run ends | Trigger the retry from a `run.completed` webhook, see [Get notified when a Batch job finishes](/academy/batch/webhooks) |
| Save the results of the retry | See [Download all results of a job at once](/academy/batch/export-results) |

## Troubleshooting

* **The rerun returns `409` with `run_not_terminal`**: the previous run is still going. Wait for it to finish (`job.run.wait()` or `zenrows batch wait`), then rerun.
* **The rerun returns `409` with `no_matching_tasks`**: no task failed in the latest run. Read the results with `status=all` to confirm.
* **The same tasks fail again after a rerun**: their codes point to the page or the settings, not to a temporary problem. Drop `404` pages and [retry the rest with stronger settings](#retry-with-stronger-settings).
* **A retried task fails with `REQS004` about `mode=auto`**: the task sets `js_render` or `premium_proxy` while the job uses `mode=auto`. Keep one approach per task, see [Use different settings for each URL in one job](/academy/batch/per-task-settings).
* **The whole run ended `failed` with a `failure_reason`**: the account needs credits, or the key's [credit cap](/first-steps/api-key-credit-caps) needs raising. Add them, then rerun with `status=failed,pending`.

## Related recipes

<CardGroup cols={2}>
  <Card title="Scrape a list of URLs" icon="list" href="/academy/scrape-from-a-list-of-urls">
    Submit thousands of URLs as one Batch job.
  </Card>

  <Card title="Use different settings for each URL in one job" icon="adjustments" href="/academy/batch/per-task-settings">
    Override the job's settings for a single task.
  </Card>

  <Card title="Retry failed requests" icon="refresh" href="/academy/retry-failed-requests">
    Handle errors when you call Fetch directly.
  </Card>

  <Card title="Download all results of a job at once" icon="download" href="/academy/batch/export-results">
    Save every page to a folder or one ZIP file.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.