How to Fix WeasyPrint Image-Loading Timeouts
Fix WeasyPrint image timeouts by checking URL resolution, network access, authentication, fetcher settings, caching and resource limits.
WeasyPrint does not download images through the browser. It retrieves images, fonts and stylesheets through its URL fetcher. The documented HTTP, HTTPS and FTP timeout is 10 seconds, so a slow, unreachable or protected image can trigger warnings or leave an image out of the PDF.
Fix the failure by identifying the exact asset URL, supplying a correct base_url, setting an explicit fetch timeout, adding authentication in a custom fetcher, and deciding whether missing assets should fail the render. These controls solve different problems.
1. Confirm which image request is timing out
- Log the final
srcafter template expansion. Do not debug the template variable before confirming the URL actually sent to WeasyPrint. - From the same machine, container or worker that renders the PDF, check DNS, TLS, redirects, status and response time.
- Compare the response to the browser request. Browser cookies, VPN access, proxy settings and logged-in sessions are often absent in a PDF worker.
python - <<'PY'
import time
import requests
url = "https://example.com/assets/hero.png"
started = time.monotonic()
r = requests.get(url, timeout=20, allow_redirects=True)
print({
"status": r.status_code,
"final_url": r.url,
"content_type": r.headers.get("content-type"),
"bytes": len(r.content),
"seconds": round(time.monotonic() - started, 3),
})
PY
A successful request from your laptop only proves that your laptop can reach the asset. Run the check where WeasyPrint runs.
2. Fix relative image URLs with base_url
Relative paths such as images/logo.png need a meaningful base URL. Without one, WeasyPrint cannot resolve the path reliably. The API reference documents base_url for this purpose: WeasyPrint API reference.
from weasyprint import HTML
html = """
"""
HTML(
string=html,
base_url="https://app.example/",
).write_pdf("out.pdf")
For local files, use the directory containing the HTML as the base:
from pathlib import Path
from weasyprint import HTML
html_path = Path("build/report.html").resolve()
HTML(
filename=str(html_path),
base_url=str(html_path.parent),
).write_pdf("out.pdf")
On the command line, use --base-url:
weasyprint report.html out.pdf --base-url /srv/app/templates
3. Increase the network timeout deliberately
URLFetcher(timeout=...) changes the timeout used for network protocols. The default is 10 seconds for HTTP, HTTPS and FTP; it does not change file:// access behavior. Set the value explicitly in application configuration so it is visible and reviewable.
from weasyprint import HTML
from weasyprint.urls import URLFetcher
fetcher = URLFetcher(timeout=20)
HTML(
string=html,
base_url="https://app.example/",
url_fetcher=fetcher,
).write_pdf("out.pdf")
The command-line equivalent is:
weasyprint report.html out.pdf --timeout 20
Choose a timeout from observed response times plus headroom. Raising it cannot repair a wrong hostname, blocked egress, invalid TLS, a 404 or missing credentials. It only lets a reachable request wait longer.
4. Add headers, cookies or authorization with a custom fetcher
The default fetcher handles common file and HTTP URLs but does not automatically know your application session, bearer token or private asset headers. Wrap it and add credentials only for the hosts that need them. Delegate unrelated URLs to the default fetcher as shown in the official URL-fetcher guidance: custom URL fetchers.
from urllib.parse import urlparse
from weasyprint import HTML
from weasyprint.urls import default_url_fetcher
ASSET_HOST = "private-assets.example"
TOKEN = "replace-with-a-short-lived-token"
def authenticated_fetcher(url, timeout=20, ssl_context=None):
parsed = urlparse(url)
if parsed.hostname == ASSET_HOST:
# Return the documented fetcher response shape. Keep the body bytes
# and content type supplied by the response.
import requests
response = requests.get(
url,
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=timeout,
)
response.raise_for_status()
return {
"string": response.content,
"mime_type": response.headers.get("Content-Type", ""),
"redirected_url": response.url,
}
return default_url_fetcher(url, timeout=timeout, ssl_context=ssl_context)
HTML(
string=html,
base_url="https://app.example/",
url_fetcher=authenticated_fetcher,
).write_pdf("out.pdf")
For cookies, pass a Cookie header or use a requests session. Restrict credentials by hostname, avoid logging tokens, and do not forward an end-user cookie to arbitrary URLs.
5. Decide whether a missing image should fail the PDF
During normal rendering, WeasyPrint generally reports fetch failures as warnings and can produce a PDF with a missing image. That is useful for noncritical decoration but dangerous for invoices, certificates or required signatures.
Enable strict HTTP-error handling while diagnosing, using the CLI option documented by WeasyPrint:
weasyprint report.html out.pdf --fail-on-http-errors
In production, classify assets:
- Required: fetch them before rendering or fail the job when unavailable.
- Optional: allow a warning and render a usable PDF.
- Decorative: consider embedding a local fallback.
6. Reduce latency and resource use
- Serve stable assets from the same network as the renderer, or embed small images as data URLs.
- Resize oversized source images before PDF generation.
- Use the
dpisetting to cap effective image resolution when appropriate. - Use an image cache and, for repeated CLI jobs, a
cache-folderso identical remote assets are not downloaded repeatedly. These options reduce repeated work; they do not fix an unreachable host. See the API reference. - Set process time and memory limits for untrusted or user-supplied HTML.
7. Security controls when increasing timeouts
HTML and CSS can reference network and file URLs. A renderer processing untrusted input could be made to access internal services, read local files or consume resources for a long time. Keep protocol allowlists, filter file access, sanitize external URLs and enforce process CPU, wall-time and memory limits. A larger timeout without those controls can amplify resource exhaustion.
8. Troubleshooting checklist
| Symptom | Likely cause | Fix |
|---|---|---|
| Relative image is missing | No or incorrect base URL | Set Python base_url or CLI --base-url; log the resolved URL. |
| Timeout after exactly about 10 seconds | Default network timeout | Use URLFetcher(timeout=...) or CLI --timeout. |
| Works in browser, fails in PDF worker | Different DNS, firewall, proxy or credentials | Test from the rendering host and configure the worker's network and headers. |
| 401 or 403 response | Missing token, cookie or signed URL | Use a restricted custom fetcher or generate a short-lived asset URL. |
| Redirect ends at an unexpected host | Redirect policy or authentication not forwarded | Log the final URL and allow only expected hosts. |
| PDF succeeds but image is blank | Fetch warning was tolerated, invalid content or unsupported format | Run with strict HTTP errors, inspect content type and validate the downloaded bytes. |
| Jobs become slow or memory-heavy | Large images, repeated downloads or excessive concurrency | Optimize images, enable caching, cap resolution and limit concurrent renders. |
9. Complete diagnostic example
from weasyprint import HTML
from weasyprint.urls import URLFetcher
html = """
Report
"""
fetcher = URLFetcher(timeout=30)
HTML(
string=html,
base_url="https://app.example/",
url_fetcher=fetcher,
).write_pdf("report.pdf")
10. Or skip the browser setup
If your goal is a reliable screenshot or PDF endpoint rather than maintaining a WeasyPrint worker, ScreenshotNeo provides a GET API and PDF capture. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets. Bot checks, blank pages, failed loads, timeouts and cache hits are not billed, and each response reports its verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP server also gives Claude, Cursor and other MCP clients take_screenshot, get_page_info and capture_pdf tools.
See the ScreenshotNeo API documentation for all options.
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, CSS-selector element capture, custom headers and cookies, custom JavaScript, waits, request blocking, caching, PDFs and async jobs. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
Does increasing timeout affect local files?
No. The timeout applies to network protocols such as HTTP, HTTPS and FTP. It does not change file:// access behavior.
Why does a PDF render even though an image failed?
Fetch errors are commonly reported as warnings, allowing the document to finish without that asset. Use strict HTTP-error handling when the image is required.
Should I use a bigger timeout for every job?
Use an explicit, measured value. Also fix DNS, authentication, URL resolution and oversized assets; a larger limit only addresses genuine network latency.
Can a custom fetcher solve private images?
Yes. Add authorization headers, cookies or a signed request for the required host, return the documented response shape, and delegate public URLs to the default fetcher.


