Build1 publisher3 min readPublished
Laravel's bootstrap cost 316KB of the 0.66MB a warm hello-world request used
A dev.to benchmark on Laravel 13.31.0 and PHP 8.4.22 splits a request into framework bootstrap and loaded data. The much-quoted 20MB to 30MB per-request figure turns out to measure PHP with OPcache disabled.
The Engineer · Build desk
What happened
- A hello-world route in a fresh Laravel 13 app peaked at about 0.66MB of PHP memory, and 316KB of that was the framework's own share, meaning everything that runs before the controller method.
- Loading 10,000 Eloquent models in the same application cost 17MB, roughly 26 times the peak of the entire hello-world request.
- The post says the "20MB, maybe 30" figure people quote for a Laravel request is accurate, and that it is the value PHP reports when OPcache is disabled.
- OPcache is off in php artisan tinker, in the test suite and in the dev container, which are the places the measurement usually gets taken.
- From there the figure flows into pm.max_children calculations, memory_limit arguments and the "Laravel is heavy" thread, and the post says nobody checks it again.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- contradiction Both figures survive scrutiny, so a Laravel memory claim is only usable if it states whether OPcache was enabled where the reading was taken.
- constraint A per-process estimate 30 to 45 times the warm peak sets pm.max_children low, and the server then runs fewer workers than its RAM would support.
- decision A Lumen migration argued on per-request memory needs re-measuring on the real application with a warm OPcache, because the 316KB covers a fresh install's bootstrap only.
- cost At roughly 1.8KB per hydrated model, the row count the query returns decides the peak, and that count is set in the controller, in the choice between get() and a chunked read.
Three numbers describe one PHP request, and the dev.to post keeps them apart. memory_get_usage() is what the script has allocated through PHP's allocator at that moment. memory_get_usage(true) is what the allocator has taken from the operating system, whether the script is using it or not [10]. The third is the process footprint, read out of VmRSS in /proc/self/status inside the same controller method [12].
The PHP manual, quoted in the post, calls memory_get_usage(true) "the value that memory_limit is enforced against" and adds that the amount the operating system has given the process is a different and typically much larger number [11]. Which counter you read matters for capacity work. The allocator underneath is the Zend Memory Manager in Zend/zend_alloc.c, and it asks the OS for memory in fixed-size chunks [16], so the post reports peak and peak_real separately [15]. The 316KB belongs to the first of the three counters [2].
Both headline figures convert cleanly. MB in the post means 1,048,576 bytes, the unit memory_limit counts in [9], so 0.66MB is about 692,000 bytes and 316KB is 323,584 bytes. The framework is around 47 percent of the warm hello-world peak [1]. Set the same 323,584 bytes against the low end of the quoted range, 20MB or 20,971,520 bytes, and it comes to about 1.5 percent [2].
Data volume moves the number much further. 17MB for 10,000 Eloquent models works out at roughly 1.8KB per hydrated row [3][3], and the row is small: a SKU, a name, a 120-character description, a price, a quantity, a flag and two timestamps [13]. The items table holds 100,000 rows [13], so loading all of them at the same rate lands near 170MB [4]. The post says the change that moved its number by a lot was how the models got loaded, after before-and-after runs on php artisan optimize, the Composer class map, deferred providers and OPcache itself [18].
The warm figure holds where its conditions hold. OPcache is on and has compiled everything it is going to compile, and every reading is the third request to the route [8]. The application is a fresh Laravel 13 install, and the 316KB covers what runs before the controller in that install [1][2]; an app with a dozen package providers registering at boot is measuring its own bootstrap, not this one. The harness was a single static PHP-FPM worker in the official php:8.4-fpm-alpine image on an Apple silicon Mac, with MySQL 8.4.11 in a second container and requests driven by cgi-fcgi [7].
The author says years of working with Laravel and Lumen showed how differently the two behave on memory [19], and went in expecting the ratio to fall the other way [17].
What to watch
- A Lumen measurement on the same single-worker cgi-fcgi harness, which the post does not publish.
- The same breakdown for an application with a realistic package and service-provider load instead of a fresh install.
- The before-and-after figures for php artisan optimize, the Composer class map and deferred providers.