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.
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.