Anyone running Laravel Octane on FrankenPHP in production? Memory and singleton horror stories wanted
Ayesha Siddiqui@laravel_ayesha
We are moving one high traffic client API (about 4M requests a day, mostly reads, Redis cached) from php-fpm to Octane with FrankenPHP. Staging benchmarks look great, p95 from 48ms to 14ms. But I have done this long enough to know staging lies.
Setup: PHP 8.3, Laravel 11, Docker, 2 containers behind a load balancer.
Questions for people who actually run it in production:
- Memory growth: do you set
--max-requests, and to what? - Singletons holding request state: we already found one (a service that cached the current tenant in a property, so request 2 saw tenant 1, uff). How did you audit the rest?
- Packages that broke under Octane?
- Any reason to choose Swoole or RoadRunner over FrankenPHP today?
Real numbers welcome, opinions also welcome, "just use Go" not welcome 😄
Accepted answer
We've had Octane on FrankenPHP in prod since spring for an ecommerce API, about 2.5M requests a day, so a similar shape. What we learned:
1. --max-requests=500 to start. We went to 1000 after a month of watching memory. A PDF library leaked about 2MB per 100 requests, max-requests hides that nicely. Set a container memory limit as well, belt and braces.
2. Auditing singletons: grep every ->singleton( in your providers and ask "does this closure touch request(), auth(), session or anything tenant-ish". If yes, make it ->scoped( instead. Scoped bindings get reset between requests under Octane. Then grep app/ for static properties that aren't constants. That found two more for us.
3. Anything left over goes in the flush array in config/octane.php.
4. Write one test that sends two requests as two different tenants through the same app instance and asserts nothing bleeds. Cheap, and it caught a regression in July.
5. FrankenPHP over Swoole because there's no PHP extension to compile and the Docker image just works. You lose Octane::concurrently, which is Swoole only. We never missed it.
Staging lies, aye, but mostly about memory. Run it for a week at 10% of traffic behind the load balancer before you switch everything.
Get the best of #php_still_pays weekly
One email each Monday with the most useful posts. No spam; unsubscribe in one click.
Comments
Sign in to join the conversation.
Sign in to commentBayu Setiawan@bayu_laravel
Not production, but in staging we found a package that keeps the locale in a static property. Users saw random language. Very funny bug, client did not laugh
Accepted answer
Imran Hussain@imran_builds
We've had Octane on FrankenPHP in prod since spring for an ecommerce API, about 2.5M requests a day, so a similar shape. What we learned:
1. --max-requests=500 to start. We went to 1000 after a month of watching memory. A PDF library leaked about 2MB per 100 requests, max-requests hides that nicely. Set a container memory limit as well, belt and braces.
2. Auditing singletons: grep every ->singleton( in your providers and ask "does this closure touch request(), auth(), session or anything tenant-ish". If yes, make it ->scoped( instead. Scoped bindings get reset between requests under Octane. Then grep app/ for static properties that aren't constants. That found two more for us.
3. Anything left over goes in the flush array in config/octane.php.
4. Write one test that sends two requests as two different tenants through the same app instance and asserts nothing bleeds. Cheap, and it caught a regression in July.
5. FrankenPHP over Swoole because there's no PHP extension to compile and the Docker image just works. You lose Octane::concurrently, which is Swoole only. We never missed it.
Staging lies, aye, but mostly about memory. Run it for a week at 10% of traffic behind the load balancer before you switch everything.
Ayesha Siddiqui@laravel_ayesha
Imran this is gold, thank you. scoped is exactly the fix for our tenant service. the 10% behind the LB plan goes into the runbook today. 2MB per 100 requests from a PDF lib, I would never have guessed
Guillaume Mercier@guillaume_rale
Bon. We run FrankenPHP worker mode with Symfony, not Octane, same lessons. One more: anything that opens a connection in a constructor (Redis, SOAP client, LDAP) keeps it open across requests and dies when the server times it out. Reconnect on failure or create it lazily. Bref, long-running PHP means you think like a Go dev now.
Emeka Nwosu@emeka_golang
Too late, I saw it 😂 Welcome to long-running processes, PHP family. We have been waiting for you
Oluwatobi Adeyemi@tobi_freelance
Bookmarked for when I'm brave. My clients are happy with fpm and I'm happy when my clients are happy