Memory is the resource that doesn't degrade gracefully. A slow request is annoying; a request that crosses memory_limit is a hard 500 with Allowed memory size exhausted, and it tends to hit your biggest, most valuable customers — the ones with enough data to blow past the limit. The frustrating part is that it's invisible until it isn't: the route works in dev, works for most users, and falls over for the one account with 50,000 rows. Measuring peak memory per request is how you find those routes before a customer does.
Measuring peak memory per request
PHP exposes two functions:
memory_get_usage(true)— current memory allocated to the script, in bytesmemory_get_peak_usage(true)— high-water mark since script start, in bytes
Capture the peak in a terminate middleware — runs after the response is sent:
app/Http/Middleware/RecordMemoryUsage.php
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class RecordMemoryUsage
{
public function handle(Request $request, Closure $next)
{
return $next($request);
}
public function terminate(Request $request, $response): void
{
$peakMb = round(memory_get_peak_usage(true) / 1024 / 1024, 2);
if ($peakMb > 80) {
Log::warning('High-memory request', [
'route' => $request->route()?->uri(),
'method' => $request->method(),
'peak_mb' => $peakMb,
'user_id' => auth()->id(),
]);
}
}
}Common memory-heavy patterns
Unbounded eager loading
// Bad — materializes users × posts × comments × authors in memory
$users = User::with('posts.comments.author')->get();
// Better — paginate and process in chunks
User::with('posts.comments.author')->chunkById(100, function ($users) {
foreach ($users as $user) {
// ...
}
});
// Best — lazy loading if you don't need everything at once
foreach (User::cursor() as $user) {
// Loads one row at a time
}Reading large files into memory
// Bad — whole file in memory
$content = file_get_contents(storage_path('big-export.csv'));
// Better — stream line by line
$handle = fopen(storage_path('big-export.csv'), 'r');
while (($line = fgets($handle)) !== false) {
// ...
}
fclose($handle);
// Laravel wrapper — stream a response
return response()->stream(function () {
foreach (User::cursor() as $user) {
echo $user->id . "\n";
}
}, 200, ['Content-Type' => 'text/plain']);Blade rendering huge collections
@foreach (\$rows as \$row) in a Blade view with 50,000 rows renders every row to an in-memory string before sending the response. Paginate on the server side, not in Blade.
Memory is input-shaped
Unlike CPU time, memory usage tracks the size of the data a request touches, not the work it does. The same GET /export endpoint uses 12 MB for a small account and 400 MB for your biggest one, because the query returns fifty times more rows. That's why average memory for a route tells you almost nothing — it's dominated by the common small case and hides the rare large one that actually OOMs. Look at p95 and max peak memory per route, and watch the gap between them: a route at 20 MB p95 and 480 MB max has one input shape that's about to become a 500.
Finding your memory hot spots
Aggregate peak memory per route pattern and sort by p95. The routes at the top are where to focus. Typical targets to investigate:
- Any route with p95 peak > 64 MB
- Routes whose p95 peak has grown 2x+ over the last 30 days (data growth outpacing pagination)
- Routes that hit memory_limit occasionally (shows up as 500 errors with
Allowed memory size of X bytes exhausted)
Octane is different
If you're on Octane (Swoole, RoadRunner, FrankenPHP) the worker process persists across requests. Memory-per-request is less meaningful than memory growth over the worker's lifetime. See the Octane monitoring guide for the specifics.
THE EASY WAY
NightOwl records peak memory per request automatically
Every request recorded includes peak memory alongside route, duration, and queries. The requests dashboard aggregates by route with p95 memory per pattern — the hot spots surface themselves. No middleware to write.
composer require nightowl/agent
php artisan nightowl:installFrom $5/month flat. Data in your PostgreSQL.