skip to content

Would you move a mature PHP-FPM application to a worker-mode runtime such as FrankenPHP, RoadRunner or Swoole, and how would you decide and de-risk the rollout?

level: principalimportance: nice to knowfreq 14%

answer

  1. measure the bootstrap share first
  2. audit statics, singletons, exit
  3. dependencies must be reset-safe
  4. canary with conservative recycling
  5. keep the per-request path as fallback

basics

~20 s

Only when bootstrap is measurably a large share of request time and the code can be made reset-safe. Audit statics, singletons, exit and global settings, add two-user tests, then canary with conservative recycling and keep PHP-FPM as the fallback.

solid answer

~50 s

It depends on where the time goes and how the code keeps state. First measure: if bootstrap is a large slice of each request and traffic is high, worker mode can cut latency and servers; if the database dominates, the gain is small. Then estimate the cost: audit static properties, singletons holding request data, `exit` calls, shutdown functions, global settings and third-party libraries that assume a fresh process; check the framework's worker-mode support. If it is worth it, roll out gradually: add two-request regression tests with different users, run a canary on a small share of traffic with a low per-worker request limit, watch memory per worker, error rates and cross-user anomalies, then raise limits step by step. Keep the PHP-FPM deployment ready as the fallback, and move endpoints that cannot be made safe last or never.

go deeper

for a junior

Recall that worker mode trades per-request bootstrap for a long-lived process, and that state kept between requests is its main risk.

for a middle

Explain which parts of a code base block a move: statics holding request data, exit, shutdown functions and global settings.

for a senior

Plan the verification: two-request tests, reset checks, memory metrics per worker, and a canary with conservative recycling limits.

for a principal

Own the go/no-go: quantify the bootstrap gain against audit and operational cost, pick the runtime, and keep a routing-level fallback to PHP-FPM.

## The question behind the question Interviewers asking this want to hear a **decision process**, not a verdict. Worker mode (FrankenPHP, RoadRunner, Swoole) trades a per-request bootstrap for a long-lived process, and the trade is only good when the saved time is large and the new failure modes are manageable. A lead is expected to measure both before committing. ## Step 1: is there enough to gain? - **Bootstrap share**: profile a typical request. If loading configuration, building the container and resolving routes take a large fraction of the response time, worker mode removes that fraction. If most time is spent waiting on the database or remote APIs, the saving is modest. - **Traffic volume**: at high request rates, lower CPU per request turns into fewer servers; at low traffic the saving may not justify the work. - **Features that need a long-lived process**: WebSockets, server-sent events or in-process scheduling can make a runtime attractive on their own. ## Step 2: what will it cost? | Area | What to check | |---|---| | Application code | static properties and `static` variables holding request data; singletons with request fields; `??=` memoisation | | Request-end assumptions | `exit`/`die`, `register_shutdown_function()`, destructors used as hooks | | Global settings | `ini_set()`, `setlocale()`, `date_default_timezone_set()`, error handlers changed per request | | Resources | long-lived database connections dropped by the server; transactions left open after failures | | Dependencies | libraries that cache per-request data statically or assume a fresh process | | Framework | official worker-mode support and its service-reset mechanism | | Runtime choice | FrankenPHP and RoadRunner keep familiar blocking PHP; Swoole's coroutines add in-worker concurrency and a stricter compatibility bar | A code base with pervasive globals, or a dependency tree full of static caches, can make the audit bigger than the gain. That is a legitimate reason to stay share-nothing. ## Choosing the runtime The three runtimes differ in how much they ask of the code: - **FrankenPHP** keeps the familiar SAPI model inside the worker loop (superglobals, `header()`, `echo`), so the move is mostly about state, not APIs. - **RoadRunner** runs PHP as CLI worker processes and hands requests over as objects, so code must build responses through that request/response API instead of `header()` and `echo`. - **Swoole** adds coroutines that interleave requests inside one worker; that raises throughput potential but also means two requests can share state at the same moment, a stricter bar than sequential reuse. ## Step 3: make regressions visible 1. Add **two-request tests**: boot the app once, handle a request as user A, then as user B, and assert nothing from A appears. 2. Add **reset checks**: after each test request, compare global settings and output-buffer level with their boot values. 3. Add **metrics per worker**: memory after each request, request count at recycle, restarts and fatal errors. ## Step 4: roll out gradually 1. Deploy the worker-mode service **beside** the existing PHP-FPM one. 2. Route a **small canary share** of traffic, preferably read-heavy endpoints first. 3. Start with a **low per-worker request limit** so any leak is bounded, then raise it as memory curves prove flat. 4. Watch error rates, latency, memory per worker and any report of users seeing foreign data; one such report is a stop signal. 5. Expand endpoint by endpoint; keep endpoints that cannot be made safe on the per-request pool. ## Step 5: keep a way back The PHP-FPM deployment stays runnable until the new one has carried full traffic for a meaningful period. Because the application code is the same, the fallback is a routing change, not a rollback of code. ## What a strong answer sounds like - It **quantifies** the gain before arguing for the change. - It names the **new failure class**, cross-request state, and a concrete way to detect it. - It is willing to conclude **"not worth it"** for a database-bound or legacy code base. - It treats recycling limits and the FPM fallback as **safety nets**, not fixes.

  • Your profile shows bootstrap is 8 % of a typical request. What would you recommend?
    Probably not to switch for performance alone. Removing 8 % of latency rarely justifies auditing the whole code base for cross-request state and running a new runtime. I would look first at the dominant costs, usually queries and remote calls, and revisit worker mode only if a feature such as WebSockets needs a long-lived process.
  • Which single signal would make you halt the canary immediately?
    Any credible report or log evidence of a user seeing another user's data. Latency and memory problems degrade service, but a cross-user leak is a confidentiality incident; route the canary traffic back to PHP-FPM, then find the leaking state with a two-request reproduction.

saying these in an interview costs you the question

  • Worker mode is always faster, so every PHP app should switch
  • A passing test suite proves the app is worker-safe
  • Recycling workers makes a state audit unnecessary
  • Switching runtimes needs no rollback plan since code is unchanged
  • Swoole, RoadRunner and FrankenPHP have identical compatibility constraints