It will not run
PDF generation runs out of memory
The container is killed with exit code 137, or the process dies without a stack trace, usually under load rather than on a single request.
What is actually happening
Two multiplying factors. A browser needs hundreds of megabytes at rest, and each concurrent render adds a tab. Then buffering the finished document in your own process adds its size again, per request. Four concurrent 20MB reports is 80MB of application heap on top of everything the browser is using.
Confirming it is this and not something that looks like it
Check the exit code. 137 is the OOM killer, and it is definitive rather than suggestive.
The fix
Stream the response instead of buffering it, and bound concurrency explicitly rather than letting the request rate set it. Streaming is the larger win: the document passes through your process without ever being fully resident in it.
import asyncio
import httpx
# Bounded, not "however many requests arrive at once".
_slots = asyncio.Semaphore(4)
async def render(html: str):
async with _slots:
async with httpx.AsyncClient(timeout=60.0) as client:
async with client.stream(
"POST",
"https://api.pdfpipe.xyz/v1/pdf",
headers={"Authorization": f"Bearer {KEY}"},
json={"html": html, "options": {"format": "A4", "printBackground": True}},
) as response:
response.raise_for_status()
# Chunks through. Never fully resident.
async for chunk in response.aiter_bytes():
yield chunkIf that was not it
These produce the same symptom often enough to be worth ruling out before assuming the fix above did not work.
- response.content or .arrayBuffer(), which materialises the whole document
- A queue with no concurrency limit, which converts a traffic spike into a memory spike
- Browser instances that are created and never closed, leaking a process each
- Images at source resolution, which cost memory during the render as well as bytes in the file
Frequently asked
Does this happen with every rendering engine?
The behaviour behind it is not specific to one tool. Two multiplying factors. Anything rendering HTML to a paged medium has to make the same decision, so the fix travels with you if you change how the render happens.
Will this show up as an error in my logs?
Yes, this one surfaces as a thrown error or a failed status, which is why it is at least findable. The harder half is that the message often names the call that was in flight rather than the thing that actually failed.
Related failures
Problems people arrive at from the same starting point, or mistake for this one.
PDF generation is too slow
Usually waiting rather than working.
Failed to launch the browser process
A headless browser is not a self-contained binary.
libnss3.so: cannot open shared object file
This is the previous error with the first missing library named.
Navigation timeout of 30000 ms exceeded
The default wait condition is the network going idle, and something on the page is keeping it busy: an analytics beacon, a font request to a host that is not responding, a polling request, or an ad script.
Protocol error (Page.printToPDF): Target closed
The tab crashed, and by far the most common reason is memory.
CSS for paged documents, and what actually works
Which parts of the paged media specification a browser-based render implements.
Everything that goes wrong, by category
The full list, grouped by where in the pipeline it breaks.
Paste your markup and see the rendered document, without signing up.