You click Print → PDF on a Sales Invoice. The tab spins for a minute or two. Then: 504 Gateway Time-out. Every format does it, including a blank one. This is frappe issue #40334, and it affects exactly three releases: v16.24.0, v16.24.1 and v16.24.2. A path-containment check loosened by PR #40099 let Chrome's setup navigation resolve to the public directory; frappe.read_file() raised IsADirectoryError; that exception killed the DevTools websocket reader; and the untimed CDP call blocked your gunicorn worker until it was SIGKILLed. The fix is PR #40365, released in v16.24.3 on 28 June 2026. This post gives you the version boundary, the workaround, the upgrade, and — the part nobody else gets right — how to tell this bug apart from the Print Designer chromium-path bug that everyone confuses it with.
Upgrading to v16 in the first place?
This is one of the sharper edges of the v16 line. If you are still deciding, read ERPNext v15 to v16: should you upgrade? first. And if your symptom is missing CSS rather than a hang, the frappe_docker asset-pipeline break is the more likely culprit.
I am Manoj, ERP implementation lead at Mith Tech in Bengaluru. We have shipped enough emergency v16 patch bumps to know that the expensive part of this bug is not the fix — it is the two days a team spends chasing "Perhaps out of memory?" in the gunicorn log, which is stock wording and means nothing.
Start here: which Chrome PDF bug do you actually have?
There are two unrelated Chrome-PDF failures circulating in the Frappe community right now, and they get conflated in nearly every forum thread and AI answer. They have different root causes, different fixes, and — this is the part that matters operationally — different current release status. One is fixed and shipped. The other is fixed on a branch and has never been released.
Pick your exact symptom, enter your frappe version, and say whether Print Designer is installed. The decider routes you to the correct root cause, the concrete fix, the current release status of that fix, and the real issue links — keeping frappe #40334 strictly separate from the Print Designer chromium path-key family.
Pick a symptom to get a verdict. The two failure families look identical from the Desk UI and have completely different fixes.
Print Designer ships its own fork of the Chromium stack that never took the change behind #40334, so it is structurally immune to it. Answering “yes” above changes the diagnosis, not the blame.
The short version, if you only remember one thing:
If your PDF hangs for a minute then 504s, it is frappe #40334 → upgrade to v16.24.3 or newer. If your PDF fails instantly with "Chromium is not downloaded", it is the Print Designer binary-path family → set
chromium_binary_pathandchromium_path, and runbench setup-chromein every container that prints.
The version boundary, proved rather than assumed
Most write-ups say "v16.24.1 and v16.24.2 are broken". That is one release short. The regression commit 235c12a369 — the backport of PR #40098 onto version-16-hotfix as PR #40099 — is an ancestor of v16.24.0, and v16.24.0's release notes credit it in plain language: PDF creation now accepts files from the site's public folders, not just the folder used for uploaded files. That loosening is the bug.
The v16.24.x release sequence with the broken window marked, the regression PR and fix PR called out with their commit hashes, and a live verdict for any version you type in — including the branch-tracker case where you are on version-16-hotfix rather than a tag.
Paste the output of `bench version` — for example `frappe 16.24.1 version-16-hotfix`.
Ancestry above is from GitHub’s compare API, not inference: 235c12a369 is an ancestor of v16.24.0 onward, and 63a94104ce is an ancestor of v16.24.3 onward.
Two footnotes that catch people out:
If you track the version-16-hotfix branch instead of a tag, your broken window is wider: 18 June 2026 (when #40099 merged) to 28 June 2026 (when the fix merged as #40364). Ten days, not four.
The develop/v17 line had it too, briefly. The same one-line guard landed on develop on 25 June 2026, carried inside PR #40295 — a large print-format-builder PR that happened to include it. It was never backported to v16 by that PR, which is exactly why the v16 hang survived three more days and needed its own emergency release.
Why a PDF bug kills an entire worker
A failing PDF should return an error. Instead it takes a gunicorn worker with it. The chain is worth understanding because it explains why the timeout knob does not help.
An eight-step walkthrough of the failure chain, from Chrome's setup navigation to the bare site URL, through the loosened containment check and the IsADirectoryError, to the dead CDP reader, the unresolvable future, the gunicorn SIGKILL and the 504 your browser finally shows.
During PDF page setup, frappe points the body, header and footer pages at get_host_url() — the site root, with no path component.
This happens before your print format HTML is rendered. That is why even a blank format fails: the bug is in setup, not in your template.
Traceback line numbers vary between checkouts, so we describe the call chain rather than quoting line numbers as canonical.
The single most useful line in the whole chain is the Error Log title: WebSocket listening error:. That row is the fingerprint. Its body carries the money line, IsADirectoryError: [Errno 21] Is a directory, pointing at your site's public folder.
A note on line numbers: the traceback pasted into the GitHub issue came from the reporter's exact checkout and its line numbers differ slightly from the live v16.24.3 source. Match on the call chain and the exception, not on line numbers.
A note on the timeout figure: reporters cite "60 seconds", and you will see 60 quoted everywhere. But bench's default http_timeout is 120, and that single key drives both gunicorn's -t and nginx's proxy_read_timeout. The 60 s figure is a per-deployment override common in hosted and Docker stacks — real, but not the default. Do not take it as the canonical number.
Fix it
Three tabs with copy buttons: the immediate wkhtmltopdf workaround with the exact navigation paths and its caveats, the permanent version bump plus a one-line cherry-pick for teams that cannot take a release bump, and a verification routine that actually proves the fixed code is running.
Get printing again in minutes without touching the frappe version. The catch most guides miss: the effective generator is read from the Print Format record, not from Print Settings — so changing the global setting alone does nothing. Set both.
# 1) UI route (do both — the second one is what actually takes effect)
#
# Global: Awesome Bar -> "Print Settings" -> PDF Settings section
# -> PDF Generator -> wkhtmltopdf -> Ctrl+S
# (menu route: Home > Settings > Print Settings)
#
# Per format: Awesome Bar -> "Print Format" list -> open the format
# you actually print with -> PDF Generator -> wkhtmltopdf
# -> Save. Repeat for every format in use.
# (Or select-all in the list view -> Edit -> PDF Generator.)
# 2) Same thing from the CLI, for every format still on chrome:
bench --site yoursite console
>>> frappe.db.get_value("Print Settings", None, "pdf_generator")
>>> stuck = frappe.get_all("Print Format",
... filters={"pdf_generator": "chrome"}, pluck="name")
>>> for pf in stuck:
... frappe.db.set_value("Print Format", pf,
... "pdf_generator", "wkhtmltopdf")
>>> frappe.db.commit()
# 3) Clear the wedged state — reverting the setting does NOT free
# workers that are already blocked.
bench restart
sudo supervisorctl restart all # if workers are still pinned
pkill -f chromium-headless-shell # reap orphaned browsers
# CAVEATS
# - download_pdf accepts ?pdf_generator=chrome. Any bookmark, custom
# button or integration carrying that param still hits Chrome.
# - wkhtmltopdf uses an old Qt WebKit engine: no modern flexbox/grid,
# different font metrics and page breaks. Formats authored against
# the Chrome renderer will visibly shift.
# - This is a stopgap. wkhtmltopdf is unmaintained upstream.Confirm it is #40334 before you change anything
Print a blank format with the chrome generator. If that also 504s, the failure is in page setup. Then search the Error Log for IsADirectoryError or the title WebSocket listening error:. Two positives and you are done diagnosing.
Buy yourself time with wkhtmltopdf
Set PDF Generator to wkhtmltopdf on the Print Format records you actually print with — that field, not the global one, is what the resolution code reads. Set the global in Home > Settings > Print Settings as well, belt and braces. Then bench restart.
Clear the wedged state
Reverting the setting does not release workers that are already blocked. bench restart, then sudo supervisorctl restart all if workers are still pinned, then pkill -f chromium-headless-shell to reap the orphaned browsers that the SIGKILLed workers left behind.
Upgrade past the window
Get to v16.24.3 or newer. bench update --reset is the standard route; checking out the tag and running bench setup requirements, bench build --app frappe, bench --site all migrate is the frappe-only route. On Docker, bump the image tag and redeploy — do not patch inside a running container. On Frappe Cloud, update the bench group; v16.24.3 was cut specifically to unblock those sites.
Verify, then switch back to chrome
Grep frappe/utils/pdf_generator/page.py for os.path.isfile(final_system_path). One hit means the guard is present. Confirm the Error Log has stopped collecting IsADirectoryError rows, request one chrome-generated PDF and expect a 200 in a couple of seconds — then move your print formats back off wkhtmltopdf, which is unmaintained upstream and renders modern CSS badly.
The Print Designer confusion, settled
This is the part worth your attention, because it is where most of the internet is wrong.
Print Designer registers its own pdf_generator in hooks.py and ships a fork of the CDP stack. Its before_request hook intercepts download_pdf and printview and serves them through its own generator entirely. Crucially, its forked page.py still uses the pre-#40098 prefix check — it only accepts paths starting with assets/ or files/. An empty root path fails both, falls straight through to continueRequest, and the directory can never reach read_file. Print Designer never inherited the regression because it never took the change.
So the commonly-cited "Print Designer breaks Chrome PDF" issues — print_designer #437, #432, #407 and frappe_docker #1821 — are a distinct bug family. Three of them predate the v16.24.0 regression by more than a year, which makes them chronologically impossible to be the same bug.
The core of that family is a config-key mismatch. Frappe core looks up chromium_path in the common site config. Print Designer's released code looks up chromium_binary_path. A site configured the core way starts throwing "Chromium is not downloaded" the moment Print Designer is installed — installing Print Designer breaks previously-working Chromium PDF generation, which is exactly what frappe_docker #1821 is titled.
And here is the status that matters: print_designer PR #523, which makes Print Designer fall back to chromium_path, was merged to develop on 7 May 2026 — but the latest tagged Print Designer release is v1.6.7 from 10 February 2026, three months earlier. No released version of Print Designer contains that fix. Set both config keys and keep them set.
What else to know before you touch production
The ?pdf_generator=chrome escape hatch. The download_pdf endpoint accepts an explicit query parameter, and it wins over your doctype settings. Any bookmark, custom button, or integration carrying that param will still hit Chrome no matter what you set in Print Settings. Grep your customisations before you declare the workaround complete.
wkhtmltopdf is a stopgap, not a destination. It renders on an old Qt WebKit engine: no modern flexbox or grid, different font metrics, different page-break behaviour. Formats authored against the Chrome renderer will visibly shift. It is also unmaintained upstream. Use it to stop the bleeding, then go back to chrome once you are on v16.24.3+.
Orphaned Chromium processes are a real second-order cost. Because SIGKILLed workers never clean up, every retry leaves a chromium-headless-shell behind and RAM climbs. Frappe's own source carries a # TODO: close browser when worker is killed. comment — this is a known adjacent problem, not something you introduced.
The pdf_generator field is undocumented. It exists on both the Print Settings and Print Format doctypes, but as of writing it is not documented on any live docs.frappe.io page. The only official written description of the chrome generator's behaviour is release-notes prose and the source itself. If a consultant tells you the docs say otherwise, they do not.
Multi-container Docker stacks need Chromium everywhere. Frappe enqueues bulk print jobs to the workers, so queue-short and queue-long need a reachable Chromium binary too — not just backend. That is a separate, still-open problem, and it is the one most likely to bite after you have fixed everything else. Our ERPNext implementation practice treats printing infrastructure as part of the deployment checklist for exactly this reason.
+Why does ERPNext PDF download give a 504 Gateway Timeout?
On frappe v16.24.0 through v16.24.2, the Chrome PDF generator's local-resource interceptor tries to read your site's public directory as if it were a file. The resulting IsADirectoryError kills the CDP websocket reader task, and because the synchronous CDP send has no timeout, the gunicorn worker then blocks forever on a response that will never arrive. Gunicorn SIGKILLs the worker at http_timeout and nginx serves the 504. Upgrade to v16.24.3 or newer.
+Which version of Frappe fixes the Chrome PDF 504 error?
v16.24.3, published 28 June 2026, via PR #40365 (commit 63a94104ce). v16.24.0, v16.24.1 and v16.24.2 are all broken — note that v16.24.0 is included, which is wider than most write-ups claim. Anything from v16.24.3 onward, including v16.24.4 and v16.25.0, contains the fix.
+Hey Google, why is my ERPNext invoice PDF not downloading?
If it spins for about a minute and then shows a 504 error page, you are almost certainly on frappe v16.24.0, v16.24.1 or v16.24.2 with the Chrome PDF generator. Switch the print format you use to wkhtmltopdf for immediate relief, then update frappe to v16.24.3 or later and switch back.
+Ok Google, is the ERPNext Chrome PDF bug fixed yet?
Yes. Frappe issue 40334 was opened on 26 June 2026 and closed roughly forty hours later on 28 June, with the fix shipping the same day in version 16.24.3. Regression tests shipped alongside it, and further tests landed on the development branch in PR #40634, so it should not come back silently.
+What does IsADirectoryError Errno 21 mean in Frappe PDF generation?
It means the PDF code called frappe.read_file() on sites/<yoursite>/public, which is a folder, not a file. It happens because Chrome navigates to your bare site URL during PDF page setup, the stripped path is empty, and the loosened path-safety check wrongly approves it. The v16.24.3 fix adds an os.path.isfile() condition to that guard.
+Does Print Designer cause the ERPNext Chrome PDF timeout?
No. Print Designer ships its own fork of the Chromium stack that never took the change behind #40334, so it is structurally immune to it. Print Designer has a different Chrome problem: it reads the config key chromium_binary_path while frappe core reads chromium_path, so it fails instantly with "Chromium is not downloaded" instead of hanging. Set both keys. The upstream fix for that one is merged to develop but not in any tagged release.
+Will raising http_timeout fix the ERPNext PDF timeout?
No. The worker is blocked on a future that nothing will ever resolve, because the task that would resolve it has died. Raising http_timeout only makes users wait longer before seeing the same 504. If anything, lowering it recycles wedged workers faster. Note also that bench's default is 120 seconds, not the 60 that is often quoted — 60 is a common per-deployment override.
+What happens if we don't upgrade past v16.24.2?
Every Chrome-generated PDF stays broken — invoices, delivery notes, quotations, statements. Each attempt burns a gunicorn worker until gunicorn kills it, so concurrent PDF attempts progressively starve your web workers of capacity and slow the whole site, and each killed worker orphans a Chromium process that keeps consuming RAM. Staying on wkhtmltopdf indefinitely is possible but means living with an unmaintained renderer and visibly different output.
None of the eight Frappe forum threads above is a report of this specific regression — we looked, hard, and it is not there. The bug was filed on GitHub, patched in forty hours, and the loudest affected users escalated through Frappe Cloud rather than the forum. That is exactly why it is worth writing down properly: the fix is trivial, the diagnosis is not, and the internet's version of this story keeps merging it with a completely different bug that has not actually been released yet.
Stuck on a v16 upgrade that broke something you did not expect?
We run ERPNext and Frappe upgrades for teams that cannot afford a two-day outage — version-boundary checks, staged rollouts, and a rollback plan that has been tested rather than written down. If a patch release broke printing, invoicing or assets, we can get you back.