timeout
The browser opened your URL, but the page did not reach its wait condition (wait.until) before wait.timeout ran out. The default is 30 seconds. The URL is fine and the server answered; the page was too slow, or it never became "finished" by the rule you chose. This page is part of the error code reference.
| HTTP status | 422 Unprocessable Entity |
error.type |
target_error |
error.code |
timeout. Today the API sends this failure as screenshot_failed with the browser's timeout message, so match on both |
error.retryable |
true |
| Credits used | 0 |
Example response
{ "error": { "type": "target_error", "code": "screenshot_failed", "message": "Navigation timeout of 30000 ms exceeded", "retryable": true }, "request_id": "req_6f0d2c9a41b7e853" }
The number in the message is your wait.timeout in milliseconds. If it says 30000 and you meant to send 45, the API did not receive your setting. Check that it is inside the wait object (POST) or sent as timeout (GET).
Three timeouts, three different errors
A slow page can fail at three layers. Each one looks different, and each one needs a different fix.
| Layer | What you get | Fix |
|---|---|---|
| The page, inside the browser | 422 with a timeout message, as above | Change wait.until or raise wait.timeout |
| The API waiting for the renderer | 500 server_error, "Failed to capture screenshot", after about 60 seconds |
Keep wait.timeout + wait.delay under 60 seconds |
| Your HTTP client | No response. The Node, Python and Ruby SDKs raise their own timeout error with HTTP status 408 | Set the client timeout above wait.timeout |
The third row is the common surprise. The Node, Python and Ruby SDK clients default to a 30 second request timeout, and the API's wait.timeout also defaults to 30 seconds. A page that needs 29 seconds plus capture time succeeds on the server, but your client has already given up. If you retry with cache.available: true, the API can return that finished render from cache without a new credit (see caching).
Common causes
networkidleon a page that never goes quiet. Analytics beacons, chat widgets, websockets and polling keep the network busy, so the browser waits until the timeout. This is the most frequent cause.- A slow origin. The first byte takes many seconds, often on cold serverless functions or overloaded staging servers.
- Heavy pages. Large images, video and many third-party scripts all count toward
load. - A challenge page. Bot protection that spins forever never fires
load. See bot protection and captchas.
How to fix it
- Read
error.message. A timeout message means the browser reached the server; anet::ERR_message is a navigation failure instead. - If you use
networkidle, switch toload, or todomcontentloadedplus a shortwait.delay. The wait strategies guide has a decision table. - Raise
wait.timeout(seconds, maximum 60) only when the page is slow but does finish. - Set your HTTP client timeout above
wait.timeout. - Retry once. A second identical attempt can succeed when the first one met a cold origin. Do not loop: a page that never finishes fails the same way every time.
The request with both settings:
curl -X POST https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com", "wait": {"until": "load", "timeout": 45}}' \ --max-time 75 -o shot.png
Retry with a lighter wait condition (Node.js)
This script tries networkidle first, because it gives the most complete page. On a page timeout it retries once on the load event with more time. Every other error stops the script with a non-zero exit code. It uses fetch, so it can send the full wait object.
// capture-slow-page.mjs (Node 22+, no dependencies) import { writeFile } from 'node:fs/promises'; const API = process.env.RS_BASE_URL ?? 'https://api.renderscreenshot.com'; async function capture(url, wait) { const res = await fetch(`${API}/v1/screenshot`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.RS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ url, wait }), // Wait longer than wait.timeout, so the API can answer before this client gives up. signal: AbortSignal.timeout(75_000), }); if (res.ok) return Buffer.from(await res.arrayBuffer()); const body = await res.json().catch(() => ({})); const err = new Error(`${res.status} ${body.error?.code}: ${body.error?.message} (${body.request_id})`); err.pageTimedOut = res.status === 422 && /timeout/i.test(body.error?.message ?? ''); throw err; } const url = process.argv[2] ?? 'https://example.com'; let image; try { image = await capture(url, { until: 'networkidle', timeout: 30 }); } catch (err) { if (!err.pageTimedOut) throw err; // The network never went quiet. Retry once on the load event, with more time. console.warn(`Timed out, retrying on the load event: ${err.message}`); image = await capture(url, { until: 'load', timeout: 45 }); } await writeFile('shot.png', image); console.log(`Saved shot.png (${image.length} bytes)`);
With the Ruby SDK
The Ruby SDK raises RenderScreenshot::TimeoutError when its own HTTP timeout fires, and a RenderScreenshot::Error with http_status 422 when the page timed out on the server. The two need different handling:
require 'renderscreenshot' # The client timeout is in seconds. Keep it above the API's own limit. client = RenderScreenshot.client(ENV.fetch('RS_API_KEY'), timeout: 75) options = RenderScreenshot::TakeOptions.url('https://example.com').wait_for('networkidle') attempts = 0 begin attempts += 1 image = client.take(options) rescue RenderScreenshot::TimeoutError # This client stopped waiting. The render can still finish on the server. raise if attempts >= 2 retry rescue RenderScreenshot::Error => e page_timed_out = e.http_status == 422 && e.message.match?(/timeout/i) raise unless page_timed_out && attempts < 2 warn "Timed out (#{e.message}), retrying on the load event" options = options.wait_for('load') retry end File.binwrite('shot.png', image) puts "Saved shot.png (#{image.bytesize} bytes)"
Tested on 2026-10-15 with Node 22.18 and with Ruby 4.0.7 plus the renderscreenshot gem 1.0.0, against a local stub that returns the error body above for networkidle and an image for load. Both scripts made exactly two requests and saved the image. A second stub that answered after 3 seconds, with the Ruby client timeout lowered to 1 second, confirmed that the TimeoutError branch retries once.
Related
- Wait parameters:
until,delayandtimeoutwith their limits. - Rendering performance: make slow pages faster to capture.
- Error handling and retries: one retry policy for every error code.
Last verified 2026-10-15 against the API source.