Table of Contents

Wait Strategies

CobaltPdf must know when the page is "ready" before it captures the PDF. The right strategy depends on how the page loads its content.

Overview

Strategy Factory method Best for
Network Idle (default) (automatic) Most pages — waits until no network requests for 500 ms
Fixed Delay WaitOptions.ForDelay(timeSpan) Pages with known animation/transition durations
CSS Selector WaitOptions.ForSelector(selector) Pages that insert a sentinel element when ready
JavaScript Expression WaitOptions.ForJavaScript(expression) Pages that expose a ready flag on the window
Manual Signal WaitOptions.ForSignal(timeout) Full control — you call window.cobaltNotifyRender()

Call WithWaitStrategy(strategy) in the fluent chain before the terminal render call.

Note

Since 1.6.4, an explicit wait strategy runs alone. Setting ForDelay, ForSelector, ForJavaScript, or ForSignal skips the two implicit pre-waits that previously always ran first: the 5 s client-side-redirect probe on URL renders, and the 10 s network-idle wait before custom JavaScript executes. Your strategy defines page readiness — nothing else queues in front of it. The default network-idle behaviour is unchanged. Both pre-waits can be re-enabled or tuned via WithClientRedirectWait and WithNetworkIdleTimeout.


Network Idle (Default)

Waits until there have been no outstanding network requests for at least 500 ms. This is the default and works well for the majority of pages.

// Explicit, but identical to the default
var pdf = await renderer
    .WithWaitStrategy(WaitOptions.DefaultNetworkIdle)
    .RenderUrlAsPdfAsync("https://example.com");

If the page never goes quiet (continuous analytics/polling, or sub-resource requests that hang), the wait gives up after 30 s and the render proceeds. Cap or skip that wait with WithNetworkIdleTimeout.

Warning

On servers with restricted outbound network access (a common IIS/DMZ configuration), requests for external fonts, analytics, or CDN assets can hang instead of failing fast — so the page never reaches network idle and every render burns the full timeout. If your renders are fast locally but slow in production, this is the most likely cause: switch to an explicit strategy (ForSelector, ForDelay, ForSignal), cap the wait with WithNetworkIdleTimeout, or host the assets where the server can reach them.


Fixed Delay

Waits for an exact duration after the page has loaded. Useful when a page has a known CSS animation that must complete before the PDF is captured.

var pdf = await renderer
    .WithWaitStrategy(WaitOptions.ForDelay(TimeSpan.FromSeconds(2)))
    .RenderUrlAsPdfAsync("https://example.com/animated-chart");

CSS Selector

Waits until a specific DOM element exists and is visible. The render fires as soon as the element appears, without waiting for a fixed time.

// Wait for the data table to render
var pdf = await renderer
    .WithWaitStrategy(WaitOptions.ForSelector("#data-table-loaded"))
    .RenderUrlAsPdfAsync("https://example.com/report");

// With a custom timeout (defaults to 30 s)
var pdf2 = await renderer
    .WithWaitStrategy(WaitOptions.ForSelector(".chart-ready", TimeSpan.FromSeconds(15)))
    .RenderUrlAsPdfAsync("https://example.com/dashboard");

If the selector does not appear before the timeout, a TimeoutException is thrown with a descriptive message.


JavaScript Expression

Waits until a JavaScript expression evaluates to a truthy value. The expression is polled repeatedly until it returns true.

// Wait until the app signals it is done loading
var pdf = await renderer
    .WithWaitStrategy(WaitOptions.ForJavaScript("window.myApp && window.myApp.isReady === true"))
    .RenderUrlAsPdfAsync("https://example.com/app");

// With a custom timeout
var pdf2 = await renderer
    .WithWaitStrategy(WaitOptions.ForJavaScript("document.querySelectorAll('.chart').length >= 3",
                                        TimeSpan.FromSeconds(20)))
    .RenderUrlAsPdfAsync("https://example.com/dashboard");

If the expression does not become truthy before the timeout, a TimeoutException is thrown.


Manual Signal (Most Precise)

The most powerful strategy. CobaltPdf exposes a function called window.cobaltNotifyRender() and waits for your custom JavaScript to call it. This gives you complete control over exactly when the PDF is captured.

Use this with WithCustomJS — your script performs whatever async work is needed, then calls window.cobaltNotifyRender():

string js = """
    (async () => {
        // Wait for the chart library to finish rendering
        await new Promise(resolve => {
            if (window.Chart && window.Chart.instances.length > 0) {
                resolve();
            } else {
                window.addEventListener('chartsReady', resolve, { once: true });
            }
        });

        // Dismiss the cookie banner if present
        const btn = document.querySelector('[data-testid="accept-cookies"]');
        if (btn) btn.click();

        // Signal CobaltPdf to capture
        window.cobaltNotifyRender();
    })();
    """;

var pdf = await renderer
    .WithWaitStrategy(WaitOptions.ForSignal(TimeSpan.FromSeconds(15)))
    .WithCustomJS(js)
    .RenderUrlAsPdfAsync("https://example.com/complex-dashboard");
Important

window.cobaltNotifyRender() must be called, otherwise the render will wait until the timeout and throw a TimeoutException. If WithCustomJS is configured with ForSignal but your script might not always call cobaltNotifyRender(), add a fallback.

Tip

CobaltPdf logs a warning at render time if ForSignal is set but no custom JS is provided.


Combining with Custom JavaScript

WithCustomJS executes your script after the page has loaded and before the wait strategy fires. For most strategies (Network Idle, Selector, JS) this means the script runs first, and then CobaltPdf waits for the selected condition. For ForSignal, the script is responsible for both the work and the signal.

With the default network-idle strategy, the page is additionally given up to 10 s to reach network idle before the script runs, so dynamically-injected elements (e.g. cookie consent buttons) exist when it executes. With an explicit strategy (1.6.4+), the script runs as soon as the DOM is ready — no implicit wait in front of it.

// Run JS to dismiss cookie banners, then wait for network idle
var pdf = await renderer
    .WithCustomJS("document.querySelector('.cookie-banner')?.remove();")
    .RenderUrlAsPdfAsync("https://example.com");
Tip

For a full guide including DOM manipulation examples, async scripts, and CSP notes, see the Custom JavaScript article.


Tuning the Internal Waits

Beyond the wait strategy itself, two options (added in 1.6.4) control the library's internal waits:

WithNetworkIdleTimeout(TimeSpan)

Caps every internal network-idle wait: the readiness wait before custom JS, the waits around the lazy-load scroll, and the default network-idle wait strategy (otherwise 30 s). TimeSpan.Zero skips them entirely.

// Give network idle at most 2 seconds anywhere it applies
var pdf = await renderer
    .WithCustomJS("document.querySelector('.cookie-banner')?.remove();")
    .WithNetworkIdleTimeout(TimeSpan.FromSeconds(2))
    .RenderUrlAsPdfAsync("https://example.com");

WithClientRedirectWait(TimeSpan) / WithoutClientRedirectWait()

URL renders probe for a client-side JavaScript redirect (e.g. bbc.combbc.co.uk) after navigation, so cookies, custom JS, and wait strategies target the true destination. Pages that don't redirect pay the probe in full, so it is skipped automatically when an explicit wait strategy is set. These methods override the automatic choice in either direction:

// URL is known to client-redirect, but we still want ForSelector to run alone otherwise
var pdf = await renderer
    .WithClientRedirectWait(TimeSpan.FromSeconds(3))
    .WithWaitStrategy(WaitOptions.ForSelector("#report"))
    .RenderUrlAsPdfAsync("https://example.com/report");

// Never probe — page is known not to redirect
var pdf2 = await renderer
    .WithoutClientRedirectWait()
    .RenderUrlAsPdfAsync("https://example.com");
Internal wait Default (network-idle strategy) Default (explicit strategy) Override
Client-redirect probe (URL renders) 5 s skipped WithClientRedirectWait
Network idle before custom JS 10 s skipped WithNetworkIdleTimeout
Network idle around lazy-load scroll 10 s 10 s WithNetworkIdleTimeout
Default network-idle strategy 30 s WithNetworkIdleTimeout
Tip

To see exactly where a slow render spends its time, enable logging (CobaltEngine.LoggingMode or CobaltEngine.OnBrowserLog) — since 1.6.4 every render emits [Browser TIMING] entries per stage (navigation, redirect probe, custom JS, wait strategy, lazy-load scroll, PDF capture, and the total).