Skip to content
getnextpdf.com

Why your PDF engine belongs in PHP, not a sidecar

Spec: ISO/IEC 25010:2023, §3.7Spec: ISO 32000-2, §7

There are two places a PDF can be made: inside your PHP process, or somewhere else you have to operate. NextPDF makes it inside. This page is the case for that choice — why an in-process engine is usually the right default, and what the “somewhere else” pattern actually costs once it is in production.

This is the architecture angle, not the framework one. How the same engine reaches Laravel, Symfony, CodeIgniter, and standalone code is a different story, told in one engine, every framework.

A PDF feature rarely starts as a system you operate. It starts as a line in a controller: render this invoice, return that report. The sidecar pattern turns that line into infrastructure. To draw the document you now run a second thing — an external binary, a headless browser, a separate microservice — and everything that second thing needs becomes your problem too: its version, its memory, its container, its network, its failure modes, its on-call page at 2 a.m.

The cost is invisible on the demo and unavoidable in production. A document engine that lives in your process has none of it. The question is not “can a sidecar make a PDF” — of course it can. It is “what did you sign up to operate to get there, and did you need to.”

  • In-process means no second runtime. NextPDF draws the PDF inside the same PHP worker that handled the request. There is no subprocess to spawn, no service to deploy, and nothing extra to keep alive.
  • A sidecar adds an operational surface you did not have. A bundled browser or external binary brings its own version, its own security footprint, and its own container — all of which you now patch and monitor.
  • Process boundaries are where things go wrong. Cold starts, timeouts, brittle inter-process plumbing, and data leaving your process are failure modes that an in-process call simply does not have.
  • In-process is testable and deterministic. The engine is typed PHP you can unit-test, mock, and reason about — not an opaque renderer you can only probe by running it and looking at the output.
  • A real browser still has real uses. For pixel-faithful rendering of arbitrary modern web pages, a headless browser is the right tool — and NextPDF can delegate to one deliberately. It is a seam, not the default.

Hold the two architectures side by side. The in-process path is a function call. The sidecar path is a distributed system in miniature — and every arrow between its boxes is a place that fails independently of your code.

  1. In-process: call the enginewriteHtml() or the document API runs inside the current PHP worker — no subprocess, no socket.
  2. In-process: receive PDF bytesThe engine returns native PDF content directly; nothing left the process.
  3. Sidecar: serialize and shipMarkup or a request is marshalled out of your process to a binary, browser, or remote service.
  4. Sidecar: cross the boundaryA process spawn or network hop — with a cold start, a timeout, and an IPC contract that can break.
  5. Sidecar: run a second runtimeAn external renderer with its own version, memory profile, and security surface to operate and patch.
  6. Sidecar: deserialize backMarshal the result back in and translate the renderer’s errors into yours.
The in-process path versus the sidecar path. In-process, the PDF is produced by a typed call inside the same PHP worker and returned directly. The sidecar path adds a serialization step, a process or network boundary, an external runtime with its own version and footprint, and a deserialization step back — each a distinct failure mode the in-process call does not have.

No second runtime to operate. The sidecar pattern is two systems wearing the costume of one feature. A bundled wkhtmltopdf, a headless Chromium service, a separate render microservice — each is a runtime with its own release cadence and its own bugs. You inherit all of it. The in-process engine ships as a Composer dependency; it is upgraded the way every other library in your composer.json is, with no daemon, image, or socket added to your deployment.

Version drift and a wider security surface. A bundled browser is a large, fast-moving codebase with a steady stream of security advisories. Pin it and it rots; track it and it churns. Either way it is a renderer’s entire web platform sitting in your supply chain to feed one document. An in-process PHP engine is a focused library of code you can read; its security surface is the PHP you already run, not a second platform you also now run.

Data stays inside your process boundary. When you shell out, the document content — which is often exactly the sensitive data a PDF exists to carry — crosses a boundary. It is written to a pipe, an argument, a temp file, or a network socket to a service. Every one of those is a place to leak, to log by accident, or to leave behind. In-process, the data never leaves the worker that owns it. The blast radius is one process, not a fleet.

Brittle plumbing, cold starts, and timeouts. Inter-process and network calls fail in ways a function call cannot: the subprocess that did not start, the socket that hung, the timeout you guessed wrong, the cold start under a traffic spike. Each needs a retry policy, a circuit breaker, and a budget. An in-process render either returns bytes or throws a typed exception you catch on the next line. There is no partial network state to reconcile.

Observability and testing get harder across the boundary. A failure in a sidecar arrives as an exit code, a truncated log line, or a 500 from a service you do not control. Reproducing it means reproducing that whole environment. An in-process engine is observable with the tools you already use — a stack trace, a debugger, a profiler — and it is testable the way the rest of your PHP is. That testability is a named software-quality property: ISO/IEC 25010 places it under maintainability (Spec: ISO/IEC 25010:2023, §3.7), and an in-process library satisfies it far more directly than a renderer you can only exercise by launching it.

The PDF those tests assert on is a defined structure, not a black box. A PDF file has a specified object and file layout (Spec: ISO 32000-2, §7), and an in-process engine emits that structure from code you can read — so a golden-file or structural test checks bytes a known function produced, rather than the output of an external program you can only observe.

The whole point fits in a handful of lines. There is no client, no base URL, no health check, and no retry policy — because there is no second system.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
// The engine runs inside this very process. No subprocess is spawned,
// no socket is opened, and the report data never leaves the worker.
$document = Document::createStandalone();
$document->setTitle('Quarterly Report');
$document->addPage();
$html = <<<'HTML'
<h1 style="color: #1E3A8A;">Quarterly Report</h1>
<p>Rendered <strong>in-process</strong> by PHP — no browser, no sidecar.</p>
HTML;
$document->writeHtml($html);
// PDF bytes are returned directly. There is no boundary to marshal across,
// so there is no timeout, cold start, or deserialization step to handle.
$bytes = $document->getPdfData();

Contrast the shape of the sidecar version — not its code, its operational shape. It needs a binary or service to be installed and reachable, a request serialized and sent, a timeout chosen, a failure path for when the renderer is cold or down, and the result marshalled back. None of that is in the snippet above, because none of it exists when the engine is a library.

The frequent assumption is that “real” PDF rendering must mean a browser, so in-process must be the toy version. That has the trade-off backwards. A browser is the right tool when you need exact, pixel-faithful rendering of arbitrary modern web content. It is the wrong default for the document-shaped work most teams actually do — invoices, reports, statements, contracts — where the layout is known, the data is yours, and correctness is checked by a validator, not by eye. For that work, the operational weight of a sidecar buys you nothing the in-process engine does not already give you, and costs you everything in the sections above.

The mirror misconception is that an in-process engine renders “the whole web” like a browser. NextPDF’s in-process HTML pipeline is a specification-aligned subset focused on document layout, with documented boundaries — the scope is laid out in the HTML pipeline. When you genuinely need full browser fidelity, that is a deliberate, opt-in delegation, not a silent fallback.

In-process is the right default. Where a document genuinely requires exact rendering of arbitrary modern CSS that the in-process engine does not cover, delegating to a headless browser is the correct choice — and NextPDF supports that path deliberately, with its network access constrained, as a seam rather than the default. The two are not rivals; they are different tools for different jobs.

Exactly which HTML and CSS the in-process pipeline covers is defined by the engine’s code and its conformance tests, and is documented with that pipeline. “In-process” describes the default rendering path.

The capability surface stays simple: the in-process engine is Core, and the browser-delegation path is an optional extension, independent of edition.

Where the PDF is rendered — edition availability
EditionAvailability
CoreCore renders PDF in-process in PHP — no subprocess, binary, or sidecar by default.
ProThe headless-browser delegation path is an optional add-on extension, independent of edition tier.
EnterpriseThe headless-browser delegation path is an optional add-on extension, independent of edition tier.
  • The HTML pipeline — the scope of the in-process engine, and exactly when delegating to a browser is right.
  • One engine, every framework — the complementary axis: how the same in-process engine reaches every PHP framework without a different library per stack.
  • Operating NextPDF in production — what running an in-process engine looks like day to day, with no extra runtime to operate.
  • Memory and streaming — how the engine keeps in-process generation bounded under load.
  • In-process generation — producing the PDF inside the same PHP worker that handles the request, with no subprocess, socket, or external service.
  • Sidecar — a separate runtime that runs alongside your application to do one job; here, an external binary, headless browser, or microservice that renders the PDF outside your process.
  • Cold start — the latency and resource spike incurred when a subprocess or service must be started from nothing before it can serve the first request.
  • IPC — inter-process communication: the pipes, sockets, temp files, or network calls used to pass data to and from a separate process, and a recurring source of brittle, hard-to-debug failures.
  • Browser-delegation seam — the optional, opt-in path that hands a render to a headless browser for exact fidelity, with subresource network access blocked; a deliberate choice, not the default.