Wait strategies
A screenshot is only as good as the moment it is taken. Too early and you capture a spinner, a fallback font or an empty chart. Too late and every request pays for seconds of idle waiting. This guide shows how to pick the wait parameters for a page, using a decision table and timings from a test page. It is one of the capture guides.
The four wait controls
All four live in the wait object of a POST request. They run in this order: the browser navigates and waits for until, then for for_selector if you set one, then sleeps for delay. timeout limits the whole wait.
| Parameter | Type | Default | Limits | What it waits for |
|---|---|---|---|---|
wait.until |
string | load |
load, domcontentloaded, networkidle |
A browser lifecycle event |
wait.for_selector |
string | none | Any CSS selector | An element in a given state |
wait.for_selector_mode |
string | visible |
visible, hidden, attached, detached |
The state for_selector must reach |
wait.delay |
integer (ms) | 0 |
Capped at 10,000 | A fixed pause after the conditions above |
wait.timeout |
integer (s) | 30 |
Maximum 60 | The limit for the whole wait |
networkidle means that the page made no network requests for 500 ms. That quiet window is the minimum extra time it adds.
With GET requests you can set only delay and timeout as flat query parameters. Use POST for until and for_selector.
Decision table
| Your page | Use | Why |
|---|---|---|
| Static HTML, server-rendered blog or docs page | until: load (the default) |
load waits for images, stylesheets and fonts that the page requests during the first load. |
| You only need the text and layout, images do not matter | until: domcontentloaded |
Fastest condition. Images and late fonts can be missing. |
| A single-page app you control | for_selector on a ready flag |
Precise. The capture happens as soon as your data is on screen. |
| A single-page app you do not control | until: networkidle, plus for_selector on a content element if you know one |
Catches data requests that finish after load. |
| A page with a loading spinner | for_selector: ".spinner", for_selector_mode: hidden |
Waits for the spinner to go away instead of guessing a time. |
| Entry animations, count-up numbers, carousels | Any of the above plus delay: 300 to 1000 |
Animations do not make network requests, so no condition detects their end. |
| Analytics that poll every few seconds, live chat, websockets | Avoid networkidle. Use for_selector |
The network never goes quiet, so networkidle runs until timeout. |
What each condition guarantees: a measured test
To show the difference, we built a local test page with three late parts. The stylesheet arrives after 300 ms. The web font it declares arrives 800 ms later. A script fetches data 100 ms after start, and the server answers after 1,200 ms. Then we loaded the page in headless Chrome with each condition and recorded what was ready.
| Condition | Time to capture (median of 3) | Web font | Data on screen |
|---|---|---|---|
domcontentloaded |
346 ms | Still loading | No |
load |
1,115 ms | Loaded | No |
networkidle |
3,001 ms | Loaded | Yes |
load + selector on a ready flag |
1,620 ms | Loaded | Yes |
Tested on 2026-10-15 with Node 22.18, Puppeteer 24.10.0 and Chrome 137 on macOS, against a local test server, 3 runs. The script is plain Puppeteer, not the RenderScreenshot API, so the absolute times are not API latency. The order is the useful part:
domcontentloadedwas fast but captured the fallback font.loadwaited for the font but not for data fetched by JavaScript.networkidlegot everything but took the longest, because it must see 500 ms of silence after the last request.- A selector on a ready flag got everything in about half the
networkidletime.
The ready-flag pattern
If you own the page, add one attribute when the content is complete, and wait for it. This is the most reliable strategy and usually the fastest.
// In your app, after the data has rendered and fonts are ready async function markReady() { await document.fonts.ready; document.documentElement.dataset.screenshotReady = 'true'; } fetch('/api/report') .then((r) => r.json()) .then((data) => { renderReport(data); // Let the browser paint the new DOM before you raise the flag requestAnimationFrame(() => markReady()); });
Then ask the API to wait for that attribute:
{ "url": "https://app.example.com/reports/42", "wait": { "until": "load", "for_selector": "html[data-screenshot-ready=\"true\"]", "timeout": 45 } }
The flag also helps with your own tests, because a person debugging a capture can see the same attribute in DevTools.
Minimal requests
Both examples expect your API key in RS_API_KEY; sign up to get one.
GET, with a fixed delay and a longer timeout:
curl -G "https://api.renderscreenshot.com/v1/screenshot" \ -H "Authorization: Bearer $RS_API_KEY" \ --data-urlencode "url=https://example.com" \ -d delay=500 -d timeout=45 \ -o page.png
POST, with a network condition and a selector:
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": "networkidle", "for_selector": "main h1", "for_selector_mode": "visible", "timeout": 45 } }' \ -o page.png
SDK examples
The SDKs take the wait timeout in milliseconds (waitForTimeout(45000)), while the raw API takes seconds (timeout: 45). Keep that in mind when you move code between the two.
Node.js
import { writeFile } from 'node:fs/promises'; import { Client, TakeOptions } from 'renderscreenshot'; const client = new Client(process.env.RS_API_KEY, { timeout: 60000 }); const image = await client.take( TakeOptions.url('https://app.example.com/reports/42') .waitFor('load') .waitForSelector('html[data-screenshot-ready="true"]') .waitForTimeout(45000) ); await writeFile('report.png', image);
Python
import os from renderscreenshot import Client, TakeOptions client = Client(os.environ['RS_API_KEY'], timeout=60.0) image = client.take( TakeOptions.url('https://app.example.com/reports/42') .wait_for('load') .wait_for_selector('html[data-screenshot-ready="true"]') .wait_for_timeout(45000) ) with open('report.png', 'wb') as f: f.write(image)
Ruby
require 'renderscreenshot' client = RenderScreenshot.client(ENV.fetch('RS_API_KEY'), timeout: 60) image = client.take( RenderScreenshot::TakeOptions .url('https://app.example.com/reports/42') .wait_for('load') .wait_for_selector('html[data-screenshot-ready="true"]') .wait_for_timeout(45_000) ) File.binwrite('report.png', image)
PHP
use RenderScreenshot\Client; use RenderScreenshot\TakeOptions; $client = new Client(getenv('RS_API_KEY'), null, 60.0); $image = $client->take( TakeOptions::url('https://app.example.com/reports/42') ->waitFor('load') ->waitForSelector('html[data-screenshot-ready="true"]') ->waitForTimeout(45000) ); file_put_contents('report.png', $image);
Java
Client client = new Client(System.getenv("RS_API_KEY"), "https://api.renderscreenshot.com", Duration.ofSeconds(60)); byte[] image = client.take( TakeOptions.url("https://app.example.com/reports/42") .waitFor("load") .waitForSelector("html[data-screenshot-ready=\"true\"]") .waitForTimeout(45000) ); Files.write(Path.of("report.png"), image);
Go
client, err := rs.New(os.Getenv("RS_API_KEY"), rs.WithTimeout(60*time.Second)) if err != nil { log.Fatal(err) } image, err := client.Take(ctx, rs.URL("https://app.example.com/reports/42"). WaitFor(rs.WaitLoad). WaitForSelector(`html[data-screenshot-ready="true"]`). WaitForTimeout(45000), ) if err != nil { log.Fatal(err) } os.WriteFile("report.png", image, 0o644)
Each example raises the client timeout to 60 seconds. The SDKs default to 30 seconds, which is the same as the default render timeout. If you allow the render 45 seconds but the HTTP client gives up at 30, you see a client timeout for a capture that would have worked.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Spinner or skeleton in the image | Data loads after load |
Wait for a content element or a ready flag with for_selector |
| Text in a fallback font | Captured before the web font arrived (domcontentloaded) |
Use load or later; in your own page, set the ready flag after document.fonts.ready |
| Request takes about 30 s, then fails | networkidle never reached because of polling or a websocket, or the selector never matches |
Replace networkidle with for_selector; check the selector in DevTools on the live page |
| Chart or counter half drawn | Entry animation still running | Add delay of 300 to 1,000 ms, or disable animations with browser.reduced_motion if the site respects it |
| Selector matches too early | The element exists in the HTML before it has data (for example an empty container) | Use for_selector_mode: visible on a child that only exists with data, or a ready flag |
| Modal or banner covers the content | It appears after the content is ready | Remove it with page.hide or page.remove, see customize the page before capture |
| Lazy images below the fold are empty | They load only on scroll | Use full-page scroll simulation, see full-page screenshots |
When a wait condition is not met before timeout, the request fails with a target_error instead of returning a partial image. That error is safe to retry once with a looser condition. The error handling guide shows the retry code.
Related parameters
waitfor the full parameter reference.- Capture modes, because
selectorfor an element screenshot is a different parameter fromwait.for_selector. - Rendering performance, for what each wait condition costs in time.
- Screenshots of single-page applications, for framework-specific ready signals.