Apify Screenshot Actor Timing Out on Slow Websites: How to Fix It
Separate page, Actor-run, and caller wait timeouts, then tune the right setting for slow Apify screenshot runs.
When an Apify screenshot run times out, first identify which clock expired: the browser’s page-navigation timeout, the overall Actor run timeout, or the time your API/client waited for a response. They have different fixes. For the Screenshot Taker Actor by Jan Čurn, the page timeout is pageLoadTimeoutSecs (1–180 seconds, default 60), retries are controlled by pageMaxRetryCount (0–10, default 2), and the post-load delay is delaySecs (0–120 seconds, default 0). These names and limits are specific to that Actor. Confirm your Actor ID and its current input schema before using them.
1. Identify which timeout occurred
There are three separate limits to check:
| Limit | What expires | Typical evidence | What to change |
|---|---|---|---|
| Page navigation | The browser has not reached the configured navigation completion condition in time. | The page is logged as failed or retried for a page-load error or timeout. | The Actor’s per-page navigation timeout, wait condition, or retry count. |
| Actor run | The entire Actor run reaches its platform timeout. | The run status is TIMED-OUT. |
The run timeout in the Actor/task configuration or API call. |
| Caller wait | The HTTP request or SDK call stops waiting for the run. | Your request errors, but the Apify run may still be running or may finish later. | Use asynchronous execution and retrieve the run/result separately; adjust client connection limits if appropriate. |
- Open the run in Apify and inspect its status, input, and logs. Record the exact Actor ID.
- If the run is still active after your request failed, investigate the caller’s wait timeout before changing the browser timeout.
- If the run ended as
TIMED-OUT, inspect the Actor run timeout. - If the logs identify a page-load timeout, tune that Actor’s page navigation settings.
- If navigation succeeded but the screenshot missed late content, tune the readiness condition or post-load wait instead.
2. Tune Screenshot Taker’s page navigation settings
The following settings and values apply to jancurn/screenshot-taker. Other Apify screenshot Actors may use different input fields.
| Input | Documented values | Use it for |
|---|---|---|
pageLoadTimeoutSecs |
1–180; default 60 | How long navigation may take before the page is considered failed. |
pageMaxRetryCount |
0–10; default 2 | Retrying a failed or timed-out page. Retries can help intermittent faults, but increase total run duration. |
waitUntil |
load (default), domcontentloaded, networkidle0, networkidle2 |
The browser navigation condition that counts as successful. |
delaySecs |
0–120; default 0 | An additional wait after navigation succeeds and before screenshot capture. |
Raise pageLoadTimeoutSecs when a genuinely slow navigation needs more time. Keep it within the Actor’s 180-second maximum. Raising it does not fix a page that never completes navigation, a blocked request, or a restrictive overall run timeout.
waitUntil and delaySecs address different stages. The navigation condition must be met first; then delaySecs gives late rendering a little more time. A page with continuous background requests may not reach a network-idle condition promptly. In that case, try another documented condition and add a bounded post-load delay only if the screenshot needs time for client-side content to appear. No one condition is a universal fix.
Example input JSON
{
"urls": ["https://example.com"],
"pageLoadTimeoutSecs": 120,
"pageMaxRetryCount": 2,
"waitUntil": "domcontentloaded",
"delaySecs": 5
}
This is an example for Screenshot Taker, not a universal Apify screenshot input. The Actor also documents viewport width and height (defaults 1200×900; each range 1–10000), image type (jpeg default or png), and proxy configuration. Change viewport or image type only when relevant to the capture; they do not increase the navigation timeout.
3. Run it through Apify’s API
For a task based on Screenshot Taker, send the JSON as the request body to the task’s run endpoint. Replace the task ID and API token. The task must be configured to run the Screenshot Taker Actor.
cURL
curl -X POST \
"https://api.apify.com/v2/actor-tasks/YOUR_TASK_ID/runs?token=YOUR_APIFY_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"urls": ["https://example.com"],
"pageLoadTimeoutSecs": 120,
"pageMaxRetryCount": 2,
"waitUntil": "domcontentloaded",
"delaySecs": 5
}'
This starts an asynchronous run and returns a run object. Store its ID, poll or retrieve the run status, then fetch the Actor’s output using the run’s output references. This avoids keeping a single HTTP connection open for the whole capture.
Python
import time
import requests
API_TOKEN = "YOUR_APIFY_TOKEN"
TASK_ID = "YOUR_TASK_ID"
base = "https://api.apify.com/v2"
payload = {
"urls": ["https://example.com"],
"pageLoadTimeoutSecs": 120,
"pageMaxRetryCount": 2,
"waitUntil": "domcontentloaded",
"delaySecs": 5,
}
response = requests.post(
f"{base}/actor-tasks/{TASK_ID}/runs",
params={"token": API_TOKEN},
json=payload,
timeout=30,
)
response.raise_for_status()
run = response.json().get("data", response.json())
run_id = run["id"]
while True:
status_response = requests.get(
f"{base}/actor-runs/{run_id}",
params={"token": API_TOKEN},
timeout=30,
)
status_response.raise_for_status()
run = status_response.json()["data"]
if run["status"] in {"SUCCEEDED", "FAILED", "TIMED-OUT", "ABORTED"}:
print("Run status:", run["status"])
print("Default dataset ID:", run.get("defaultDatasetId"))
break
time.sleep(5)
if run.get("status") == "SUCCEEDED" and run.get("defaultDatasetId"):
items = requests.get(
f"{base}/datasets/{run['defaultDatasetId']}/items",
params={"token": API_TOKEN},
timeout=60,
)
items.raise_for_status()
print(items.json())
The polling loop’s request timeout applies to each status request, not to the Actor run. Add a maximum polling deadline and application-specific handling if this runs unattended.
Node.js
const API_TOKEN = 'YOUR_APIFY_TOKEN';
const TASK_ID = 'YOUR_TASK_ID';
const base = 'https://api.apify.com/v2';
const started = await fetch(
`${base}/actor-tasks/${TASK_ID}/runs?token=${encodeURIComponent(API_TOKEN)}`,
{
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
urls: ['https://example.com'],
pageLoadTimeoutSecs: 120,
pageMaxRetryCount: 2,
waitUntil: 'domcontentloaded',
delaySecs: 5,
}),
},
);
if (!started.ok) throw new Error(`Start failed: ${started.status} ${await started.text()}`);
const startedBody = await started.json();
const runId = (startedBody.data ?? startedBody).id;
let run;
for (;;) {
const response = await fetch(
`${base}/actor-runs/${runId}?token=${encodeURIComponent(API_TOKEN)}`,
);
if (!response.ok) throw new Error(`Status check failed: ${response.status}`);
run = (await response.json()).data;
if (['SUCCEEDED', 'FAILED', 'TIMED-OUT', 'ABORTED'].includes(run.status)) break;
await new Promise(resolve => setTimeout(resolve, 5000));
}
console.log('Run status:', run.status);
if (run.status === 'SUCCEEDED' && run.defaultDatasetId) {
const result = await fetch(
`${base}/datasets/${run.defaultDatasetId}/items?token=${encodeURIComponent(API_TOKEN)}`,
);
if (!result.ok) throw new Error(`Result fetch failed: ${result.status}`);
console.log(await result.json());
}
Keep API tokens out of source control and logs. These examples show the asynchronous task flow; if you call an Actor directly rather than through a task, use the corresponding Actor run endpoint and the same principle: start, inspect the returned run ID, and fetch output after completion.
4. Distinguish run timeout from API wait timeout
The Actor run timeout is a platform limit configured in seconds. It may differ by Actor template, and run options or API calls can override it. A value of zero means no timeout for the Actor run setting. Check the effective run configuration before increasing it.
Apify’s synchronous task endpoint waits at most 300 seconds for completion before the HTTP request fails; Apify documents that this request timeout does not abort the run itself. Network conditions and your HTTP client’s own connection timeout can also end the wait earlier. A disconnected caller may not receive the run ID or status, so for work that could take a while, start an asynchronous run and save the returned run ID.
5. Check browser resources and workload
Browser workloads use substantial resources, and heavier pages take longer to load. Apify’s resource documentation describes 4096 MB as a middle ground for many workloads, while noting that a single-threaded Node.js Actor generally does not gain more CPU from allocations above 4096 MB unless it uses multithreaded components. This is context, not a universal memory recommendation.
- Check the run’s memory and CPU usage and whether it stopped for a resource limit.
- Try the same URL alone to see whether batch size or concurrency contributes to resource pressure.
- Compare a light page and the failing target using the same input.
- Inspect page weight, redirects, slow third-party resources, and whether the target is accessible from the Actor’s network location.
- Change memory only when run data suggests a resource bottleneck; more memory is not a substitute for the right timeout setting.
6. Troubleshooting common symptoms
| Symptom | Likely cause | Fix |
|---|---|---|
| Page is retried and ultimately reported as a load failure | Navigation did not meet waitUntil within the page timeout, or a network/navigation error occurred. |
Confirm the Actor schema. Raise its page timeout within the supported range; test another documented readiness condition; check URL and network access. |
Run status is TIMED-OUT |
The overall run limit expired, possibly after slow pages or retries. | Inspect run logs and duration. Adjust the run timeout in the run configuration/API where appropriate, and review workload and concurrency. |
| HTTP request timed out, but run keeps going | The synchronous caller wait limit or client connection timeout expired. | Use the asynchronous run endpoint, retain the run ID, and poll/retrieve the result separately. |
| Navigation succeeds, screenshot is blank or misses content | Content rendered after the navigation condition, or depended on scrolling/interactions. | Use a suitable documented waitUntil value or a bounded delaySecs. Confirm the Actor supports any needed scrolling or interaction; do not assume fields from another Actor. |
| Adding retries makes the job take much longer | Each failed attempt consumes time, and the configured retry count permits repeated attempts. | Keep retries for intermittent failures; lower pageMaxRetryCount for consistently inaccessible pages and correct the underlying cause. |
| Changing Screenshot Taker fields is rejected or has no effect | You may be running a different Actor or a task whose input schema/defaults differ. | Check the task’s linked Actor ID and the current Actor input schema. Use only fields that schema exposes. |
| Higher memory does not shorten the run | The bottleneck may be page/network wait or single-threaded execution rather than memory. | Use run usage and logs to identify the bottleneck before allocating more resources. |
7. Actor identity matters
“Apify Screenshot Actor” does not identify one input schema. The timeout names above are documented for jancurn/screenshot-taker. Apify’s separate Website Screenshot Generator repository describes a simple example Actor with configurable delay and optional scrolling, and recommends Website Content Crawler for more advanced crawling options. Do not copy pageLoadTimeoutSecs into another Actor’s input unless that Actor’s schema documents it.
Or skip the browser setup
ScreenshotNeo provides a screenshot API and MCP server. One GET request returns an image or PDF, without you managing a browser Actor:
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo API documentation for parameters and response handling. Cookie banners, popups, and chat widgets are removed before capture; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
FAQ
Does a page timeout cancel the whole Apify run?
Not necessarily. In Screenshot Taker, a page-load timeout is treated as a page failure and retried according to its retry setting. The overall Actor run has its own limit.
Should I always use networkidle0 for slow pages?
No. It is one documented choice, but pages with persistent background traffic may not reach that condition promptly. Choose based on when the page is useful to capture.
Will increasing the timeout fix a CAPTCHA or access-denied page?
No. More waiting does not resolve an access check or a page that cannot be reached. Inspect the rendered result and logs to distinguish access problems from slow navigation.
Can I tell why a specific run timed out from this guide alone?
No. The exact Actor ID, run logs, input JSON, and target URL are needed to identify a case-specific failure.


