Practical comparison — Nginx Unit vs PHP‑FPM vs RoadRunner for PHP monoliths in containers — latency, throughput & memory

By · Published on · 9 min read

Laptop showing a metrics dashboard with charts, keyboard in foreground
Blurred screen with charts and metrics, suitable for content about latency, throughput and memory. Photo: Atlantic Ambience / Pexels

Comparison: Nginx Unit vs PHP‑FPM vs RoadRunner for running PHP monoliths in containers — latency, throughput and memory. Here you’ll find what actually changes in p50/p95/p99, RPS per core and RAM usage. The goal is to guide your choice with objective criteria and practical production-ready adjustments.

Methodology: what we measured and why it matters

Tail latency (p95/p99), sustainable throughput and memory per container are the indicators that most reliably expose production bottlenecks. Measuring these three points under different load patterns prevents surprises after deploy.

To isolate the runtime, use HTTP load with keep‑alive, warm up OPcache and preloading, and include the actual internal routing you use (for example, a proxy or ingress). Evaluate across concurrency ranges (for example, 25/50/100 connections) and collect p50/p95/p99, stable RPS and RAM usage reported by the cgroup.

In PHP monoliths, libraries, autoload and metadata caches weigh more on cold latency than the proxy itself. Runtimes with persistent workers tend to benefit heavy frameworks; request‑per‑process models help contain memory growth at the cost of more frequent cold starts.

For more nuance on Unit vs FPM (without RoadRunner), see the dedicated Unit × FPM comparison. To plan reproducible measurements, follow the load testing guide with k6.

  • Key metrics: p50/p95/p99, sustainable RPS, RAM usage by cgroup
  • Load patterns: concurrent connections, keep‑alive and arrival jitter
  • Warm the app: OPcache, preloading, framework caches
  • Environment: same base image, same extensions, same number of workers

Which has lower latency under load?

RoadRunner often shows lower tail latency (p95/p99) in apps with heavy framework bootstrap, because it keeps PHP workers warm between requests. Nginx Unit and PHP‑FPM approach RoadRunner when OPcache and preloading are well tuned and the pool minimizes cold starts.

In I/O‑bound workloads (fast cache/DB), p50 differences can be small. What changes is variance under spikes: short queues and persistent workers tend to stabilize p95, provided there is back‑pressure to avoid CPU saturation.

If the monolith does a lot of work per request (heavy serialization, validation), persistent context reduces repeated parse/autoload, favoring RoadRunner. For lightweight endpoints, the simplicity of FPM or Unit can tie on average latency.

Which delivers higher throughput per core?

When the bottleneck is user CPU (parsing, view rendering, serialization), persistent workers and efficient pooling usually yield more RPS per core. In those scenarios, RoadRunner often scales better per core than PHP‑FPM and Nginx Unit.

When I/O dominates (in‑RAM cache, low‑latency DB), throughput converges and the limiter becomes backend connections and network queues. Adjustments like pool sizes, CPU affinity and disabling synchronous logs matter more than the runtime choice.

For fairness, compare stable RPS (without drops) over minutes, not instant peaks. Also watch the RPS vs p95 relationship: throughput that grows at the cost of uncontrolled p95 is not sustainable.

Which uses less memory per request and per pod?

PHP‑FPM and Nginx Unit tend to have a lower baseline memory per process, with more predictable recovery between requests. RoadRunner keeps the app resident, which raises worker baseline but reduces repeated allocations on each call.

In large frameworks, RoadRunner’s per‑worker consumption rises because caches and singletons remain alive; control this by setting limits and recycling workers after N requests or based on memory usage. In FPM, pm.max_requests plays a similar role, reducing fragmentation.

Correct measurement in containers uses cgroup PSS/RSS, not the host RSS. Compare the sum of master + workers and normalize by stable RPS to estimate “MB per 100 RPS” under the same load profile.

  • RoadRunner: higher memory baseline; fewer repeated allocations
  • PHP‑FPM: lower to moderate baseline; recycling via pm.max_requests
  • Nginx Unit: app processes managed by Unit; profile similar to FPM

Architecture and process model of each runtime

All three run PHP, but with different models, which explains differences in latency, throughput and memory.

PHP‑FPM: a process manager (master) creates pools of workers and each request is served by a dedicated worker. This classic request‑per‑process model provides predictable isolation and configurable recycling.

Nginx Unit: a multiprotocol application server that manages PHP app processes directly, without FPM. Runtime configuration via API allows changing routes and processes without a full reload.

RoadRunner: a Go server that keeps PHP workers persistent and communicates with them via IPC. HTTP server, queues and jobs live in the same binary, reducing hops and keeping bootstrap warm.

  • Isolation: stronger in FPM/Unit; persistence: stronger in RoadRunner
  • Reload: FPM via signals; Unit via API; RoadRunner with graceful worker restarts
  • Observability: export queue/pool metrics to Prometheus where possible

Essential tuning in containers for consistent results

Some adjustments have high practical impact regardless of runtime. Apply them before concluding a comparison.

In PHP‑FPM: size pm.max_children according to the pod CPU/RAM limits; use pm = dynamic or static based on predictability; tune pm.max_requests to recycle workers and reduce fragmentation; handle slowlog and file limits. See the PHP‑FPM tuning service.

In Nginx Unit: set the number of app processes to match container CPU; use the API for zero‑downtime reconfiguration; disable verbose logs in production and use appropriate buffers.

In RoadRunner: fix workers per CPU; limit jobs per worker and set memory caps to prevent unbounded growth; implement back‑pressure when queues grow; separate access and app logs; test graceful restart behavior before deploy.

  • OPcache: enable, tune memory_consumption and revalidate_freq
  • Preloading: load framework hotpaths at startup
  • Kernel/container: use tini/PID 1, proper ulimits, TCP backlog
  • Proxy/Ingress: align buffers, timeouts and keep‑alive with the runtime

Running in production: health checks, logs and zero‑downtime deploys

All support zero‑downtime operation if health/readiness, logs and graceful reloads are correctly configured. Simple liveness and readiness checks that include DB/cache connections prevent serving traffic before warming up.

Implement readiness with an endpoint that validates dependencies and backend latency, as described in the health checks guide for PHP on Swarm. For deploys, canary or blue‑green strategies reduce risk; see the step‑by‑step blue‑green deploy with Docker Compose.

Synchronous logs reduce throughput; prefer STDOUT/STDERR with aggregation, structured formats and access sampling under high volume. Test reloads under load, measuring p95 during the swap to ensure no regressions.

  • Readiness: only become Ready after warming OPcache/preloading
  • Reload: test each runtime’s signal/strategy under real load
  • Logs: structure them and avoid synchronous flushes on hot paths

When to choose each one (objective criteria)

The decision depends on framework weight, CPU/I‑O profile and operational requirements. Use the criteria below as a shortcut and validate with load tests on your own code. This comparison focuses on containers for PHP monoliths and the practical effects on latency, throughput and memory.

For further reading on Unit vs FPM in containers, consult the dedicated Unit × FPM comparison.

Criterion Nginx Unit PHP‑FPM RoadRunner
Tail latency (heavy frameworks) Good, improves with OPcache/preloading Good, improves with OPcache/preloading Very good — persistent workers reduce cold starts
Throughput per core (CPU‑bound) Good, close to FPM with tuning Good, scales with pools and affinity Very good — benefits from warm bootstrap
Memory per worker Moderate — processes managed by Unit Low to moderate — recyclable via pm.max_requests Higher baseline; control via recycling/limits
Operation and reload Dynamic API, reconfigure without restart Signals to master, well‑established pattern Graceful in‑process worker restarts
Stack complexity Low: Unit can serve HTTP directly Low to medium: usually paired with Nginx/Ingress Medium: Go server + workers and orchestration
Ecosystem compatibility Broad, without FPM Maximum, supported by most guides Broad via PSR; watch out for stateful singletons
Where it shines Dynamic deploys and operational simplicity WordPress, stable monoliths, maturity Laravel/Symfony heavy apps, queues + HTTP in one binary

Frequently asked questions

Is RoadRunner compatible with any PHP framework?

Yes — RoadRunner serves applications that follow PSR (for example PSR‑7/PSR‑15). Popular frameworks usually work well, but review singletons and caches that hold state and configure worker recycling to contain memory growth over time.

Can Nginx Unit replace a reverse proxy like Nginx/Ingress?

It can serve HTTP directly and expose the application, reducing layers. In clusters you’ll still often use an Ingress Controller or edge proxy for TLS, rate limiting and global routing. Unit shines when you need runtime reconfiguration of routes/processes via API.

Is PHP‑FPM still the default choice for WordPress?

In most cases, yes. PHP‑FPM is widely supported by tutorials, plugins and hosting panels. It offers simple isolation by pool and predictable recycling. Combine it with OPcache and a well‑configured proxy for good performance and stability.

How do I measure p95/p99 without skewing results?

Warm OPcache/preloading, use keep‑alive and generate load with realistic jitter. Collect p50/p95/p99 over minute windows, not instantaneous spikes. Measure RAM via the cgroup and run the load generator close to the service to avoid network latency masking the runtime.

Can I reload without downtime on all three runtimes?

Yes, with their respective strategies: PHP‑FPM accepts signals to recycle pools, Nginx Unit reconfigures via API without restarting, and RoadRunner performs graceful worker restarts. Test under load to ensure swaps don’t increase p95 or drop connections.

Do persistent workers increase the risk of memory leaks?

There is a risk if the app accumulates state. Mitigate by recycling workers after a set number of requests or when memory limits are reached, and monitor heap growth. In FPM/Unit the request‑per‑process model already limits worker lifetime by default.

Want to choose between Nginx Unit, PHP‑FPM and RoadRunner based on your monolith?

We’ll design a reproducible load testing plan, tune the runtime and validate latency, throughput and memory before you deploy.

Discuss your PHP runtime

See also