Fix Images Not Loading in Puppeteer Screenshots on Indian Websites
Missing images in a Puppeteer screenshot? Log image requests and response codes, then wait for the page’s actual image state before capturing.
If images are missing from a Puppeteer screenshot, first log each image request, its failure (if any), and its HTTP response status. Then check whether the image element is actually loaded and decoded before taking the screenshot. networkidle2 is a timing condition, not proof that every image succeeded. The title alone does not establish that an Indian website, the runner’s location, or any single cause is responsible.
Use the diagnostic script below from the same machine or container that produces the bad screenshot. It records image request failures and HTTP statuses, waits for navigation, waits for images in the initial document to finish loading or fail, and saves the capture. Replace the example URL with the affected page.
1. Run a diagnostic screenshot
Install Puppeteer in a Node.js project with npm install puppeteer, save this as screenshot.mjs, then run node screenshot.mjs. Puppeteer’s screenshot guide uses page.goto() with waitUntil: 'networkidle2' and then page.screenshot(); this example adds request logging and an image-state wait. Puppeteer screenshot documentation.
import puppeteer from 'puppeteer';
const target = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
page.setDefaultNavigationTimeout(45_000);
page.setDefaultTimeout(15_000);
// Attach listeners before navigation so early image requests are captured.
page.on('request', request => {
if (request.resourceType() === 'image') {
console.log('[image request]', request.url());
}
});
page.on('requestfailed', request => {
if (request.resourceType() === 'image') {
console.error('[image failed]', request.url(), request.failure()?.errorText);
}
});
page.on('response', response => {
if (response.request().resourceType() === 'image') {
console.log('[image response]', response.status(), response.url());
}
});
page.on('console', message => console.log('[browser console]', message.type(), message.text()));
page.on('pageerror', error => console.error('[page error]', error.message));
const navigation = await page.goto(target, {
waitUntil: 'domcontentloaded',
timeout: 45_000,
});
console.log('[document response]', navigation?.status(), page.url());
// Optional timing signal. It does not validate image success.
try {
await page.waitForNetworkIdle({ idleTime: 500, concurrency: 2, timeout: 15_000 });
} catch {
console.warn('[network idle] Timed out; checking image elements anyway.');
}
// Wait only for image elements currently in the DOM. A broken image is
// reported as broken and does not make this wait hang indefinitely.
const imageStates = await page.evaluate(async () => {
const images = [...document.images];
await Promise.all(images.map(image => {
if (image.complete) return Promise.resolve();
return new Promise(resolve => {
image.addEventListener('load', resolve, { once: true });
image.addEventListener('error', resolve, { once: true });
});
}));
return images.map(image => ({
src: image.currentSrc || image.src,
complete: image.complete,
naturalWidth: image.naturalWidth,
naturalHeight: image.naturalHeight,
loaded: image.complete && image.naturalWidth > 0,
loading: image.loading,
}));
});
console.table(imageStates);
await page.screenshot({ path: 'page.png', fullPage: true });
console.log('Saved page.png');
} finally {
await browser.close();
}
This code intentionally distinguishes a failed request from an HTTP error response and from a successful response whose image is not rendered in time. Puppeteer documents that requestfailed is emitted when a request fails, while HTTP errors such as 404 or 503 still complete as HTTP requests. Redirects issue a new request to the redirected URL. HTTPRequest lifecycle documentation.
2. Read the evidence before changing the script
| Observation | What it establishes | Next check |
|---|---|---|
[image failed] with an error |
The browser request did not complete successfully. | Inspect the exact URL, failure text, redirects, TLS or DNS errors, and whether the same request fails outside the runner. |
| Image response is 404, 403, 429, or 5xx | The server returned an HTTP response, but its status may explain the missing image. | Open the precise URL, inspect its redirect destination and access requirements, and check the origin/CDN logs if you control it. |
Image response is 2xx, but naturalWidth is zero |
The element has not produced a usable decoded image at the time inspected. | Check the final currentSrc, content type, browser console, responsive source selection, and whether the page replaces the element later. |
naturalWidth is positive, but screenshot still looks blank |
The image decoded, so investigate page state and capture timing. | Check CSS visibility, overlays, animation, lazy loading, iframe/shadow-root content, and whether the screenshot is of the intended page/frame. |
| No image request appears | The page may not have requested it yet, or the asset may not be an <img>. |
Inspect CSS background images, lazy loading, JavaScript-triggered requests, iframe content, and the selectors present before capture. |
Keep the exact request URL and status in your notes. “The page works in my browser” is not enough to distinguish a different image URL, a redirect, an access rule, or a later page state.
3. Choose a readiness condition that matches the page
page.waitForNetworkIdle() waits until network activity is idle and always waits at least the configured idle time. The documented default idle time is 500 milliseconds; concurrency controls the maximum number of connections considered inactive. A quiet network can still contain a broken image, and a page with polling or analytics may never become idle. waitForNetworkIdle() and its options.
- Ordinary eager images: wait for image elements to complete, then inspect
naturalWidth. - Lazy images below the fold: scroll the page or the relevant container so the site triggers loading, then wait for those images. A full-page screenshot does not guarantee that every site has eagerly loaded its lazy content.
- Images set by application code: wait for a page-specific selector or state that signals the application has finished rendering.
- Continuously active pages: use a bounded image or selector wait instead of treating network idle as mandatory.
- Element capture: wait for the target element and capture it with
ElementHandle.screenshot(); Puppeteer documents that this can scroll a hidden element into view. Screenshot guide.
For a lazy-loaded page, a basic scroll trigger can be inserted before the image-state check:
await page.evaluate(async () => {
const step = Math.max(300, Math.floor(window.innerHeight * 0.75));
for (let y = 0; y < document.body.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise(resolve => setTimeout(resolve, 150));
}
window.scrollTo(0, 0);
});
await page.waitForNetworkIdle({ idleTime: 500, concurrency: 2, timeout: 15_000 });
This scroll loop is a trigger, not a guarantee: virtualized lists may remove off-screen items, and a site may use a scroll container other than the window. Adapt it to the actual page. Avoid adding long fixed sleeps without first identifying which state needs time.
4. Check geography only when the evidence supports it
An Indian domain or Indian audience does not by itself explain a missing image. The same URL may behave differently from different execution environments because of server rules, CDN routing, access controls, or network conditions, but this must be established by comparing observations. Run the same script and record the same image URL, redirect chain, failure text, and status from the relevant runner and a normal browser or another known environment. If results differ, investigate the specific host and response. Do not label the cause India-specific unless a controlled comparison isolates geography.
Also check whether the page’s own image URL is HTTP while the document is HTTPS, whether it requires a session cookie or authorization, and whether the URL is signed or expires. These are diagnostic possibilities, not claims about the site in question.
5. Screenshot format and capture details
Once the image state is correct, choose capture options that fit the job. Puppeteer supports full-page screenshots and element screenshots. Set the viewport before navigation when responsive image selection matters, because the page may select a different srcset candidate at another viewport. Record currentSrc to see which candidate the browser selected.
await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });
await page.goto(target, { waitUntil: 'domcontentloaded' });
const hero = await page.waitForSelector('.hero-image', { visible: true });
await hero.screenshot({ path: 'hero.png' });
// For the entire document instead:
await page.screenshot({ path: 'full-page.png', fullPage: true });
Use a real selector from the page. Element capture can help isolate whether the target image is loaded while the overall page has a layout or overlay issue.
6. Common errors and fixes
| Error or symptom | Likely interpretation | Fix or next diagnostic step |
|---|---|---|
Navigation timeout |
The selected navigation milestone did not occur before the timeout. | Try domcontentloaded for initial HTML, then wait for the image or application condition you actually need. Keep a finite timeout and log the page URL. |
TimeoutError waiting for network idle |
Requests remain active or the page never satisfies the idle condition. | Catch the timeout and use a page-specific readiness check; do not infer that images failed solely from this timeout. |
requestfailed with a browser error |
The request did not receive a usable completion. | Record request.failure()?.errorText and the full URL. Diagnose the matching network, policy, or browser error before changing launch settings. |
404 or 503, but no requestfailed |
HTTP error responses still complete as HTTP requests. | Handle the response status separately from transport failures; correct the URL or investigate the origin/CDN response. |
| 403 or 429 | The server declined or throttled the request. | Check access policy, required session state, and rate limits with the site owner. Do not assume a longer wait will fix a denied response. |
net::ERR_BLOCKED_BY_CLIENT |
A browser-side block may be involved. Puppeteer’s troubleshooting guide documents a specific HTTPS-First remote HTTP navigation case that can produce this error. | First verify the exact request and whether it is that documented case. Apply the guide’s launch-argument workaround only when the observed failure matches; it is not a general image fix. Puppeteer troubleshooting. |
| Image URL redirects to a login or consent page | The final response may be HTML or require session state rather than being the expected image. | Inspect each redirected URL and response status. Reproduce the browser’s required authentication or cookie state only when authorized. |
| Image succeeds in logs but is missing in capture | The issue may be decode timing, CSS, overlay, animation, or the wrong frame. | Check naturalWidth, visibility, bounding box, computed styles, and the selected frame immediately before capture. |
| Browser closes before screenshot is written | The capture may be interrupted by cleanup or an earlier exception. | Keep screenshot inside the try block and browser cleanup in finally, as in the example. |
7. Performance, reliability, and cost
- Keep waits bounded. Set navigation and condition timeouts so a stalled page does not hold a worker indefinitely.
- Wait for the smallest useful condition. A specific image or page selector is often more predictable than waiting for every request to stop.
- Log only what helps diagnose. Image URLs can contain signed tokens or personal data; redact query parameters before storing logs where appropriate.
- Close the browser in cleanup. Use
try/finallyso exceptions do not leave browser processes running. - Do not blindly retry. A 404 or access denial is unlikely to improve with repeated immediate attempts; retry transient network errors selectively and with a limit.
- Cost depends on your own runtime and network setup. The dossier provides no Puppeteer benchmark or hosting price, so estimate from your browser hosting, concurrency, and target traffic rather than assuming a fixed per-screenshot cost.
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Its one-request API can return an image or PDF without you managing a Puppeteer browser. See the ScreenshotNeo API documentation for parameters and response details.
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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Replace https://stripe.com with the page you need and keep your API key secret. ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
Does Puppeteer require a physical computer to capture screenshots?
No. Puppeteer is browser-control software; it can run in a suitable server or container environment. Follow the installation and system requirements for the environment you use.
Does a 200 image response prove the image is visible?
No. It indicates an HTTP success status. Check that the browser selected the expected URL and that the image element has a positive naturalWidth before capture.
Should I always use networkidle2?
No. It is one navigation wait strategy. Choose a wait condition that matches the page, and separately verify image state when missing images are the issue.
How can I prove that location causes the failure?
Compare the same script, target, selected image URL, and response evidence across environments. A location-related explanation needs a controlled comparison that isolates that variable.


