Sitemap

FrankenPHP vs PHP-FPM (Part 3): CPU, Memory, and the Hidden Cost of Doing Nothing

11 min readAug 3, 2025

In Part 1, we compared FrankenPHP and PHP-FPM using raw “Hello World”-style benchmarks. Even in those simple cases, FrankenPHP surprised us with significantly faster responses.

Part 2 raised the stakes: real Symfony apps, full service autowiring, controllers, middleware, and larger payloads. Once again, FrankenPHP — especially in worker mode — outperformed FPM under load.

But raw performance isn’t the full story. What happens between requests? When no one is actively using your site, what’s going on under the hood? What are the hidden costs of just keeping things online?

In this third part, we explore an often-overlooked angle:
Memory, CPU usage, and idle behavior over time.

Press enter or click to view image in full size

Let’s Start: Why This Test Matters

For many production apps, traffic isn’t constant. You might have spikes in the morning and evening, followed by long quiet periods. If your server holds onto memory unnecessarily or spins the CPU even when idle, those hidden inefficiencies add up.

In large-scale environments, this matters for server consolidation and cost optimization. In smaller projects or VPS setups, it can be the difference between stability and memory exhaustion.

So we asked:

  • How much memory and CPU do these runtimes use while serving traffic?
  • What about when they’re idle?
  • Do any of them “calm down” when the load drops, or do they keep holding onto everything?

Test Setup: Minimal Resources, Realistic Pressure

To expose these behaviors clearly, we constrained our environment intentionally.

  • Each container was limited to 2 CPUs and 1 GB RAM, simulating a cheap VPS or small Docker host.
  • The same heavy Symfony app from Part 2 was used for all tests.
  • Three scenarios were tested:
    -
    PHP-FPM (classic setup)
    - FrankenPHP without worker mode
    - FrankenPHP with native worker mode enabled

These were not cloud load tests. Everything ran inside one AWS VM, but tools like wrk and psrecord were run locally, without external traffic or proxies interfering.

We used:

  • wrk for HTTP stress testing (multiple threads, high concurrency)
  • psrecord to monitor per-process CPU and memory usage, producing time-based graphs
  • Additional observations using Symfony profiler and various other logs

The goal wasn’t to simulate traffic from millions of users — but rather to understand how each engine behaves under repeated pressure and idle recovery on constrained hardware.

You’ll see wrk stats and psrecordcharts throughout this article — not because it’s the only tool we used, but because it’s one of the easiest ways to make performance differences visually obvious.

Behind the scenes? Let’s just say wrk was the tip of the iceberg. We also ran tests using wrk2, k6, and Blackfire, experimented with various durations (some lasting hours), and hit the servers with both normal and pathological loads.

The results you see here are the cleanest snapshots of those experiments — but the trends were consistent no matter what we threw at them.

Load Tests: Memory and CPU Under Pressure

When placed under sustained traffic, PHP-FPM and FrankenPHP (in worker mode) behave fundamentally differently — not only in terms of raw performance, but in how they consume CPU and memory. This section explores what those differences look like in practice.

How PHP-FPM Handles Load

PHP-FPM (FastCGI Process Manager) manages a pool of worker processes, each running in isolation. For every incoming request:

  • A process is assigned (or created dynamically).
  • The full Symfony kernel is bootstrapped.
  • Services are instantiated.
  • The request is handled, and the response is returned.

Depending on your FPM configuration (dynamic, ondemand, or static), these processes may stick around for a few more requests — but they’re short-lived by design. Crucially, every single request goes through the full boot cycle.

This architecture provides strong stability: memory leaks are less likely to accumulate, and rogue requests are isolated by design. However, it comes at a cost — each request incurs startup overhead and burns CPU just to get Symfony up and running.

PHP-FPM Load Test Setup

We simulated steady traffic using:

wrk -t4 -c100 -d90s localhost:8080

That’s 4 threads and 100 concurrent connections, sustained over 90 seconds.

PHP-FPM Results

Here’s what we observed during the load test (container-level metrics):

  • Peak memory usage: ~25 MB
  • Average CPU usage: ~150–200%
  • Average latency: 416 ms
  • Throughput: ~240 requests/sec

Interpretation:
FPM handled the load reliably. Memory stayed stable — thanks to the isolated nature of FPM workers — and CPU was high but not extreme. However, the average latency was substantial, and throughput was modest. Much of the performance cost goes into bootstrapping the framework again and again.

FrankenPHP (Without Worker): Minimalist, But Still Costly?

You might expect FrankenPHP — when running without worker mode — to behave similarly to FPM. After all, in this mode, FrankenPHP spins up a fresh Symfony application per request, just like PHP-FPM does.

However, in practice, things are slightly different.

Despite the similarity in architecture, FrankenPHP still keeps more memory in use over time. This may be due to how it internally manages the HTTP stack (since it’s also acting as a full web server), or differences in how processes are spawned and reused under the hood.

How FrankenPHP (Without Worker) Handles Load

When not using worker mode, FrankenPHP behaves more like a traditional PHP-FPM stack. For every request:

  • A new PHP process is forked (internally by the FrankenPHP binary).
  • The Symfony kernel is booted fresh.
  • Services are rebuilt.
  • The response is served.

Unlike worker mode, there is no in-memory kernel reuse. Each request starts from scratch — similar to FPM — but handled inside the FrankenPHP binary without FastCGI overhead.

This mode benefits from simplicity and better compatibility with legacy workflows. However, it loses most of the performance and resource advantages of FrankenPHP’s full worker setup.

FrankenPHP (No Worker) Load Test Setup

We used the same test parameters for a fair comparison:

wrk -t4 -c100 -d90s http://localhost:8081

FrankenPHP was explicitly run without worker mode enabled, so each request was bootstrapped from zero.

FrankenPHP (No Worker) Results

During the 90-second test, container-level monitoring showed:

  • Peak memory usage: ~110 MB
  • Average CPU usage: ~200%
  • Average latency: ~324 ms
  • Throughput: ~310 requests/sec

Interpretation:
Despite not using workers, FrankenPHP handled significantly more requests than FPM — with lower latency and better throughput. However, this came at a price: higher CPU and much higher memory usage.

Unlike FPM, which isolates workers and tends to release memory quickly, FrankenPHP’s monolithic process retains significantly more memory between requests — even when worker mode is turned off. This suggests that internal optimizations (like static data, caching layers, or Symfony bootstrapping) are handled differently under the hood.

And that leads to a surprising question:

If FrankenPHP without workers behaves similarly to FPM in how it boots the framework per request…
Why does it consume so much more memory and CPU over time?

The answer lies in how the two execution models are architected.

PHP-FPM uses a traditional FastCGI model where incoming requests are handled by a pool of isolated worker processes. Depending on the configuration (dynamic, ondemand, static), a worker may either be spawned fresh or reused for a few requests before being recycled. In either case, memory is frequently released back to the OS — either when the worker dies or when its lifecycle ends.

FrankenPHP, on the other hand, operates as a monolithic HTTP server. Even with workers disabled, it does not fork a new process per request. Instead, all requests are handled inside a single long-running process — similar to how Go or Node.js servers operate.

In short:

FPM behaves like a script runner. FrankenPHP behaves like a server.

Even in non-worker mode, FrankenPHP doesn’t reset the way FPM does — and that architectural distinction explains the higher resource usage.

How FrankenPHP Handles Load (Worker Enabled)

When running in its native worker mode, FrankenPHP unlocks one of its biggest advantages: persistent, warm application memory across requests.

Instead of bootstrapping Symfony from scratch every time, each worker:

  • Boots the Symfony kernel once.
  • Holds it in memory between requests.
  • Serves subsequent requests from that already-initialized state.

This approach is similar to what RoadRunner, Swoole, and other persistent PHP runtimes aim to do — but FrankenPHP integrates it directly into the HTTP layer, without extra dependencies or side processes.

This makes a huge difference in performance… but what about resource usage?

FrankenPHP (Worker Mode) Load Test Setup

We used the same load scenario as in previous tests:

wrk -t4 -c100 -d90s http://localhost:8081

This hit the server with 4 threads, 100 concurrent connections, over 90 seconds of continuous pressure — identical to the PHP-FPM scenario.

FrankenPHP (Worker Mode) Results

From the 2-minute stress test:

  • Peak memory usage: ~100–120 MB
  • Average CPU usage: ~200% (2x100%)
  • Average latency: ~140 ms
  • Throughput: ~711 requests/sec

Interpretation:

This is where FrankenPHP truly shines:

  • It’s almost 3× faster than PHP-FPM in raw throughput.
  • Latency drops by more than 60% compared to FPM.
  • Even with more requests, memory usage stays modest and predictable.

Why is this possible? Because worker mode allows FrankenPHP to:

  • Reuse the Symfony kernel across requests.
  • Keep services, routes, and caches warm and ready.
  • Avoid constant reboots or unnecessary I/O.

While the memory usage looks similar to the non-worker version in charts, the difference is architectural:

  • Non-worker mode still bootstraps the app each time.
  • Worker mode keeps the full app alive and ready — which explains the huge performance gains without any extra RAM cost.

In many ways, this is what people wanted PHP to become for modern web workloads: fast, persistent, predictable — and efficient even under stress.

So those are the under-load behaviors. But how do they behave when the traffic stops? That’s what we tested next.

Idle State: What Happens When Nobody’s Home?

Performance under load is important — but in reality, most servers spend the majority of their time waiting. During nights, weekends, or periods of inactivity, what happens to system resources?

Do memory and CPU usage drop to near-zero? Or do some engines keep “holding on” to more than they should?

This section explores how PHP-FPM, FrankenPHP without worker mode, and FrankenPHP with worker mode behave when left completely idle — right after boot, with no requests sent.

All containers were launched fresh, with the same resource constraints (2 CPUs, 1 GB RAM), and allowed to settle before recording.

PHP-FPM in Idle

As expected, PHP-FPM uses very few resources when idle:

  • Peak memory usage: ~20–25 MB
  • CPU usage: < 1%
  • Processes are forked, but sit in sleep mode unless invoked.

PHP-FPM is Ideal for bursty or infrequent workloads, like cron-driven tasks, legacy CMS sites, or low-traffic WordPress installs. Resource usage is minimal when inactive.

This also applies where your current architecture is doing great job

FrankenPHP (No Worker Mode) in Idle

FrankenPHP without workers also stays relatively modest when idle:

  • Peak memory usage: ~60 MB
  • CPU usage: ~1–3% (occasionally spikes, probably some internal checks)
  • The Symfony app isn’t persistently running, but the server binary itself is more complex and likely holds shared resources.

This may not seem like much, but compared to FPM, it’s 2–3× higher memory usage, even when doing nothing.

This hints that even without workers, FrankenPHP keeps more in memory — likely for internal routing, precompiled logic, or optimization layers.

FrankenPHP (With Worker Mode) in Idle

Once the worker mode is enabled, FrankenPHP essentially runs as a long-lived application. As a result:

  • Peak memory usage: ~85 MB
  • CPU usage: ~1-3% idle (occasionally spikes, probably some internal checks)
  • The Symfony kernel remains fully loaded and “hot” in memory.

This is the cost of keeping your app always ready — zero boot time, but a persistent memory footprint.

This is great for modern apps, APIs, or real-time systems. But it could be overkill for static sites or admin dashboards used once a week.

Which One Should You Use and When?

After weeks of testing, experiments, Docker tweaks, and way too many wrk wrk2, Blackfire profiles, k6… here’s where I landed:

There’s no silver bullet. But there is a pattern.

Use PHP-FPM if:

  • Your application receives low or sporadic traffic (e.g. admin dashboards, internal tools, small WordPress sites).
  • You host multiple small projects on the same server, most of which are idle most of the time.
  • You prefer stability and isolation over raw performance.
  • You already have infrastructure built around Nginx/Apache + PHP-FPM and don’t need major changes.

Use FrankenPHP (especially with workers) if:

  • You’re running high-traffic applications or APIs that receive constant traffic.
  • You want lower latency and higher throughput per container or per VM.
  • You benefit from built-in server capabilities (like static file serving, Caddy integrations, HTTP/2/3).
  • You’re building modern, containerized applications and want to reduce server complexity.

The tradeoff is clear:

  • FrankenPHP delivers performance — but retains memory and uses CPU more aggressively.
  • PHP-FPM saves resources when idle — but pays the price on every single request.

Choose based on your workload, not your instincts.

Cost Efficiency (Rough Estimation)

While performance benchmarks are insightful, it’s often raw cost efficiency that matters most.

The chart below shows a very rough approximation based on our own internal testing. It assumes a pricing model reflecting compute usage under load.

FrankenPHP starts cheaper and scales more gracefully as traffic increases. FPM might be more cost-effective if your apps sit mostly idle (e.g., WordPress admin sites, legacy dashboards, or infrequent APIs), but beyond a few thousand requests per hour, FrankenPHP becomes the financially smarter choice.

If you’re building for scale, here’s a rough cost model based on hourly request volumes — it shows where FPM starts costing more due to scaling inefficiencies.

Press enter or click to view image in full size
Very rough estimation, but the point stays, more trafic, more reasons to go on FrankenPHP

To handle the same amount of requests, PHP-FPM needs more machines, more memory, and more CPU — because every request is “expensive”.

When you realize FrankenPHP with workers saves more money in cloud costs
than your entire dev team’s coffee budget

One Final Note on Long-Term Behavior

While these tests focused on short-term benchmarks, real-world apps live for weeks or months — and this is where FrankenPHP’s design becomes especially interesting.

FrankenPHP offers:

  • The ability to auto-restart worker processes after X requests or Y time
  • The ability to restart worker processes via an API call
  • The ability to restart worker processes after X failed requests
  • Memory usage limits and crash recovery for rogue requests
  • Options to fine-tune how and when resources are released
  • A smaller surface for memory leaks to hide, compared to traditional FPM pools

But here’s the catch:

These features only help if you use them.
And long-term stability still depends on your app, your framework, and your code.

So if you’re running long-lived containers or processes, don’t just benchmark — monitor. Use Blackfire, Prometheus, Grafana, etc. to catch issues before they cost you money or uptime.

--

--

Ivan Vulovic
Ivan Vulovic

Written by Ivan Vulovic

Over 20 years of programming experience mixed with over 5 years in leading various teams in various tech stacks

Responses (7)