---
name: evolution_pdf_render_throttle
description: "All wkhtmltopdf calls must go through library/pdfRender.php — flock slot pool, hard timeout, local header/footer; never exec wkhtmltopdf directly"
metadata: 
  node_type: memory
  type: project
  originSessionId: 088b0dfd-930a-4213-853f-ce04baeaea6c
  modified: 2026-07-28T03:58:44.932Z
---

**`evolution/library/pdfRender.php` (added 2026-07-27) is the only sanctioned way to run wkhtmltopdf.**
`PdfRender::render(['html'=>…, 'out'=>…, …])`. Never add a bare `exec("…wkhtmltopdf…")` — it bypasses
the host-wide cap and the pile-up comes back.

**The bug it fixes (non-obvious, worth not re-deriving):** `--header-html`/`--footer-html` were absolute
URLs back into our *own* Apache (`print.php:1741/1744` build `$url . $row->path`, `:2023` appends
`?type=&userdb=`). Each render held a PHP worker inside `exec()` while wkhtmltopdf made two more requests
that themselves needed PHP workers → self-deadlock under a print burst, no timeout, no cap, renderers at
~100-300 MB RSS each never exiting. Retry amplification via `printdiv.php` (`target="_blank"`, button
never disabled) closed the loop. Host-RAM contributor to [[mariadb_oom_investigation]] — does **not**
explain the unnamed ~9.6 GB allocator.

Key behaviours: flock slot pool (`$pdfMaxConcurrent`, default 4) → returns `error=>'busy'` rather than
queueing forever; `timeout -k 5 90`; same-host header/footer resolved to local files, `.php` templates
rendered in-process via `ob_start`+`include`, but **skipped if the template contains `die`/`exit`**
(it would kill the whole print). `exec($cmd,$out,$rc)` — the old `exec(...) or die(...)` was always
broken because `exec()` returns the last output line, not a success flag.

Gotchas:
- Callers must pass **raw** header/footer refs, not pre-wrapped `--header-html '…'` strings. Two spots in
  `print.php` (printPreview, and `emailReportInline` ~:2138) used to unpick the CLI string and were fixed
  to read the raw ref; check for more if new call sites appear.
- Disabling the print button during `onsubmit` would drop `$_REQUEST['printFormSubmit']` (read at
  `print.php:436` for logging) — the lock defers DOM changes with `setTimeout(…,0)`.
- `bin` option preserves a site's existing `/usr/local/bin/wkhtmltopdf.sh` wrapper (may set up xvfb).
- Tunables are plain globals read from `.env`; documented in `.env.dist`. `$pdfLocaliseTemplates=false`
  reverts to HTTP header/footer with no code change.
- `evolution/library/print.php` has a **pre-existing fatal parse error at :47** (`private $url = "$home";`)
  and is referenced by nothing — dead file, never loadable. Candidate for deletion, don't "fix" it.
- `cron/emailAlerts.php` + `cron/sendReminders.php` curl loops are **serial-blocking, not a fan-out** —
  the round-1 audit note calling them a simultaneous fan-out was wrong. Real hazard was no curl timeout.

**Status: committed to `staging` 2026-07-27.** Verified with a stubbed binary (21 assertions + concurrency
runs: cap holds 5-concurrent vs max=2, `busy` on queue expiry, rc 124 at the timeout, `.php` header renders
in-process). **Never smoke-tested against a real wkhtmltopdf** — no binary in the code-server container —
so the first dev print is still the real proof. If prints break after this lands, suspect header/footer
localisation first and set `$pdfLocaliseTemplates = false` in `.env` to revert that half without a deploy.
