Why Are Fonts Missing in Headless Chrome Website Screenshots?
Missing fonts usually mean Chrome captured too early, a font request failed, or the runtime lacks the needed font or glyphs. Here’s how to tell which.
Fonts are usually missing from headless Chrome screenshots for one of three reasons: the screenshot was captured before a web font finished loading, the font request failed, or the browser’s operating system does not have the requested local font or glyphs. Headless mode by itself does not mean Chrome cannot render web fonts. Identify which case applies before changing the page or adding delays.
Chrome’s page load event does not guarantee that every external font request succeeded or that the intended face appears in the screenshot. Google’s documentation notes that Chrome can render the rest of a page while leaving blank space where text using a still-loading font belongs. Google Fonts: technical considerations.
1. Check whether the screenshot raced font loading
Start by making the capture wait for the document’s font set to finish loading. In Puppeteer, document.fonts.ready resolves when the document’s font loading and layout operations have completed. This is a readiness signal, not proof that every requested font succeeded: inspect request failures as well.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
page.on('requestfailed', request => {
if (request.resourceType() === 'font') {
console.error('Font request failed:', request.url(), request.failure()?.errorText);
}
});
page.on('response', response => {
if (response.request().resourceType() === 'font' && !response.ok()) {
console.error('Font response:', response.status(), response.url());
}
});
await page.goto('https://example.com', { waitUntil: 'networkidle2', timeout: 60000 });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'shot.png', fullPage: true });
} finally {
await browser.close();
}
})();
Save this as capture.js, install Puppeteer with npm install puppeteer, and run node capture.js. Replace the example URL with the affected page. If the site keeps making network requests, networkidle2 may never be a useful readiness condition; remove that wait condition or use waitUntil: 'load', then explicitly wait for fonts and any page-specific selector the screenshot needs.
For a quick readiness check in an existing Puppeteer page:
await page.evaluate(() => document.fonts.ready);
console.log(await page.evaluate(() => document.fonts.status));
The status can be loaded even if an individual font failed and the browser fell back. Check the font requests and rendered style as well. To inspect an element’s computed stack:
const fontInfo = await page.$eval('h1', element => {
const style = getComputedStyle(element);
return { family: style.fontFamily, weight: style.fontWeight };
});
console.log(fontInfo);
font-family reports the CSS stack, not definitive proof of which font supplied every glyph. Browser developer tools or a page-specific visual comparison can help confirm the actual rendering.
2. Check whether a font request failed
If the intended web font is not used after the font set settles, inspect the browser console and network log for font requests. Check the requested URL, response status, and whether the capture environment can reach the font host. For self-hosted fonts, verify the deployed file exists and the CSS URL points to it. For cross-origin fonts, check the server’s cross-origin response configuration.
These checks narrow down ordinary browser resource-loading failures; a failed request’s specific cause depends on the site and runtime. Reproduce with the same URL, viewport, locale, and runner image as the failing capture. A font that loads on a developer laptop may be inaccessible from a CI job or server environment.
3. Check local fonts and glyph coverage
A page may use a local or system font, or rely on fallback fonts for characters missing from its primary face. Slim Linux container images may not include the requested family or fonts covering the text’s script. If only some characters are absent, investigate glyph coverage rather than treating it as a general font-loading problem.
Install packages that supply the specific family and character coverage your page needs, using the package manager and package names for the distribution in the capture image. Puppeteer’s troubleshooting guide lists Linux dependencies and gives examples of installing fonts in Docker; Chinese, Japanese, and Korean text may need additional font files. Do not copy a package list blindly across distributions. Puppeteer troubleshooting: Linux dependencies and fonts.
After installing fonts, refresh the font cache if required by that distribution and rebuild the image used by the capture job. Keep the same image when comparing before and after screenshots so you can isolate the change.
4. Verify the Chrome executable and version
Record the browser version, automation-library version, executable path, and whether the runner launches Chrome, Chrome for Testing, or chrome-headless-shell. This helps reproduce version-specific behavior and confirms the process is using the runtime you think it is.
Chromium documents that the old headless implementation stopped being part of the Chrome binary as of M132, and directs users of that mode to chrome-headless-shell. That packaging change is a reason to identify the executable; it does not mean headless Chrome universally drops fonts. Chromium Headless documentation.
5. Use Chrome’s command-line screenshot timing options
Chrome’s headless command-line capture takes the screenshot as soon as the page is loaded by default when neither --timeout nor --virtual-time-budget is supplied. A bounded timeout can give late-loading assets time to arrive, but a delay does not fix a failed font request. Chrome for Developers: headless mode.
chrome --headless --screenshot=shot.png --timeout=5000 https://example.com
This asks Chrome to wait up to five seconds before capture. Adjust the bound for the page and environment; do not treat an arbitrary long timeout as the sole diagnosis. For scripted automation, explicitly wait for font readiness and inspect request results.
6. Diagnose in this order
- Reproduce with the same URL, viewport, locale, container image, and browser executable as the failing job.
- Log font requests and responses. Fix bad URLs, unavailable hosts, or unsuccessful responses first.
- Wait for
document.fonts.readybefore capture, then check that the expected requests actually succeeded. - If the page uses local fonts or only particular scripts are affected, install fonts with the needed family and glyph coverage in the runtime image.
- Record browser and automation versions and verify which binary is launched.
- Compare screenshots after font readiness and after any environment change. Keep other capture settings constant.
Common errors and fixes
| Symptom | Likely cause | What to do |
|---|---|---|
| Text uses a visibly different typeface | The web font is pending or its request failed, so Chrome used a fallback. | Wait for font readiness and inspect font requests, response codes, and the console. |
| Text is blank in the screenshot, then appears later in a normal browser | The capture happened while the web font was still loading. | Wait for document.fonts.ready or use a bounded command-line timeout. Confirm the font request succeeds. |
| Only some letters or one language’s text is missing | The installed fonts lack glyph coverage for those characters. | Identify the missing script and install a font package that includes its glyphs. |
| Works locally but fails in CI or Docker | The runtime differs: it may lack fonts, network access, or the expected browser binary. | Compare the image, executable path, browser version, font packages, and network access. |
| Adding a long sleep sometimes helps but failures return | The delay masks a timing issue, while a failed request or variable network delay remains. | Wait on font readiness, log request outcomes, and fix the underlying delivery or environment problem. |
| Capture fails after a Chrome upgrade | The launched executable or headless packaging may have changed. | Record the version and binary path; check whether the runner needs Chrome or chrome-headless-shell. |
Performance, reliability, and cost
Waiting for fonts can add time when a page loads them slowly, and waiting for total network idleness can add more time or fail on pages with ongoing requests. Prefer the narrowest readiness condition that matches the screenshot: wait for the document font set and any required page element, while logging font failures. Use a bounded navigation timeout so a broken page cannot hold a job indefinitely.
Installing fonts in the capture image makes local-font rendering repeatable across runs, but increases the image’s installed packages and requires rebuilding it when the environment changes. Pin browser and automation versions while diagnosing so an unrelated runtime change does not complicate comparison. No single wait duration or font package fits every website and Linux distribution.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. For a quick capture, send one GET request; see the API documentation for options and setup.
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}`);
Cookie banners are accepted and removed before capture, along with known newsletter popups and chat widgets; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Does headless mode disable web fonts?
No. Headless Chromium runs in a server environment; missing fonts point to timing, failed requests, or missing local font or glyph coverage. Chromium Headless documentation.
Does document.fonts.ready prove that the intended font rendered?
No. It tells you the document’s font loading and layout work has settled. Check the network results and whether the screenshot shows the expected face.
Should I always use networkidle2?
No. Pages with recurring network activity may not reach that state reliably. Wait for the specific fonts and page content needed for your capture.
Why are only a few characters missing?
The selected font may lack those glyphs, and the runtime may not have a suitable fallback. Check which script and characters are affected, then add font coverage for them.


