CLOUD
development

FrankenPHP with Laravel in Production: One Binary Instead of Nginx Plus PHP-FPM

CLOUD|November 1, 2026|7 min read|24 views
FrankenPHP with Laravel in Production: One Binary Instead of Nginx Plus PHP-FPM

FrankenPHP merges the web server and PHP into one binary, and its worker mode keeps Laravel booted in memory so requests per second multiply several times, provided your code does not leak shared state between requests.

The short answer

FrankenPHP is a modern PHP application server built on Caddy, which is written in Go, with the PHP interpreter embedded directly inside it. Instead of the classic Nginx plus PHP-FPM pair, you run one executable that accepts HTTP, serves static files, executes PHP, obtains HTTPS certificates automatically, and speaks HTTP/2 and HTTP/3. That alone simplifies deployment, but the bigger win is worker mode: the Laravel application boots once and stays in memory, and later requests are served without reloading the framework every time.

With classic PHP-FPM every request goes through a full boot: loading the autoloader, reading configuration, registering service providers, which usually costs 15 to 40 milliseconds before a single line of your logic runs. Worker mode removes that phase, so requests per second rise roughly two to five times depending on the application. The one condition, and it is a serious one, is that your code must be free of shared state, because any static variable or singleton holding data from a previous request leaks into the next. The recommended route is Laravel Octane with the FrankenPHP driver, kept alive by a systemd service.

How it differs from Octane with Swoole and RoadRunner

Laravel Octane is the layer that runs Laravel in worker mode on top of one of three servers: Swoole, RoadRunner or FrankenPHP. All three deliver the same core idea, yet the day-to-day operational differences are real:

CriterionSwooleRoadRunnerFrankenPHP
InstallationPHP extension built via PECLGo binarySingle binary with PHP inside
Automatic HTTPSNo, needs a proxy in frontPossible with extra setupYes, built in from Caddy
HTTP/3NoLimitedYes
Static file servingWeak, Nginx preferredAcceptableStrong through Caddy
ExtrasConcurrent tasks, memory tablesMany Go pluginsEarly Hints, Mercure

Swoole is stronger when you need its specific features, such as concurrent execution inside a request, but it needs a proxy in front and an extension rebuild with every PHP upgrade. FrankenPHP is the simplest to operate: no Nginx, no Certbot, no PHP-FPM, just one process that systemd supervises.

Rough numbers against PHP-FPM

These are approximate figures for a Laravel endpoint that reads from the database and returns JSON, on a server with two cores and 4GB RAM. Real results depend on your logic and query weight:

SetupRequests per secondp95 latencyApplication memory
Nginx with PHP-FPM and OPcache350 to 500About 90msAbout 30MB per FPM process
FrankenPHP, classic mode400 to 550About 80msSimilar
FrankenPHP, worker mode1,500 to 2,500About 20msAbout 60MB per worker, constant

Classic mode gives no big jump, so the real gain comes from worker mode. The improvement also shrinks as requests get heavier: a page that spends 300ms in database queries will not get much faster, because the boot you eliminated was a small slice of its time. Lightweight APIs and applications with many short requests benefit most.

Automatic HTTPS, HTTP/3 and Early Hints

Because FrankenPHP is built on Caddy, it inherits complete certificate management: it requests a Let's Encrypt certificate on the first request for a domain, renews it before expiry, and redirects HTTP to HTTPS without a single line of configuration. That removes Certbot and its cron jobs, along with the most common cause of outages on small sites, an expired certificate nobody noticed.

HTTP/3 support means the browser connects over QUIC on UDP instead of TCP, cutting the connection handshake from two round trips to roughly one and coping better with packet loss on mobile networks. Open port 443 for UDP in the firewall, or browsers will silently fall back to HTTP/2. Early Hints send an early 103 response telling the browser which CSS and JavaScript files it will need before Laravel finishes building the page, so downloads start in parallel and content appears tens of milliseconds sooner on heavier pages.

What your code needs for worker mode

Under PHP-FPM each request starts with clean memory and ends by wiping it, so state management mistakes stay hidden. In worker mode memory survives between requests, and three problems surface:

Shared state

If you register a singleton that receives the Request object or the current user in its constructor, it will hold on to the first user's request and serve everyone after with it. That is a security bug rather than a performance bug, because one user may see another user's data. Inject the container itself, or call request() and auth() inside methods when you need them rather than in constructors. The same goes for static properties that accumulate data: they grow with every request and are never cleared.

Memory leaks

An array that gains entries on every request, or an event listener registered once per request, slowly raises memory until the worker falls over. The practical safeguard is --max-requests=500, which restarts each worker after 500 requests and clears any leak before it hurts. Watch process memory during the first week, and if it climbs in a straight line, find the source instead of relying on restarts.

Third-party packages

Most popular Laravel packages support Octane today, but older or in-house packages may keep state. Run your tests under Octane locally and try two users logging in alternately from different browsers, since that simple test exposes the most dangerous kind of leak within minutes.

Deploying as a systemd service

  • Install the package with composer require laravel/octane and then php artisan octane:install --server=frankenphp, which downloads the FrankenPHP binary automatically.
  • Create a unit file in /etc/systemd/system named laravel-octane.service running as www-data, with WorkingDirectory set to the application folder.
  • Make the start command php artisan octane:start --server=frankenphp --host=example.sa --port=443 --https --workers=4 --max-requests=500, where the https flag enables automatic Let's Encrypt certificates.
  • Add AmbientCapabilities=CAP_NET_BIND_SERVICE so an unprivileged user can listen on ports 80 and 443 without root, and Restart=always to recover from any crash.
  • In your deploy script, after pulling code and running migrations, run php artisan octane:reload to reload the workers with the new code without dropping open connections.

Start with twice the core count in workers, so 4 on a two-core server. Each worker holds its memory permanently, so budget for 4 workers at about 60MB each plus headroom for queues and the database. Do not raise the count without measuring, because extra workers compete for the same cores and add nothing.

Make sure ports 80 and 443 are open before the first launch and that the DNS record points at the server, since Caddy needs both to complete the Let's Encrypt challenge. After that it renews the certificate on its own about 30 days before expiry.

For logs, follow the service output with journalctl -u laravel-octane -f, where every request and every PHP error shows up instead of scattered Nginx files. Cap the journald log size so it never fills the disk, since a server handling 1,500 requests per second writes dense logs within hours, and send Laravel errors themselves to whatever alert channel your team watches.

Where it fits and where it does not

FrankenPHP needs full control of the server: systemd, ports and a custom binary. Its natural home is therefore a cloud server rather than managed hosting. On managed cloud hosting, PHP runs through the traditional setup already tuned alongside cPanel, which is plenty for most Laravel sites with moderate traffic.

For a mid-sized Laravel application with its database on the same server, the Growth plan (2 vCPU, 4GB RAM, 40GB SSD, 500GB transfer) at 6,900 SAR a year comfortably runs four workers alongside MySQL or PostgreSQL. If the API backs a mobile app with thousands of requests a minute, the Business plan (4 vCPU, 8GB RAM, 1TB transfer) at 11,500 SAR a year gives you 8 workers and comfortable headroom. Servers are in Riyadh at about 8ms, and resource details are on the Cloud Server page.

A practical migration checklist

  • Search the code for static properties and any singleton that takes the Request or the user in its constructor, and fix those first.
  • Run the application under Octane locally with --max-requests=500 and test the alternating two-user scenario.
  • Benchmark with a tool like wrk or k6 before and after on the same server. If the gain is under 50%, your requests are bound by the database rather than by boot time.
  • Deploy with a systemd service and octane:reload in the deploy script, and keep the old Nginx configuration for a week in case you need to roll back.
Share