ScreenshotNeo

BlogHow-to

Why Scheduled Website Screenshots Have Missing Images and How to Fix Them

Missing images usually mean the capture ran before they were ready or the browser could not load them. Diagnose timing, lazy loading, and failed requests to find the fix.

By the ScreenshotNeo team4 October 20268 min read

Scheduled website screenshots can show missing images even when the page itself appears. The usual issue is browser readiness: the capture may happen before an image loads, a page may load images only after scrolling, or the browser may fail to fetch the image. The right fix depends on which of those is happening, so start by checking capture timing, image loading behavior, and failed requests.

Diagnose the cause in four checks

  1. Check when capture starts. Compare the scheduler’s navigation timeout and any screenshot delay with the time the page and its images need to load. A timeout can let Chrome Headless proceed to capture while the page is still loading. Its --timeout flag sets the maximum wait before capture proceeds. A longer fixed delay may help diagnose an early capture, but it is not a guarantee: page speed and resource behavior vary. Chrome Headless documentation.
  2. Check the image itself. A page-level signal such as network idle does not prove that the particular image is ready, decoded, or visible. Inspect the image element and its source in the same browser context used by the scheduled run.
  3. Check whether scrolling triggers it. If images are missing mostly below the fold, scroll through the relevant page area and see whether they then appear. A full-page screenshot covers the full scrollable page, but that does not necessarily trigger every site’s scroll-dependent or lazy-loading behavior. Playwright screenshot documentation.
  4. Check the resource request. Look for failed image requests, error responses, and browser console messages. If the page works manually but not on schedule, compare authentication, cookies, network access, viewport, and responsive image selection. These are diagnostic possibilities, not a universal explanation.

Fix capture timing with an image-specific readiness condition

Prefer a condition tied to the page or image you need over an arbitrary sleep. For example, wait for the target image to be present, complete, and have a nonzero natural width. Use a selector that matches the actual page and adapt the condition if the site swaps image sources or renders images in another way.

In Playwright, networkidle means there have been no network connections for at least 500 ms. The Page API discourages using it for tests and recommends web assertions to assess readiness instead. Network idle does not promise that an image has decoded or painted. See the Playwright Page API.

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });

page.on('requestfailed', request => {
  console.error('Request failed:', request.url(), request.failure()?.errorText);
});
page.on('response', response => {
  if (response.request().resourceType() === 'image' && !response.ok()) {
    console.error('Image response:', response.status(), response.url());
  }
});

await page.goto('https://example.com', { waitUntil: 'domcontentloaded', timeout: 60000 });
const image = page.locator('img.hero-image');
await image.waitFor({ state: 'visible', timeout: 30000 });
await image.evaluate(async img => {
  if (!img.complete) {
    await new Promise((resolve, reject) => {
      img.addEventListener('load', resolve, { once: true });
      img.addEventListener('error', reject, { once: true });
    });
  }
  if (img.naturalWidth === 0) throw new Error(`Image did not load: ${img.currentSrc || img.src}`);
  if (img.decode) await img.decode();
});
await page.screenshot({ path: 'scheduled-shot.png', fullPage: true });
await browser.close();

Replace https://example.com and img.hero-image with the page URL and a selector for the image that matters. If the page has several critical images, wait for each one or for a page-specific readiness condition that represents them all. Add a timeout and log failures so the scheduled job reports a useful diagnosis instead of waiting forever.

Check lazy-loaded images and scroll-triggered content

Many pages defer image loading until an image approaches the viewport. A full-page capture describes the output area; it does not establish that the page’s own lazy-loading logic has run for every section. Compare a viewport capture with a full-page capture, then test whether scrolling to the missing images causes their sources to load.

await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.evaluate(async () => {
  const step = Math.max(300, Math.floor(window.innerHeight * 0.75));
  for (let y = 0; y < document.documentElement.scrollHeight; y += step) {
    window.scrollTo(0, y);
    await new Promise(resolve => setTimeout(resolve, 150));
  }
  window.scrollTo(0, 0);
});

await page.locator('img').evaluateAll(async images => {
  await Promise.all(images.map(async img => {
    if (img.loading === 'lazy' && !img.complete) {
      await new Promise(resolve => {
        img.addEventListener('load', resolve, { once: true });
        img.addEventListener('error', resolve, { once: true });
      });
    }
    if (img.complete && img.naturalWidth > 0 && img.decode) {
      await img.decode().catch(() => {});
    }
  }));
});
await page.screenshot({ path: 'full-page.png', fullPage: true });

This diagnostic scroll loop is a starting point, not a universal lazy-loading recipe. Sites can use intersection observers, virtualized lists, custom scroll containers, or application-specific triggers. If the content lives in a nested scrolling panel, scroll that panel. If the page adds content as you scroll, check the final page height and continue until it stops growing or a site-specific completion condition is met.

Inspect failed requests and scheduled-run context

If extra waiting does not restore the images, compare the scheduled run with a manual run using the same browser, viewport, session, and network conditions. Check:

  • Whether the image element exists and what src, srcset, and currentSrc it uses.
  • Whether the request failed, returned an error status, or was blocked by the browser or environment.
  • Whether the page needs login state, cookies, or other request headers.
  • Whether a different viewport selects another responsive image or hides the image.
  • Whether a third-party image host is reachable from the scheduler’s network.
  • Whether the page shows a consent dialog, bot check, or other overlay that changes what is visible.
const imageDetails = await page.locator('img').evaluateAll(images =>
  images.map(img => ({
    src: img.src,
    currentSrc: img.currentSrc,
    complete: img.complete,
    naturalWidth: img.naturalWidth,
    naturalHeight: img.naturalHeight,
    loading: img.loading
  }))
);
console.table(imageDetails);

A nonzero naturalWidth is evidence that an image resource loaded; it is not proof that the image is visible in the final composition. Also check computed styles, clipping, overlays, and whether the element is inside the captured region.

Use screenshot stability checks where they fit

For Playwright Test visual comparisons, toHaveScreenshot() waits for two consecutive screenshots to produce the same result before comparing the last one. This can help catch visual instability in a test workflow. It is a Playwright Test assertion behavior, not a readiness setting guaranteed to exist in every scheduler, and stable output can still contain a consistently missing image. See Playwright visual comparisons.

Common errors and fixes

Symptom Likely cause What to do
Images appear in a manual screenshot but not the scheduled one Different wait limits, cookies, authentication, viewport, or network context Log the scheduled context and match it in a local reproduction; wait for the needed image condition.
Only below-the-fold images are missing Lazy loading or scroll-triggered rendering Scroll through the page or relevant container, wait for image completion, then capture.
The screenshot is blank where the image should be, but no request error is logged The image may not have been requested yet, may be hidden, or may be clipped Inspect currentSrc, completion state, computed styles, and element bounds.
Waiting for networkidle still produces missing images Network quiet is not an image decode or paint guarantee; long-lived traffic may also prevent idle Wait for the specific image or application-ready condition instead.
The job times out after increasing its wait A request may hang, the page may keep making requests, or the wait condition may never become true Set bounded navigation and condition timeouts; log failed requests and report which selector or image timed out.
A full-page screenshot omits images that appear after manual scrolling The page’s loading trigger may depend on scrolling Scroll in increments before capture and confirm the images load; adapt for virtualized or nested-scroll content.
An image element reports zero natural width Its resource did not load or the source is invalid Check the request URL, response, authentication, cross-origin host access, and selected responsive source.

Performance, reliability, and cost considerations

Waiting for every image on a large page can make a scheduled capture slower and can cause it to fail because of an irrelevant image or a permanently broken resource. Identify the images that matter, use bounded waits, and record which requests fail. Scrolling the entire page also takes time; use it only when the content or capture requires it. A fixed delay is easy to add but can waste time on fast runs and still be too short on slow ones.

For reliable recurring captures, keep the viewport, authentication state, browser settings, and network route consistent. Store diagnostic details alongside a failed capture: URL, timestamp, viewport, wait condition, image source, request status, and timeout. This makes intermittent site or network changes easier to distinguish from a timing issue. No source here supports a universal wait duration or a claim that one scheduling approach is always faster or cheaper.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. It accepts one request with a URL and returns a PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Only clean shots are billed: bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses report the page verdict and billing status in headers. AI agents can use its MCP tools, including take_screenshot, get_page_info, and capture_pdf. See the ScreenshotNeo site and API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo includes full-page capture with lazy images loaded, waits for a selector, delay, or network idle, and supports custom headers and cookies when a page needs a specific context. It also offers selector capture, device and viewport settings, custom CSS and JavaScript, request blocking, caching, bulk capture, and async jobs. Check the documentation for parameter names and response details before adapting an existing scheduler.

Plans include 1,000 screenshots a month free with no card; paid plans start at $5 for 3,000 screenshots. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots. Sign up for 1,000 free screenshots a month, with no card required.

FAQ

Does a full-page screenshot automatically load every image?

It captures the full scrollable page, but a site’s lazy-loading or scroll-triggered behavior may still need to run. Test the target page by scrolling and checking image state.

How long should a scheduled screenshot wait?

There is no universal duration. Use a bounded wait for the specific page or image condition that matters, and use a delay only as a diagnostic or when the page offers no better readiness signal.

Does networkidle guarantee images are ready?

No. It describes a period without network connections, not whether a particular image decoded, painted, or became visible.

Could this require different hardware?

The documented remedies are in browser automation and page-loading workflow. The available research does not indicate that buying hardware or an accessory is needed.