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:

  • domcontentloaded was fast but captured the fallback font.
  • load waited for the font but not for data fetched by JavaScript.
  • networkidle got 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 networkidle time.

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 in this topic

Was this page helpful?