audunru/memory-usage
A lightweight Laravel package for monitoring memory usage in your application. It provides simple helpers to read current and peak memory consumption, making it easy to log, display, or debug resource usage with minimal setup.
Install and pin the fixed version. The package is a small composable observability add-on, so installation is one line — but pin the patch explicitly, because v5.1.0 produced inflated peak-memory figures whenever more than one matched request ran in the same process:
composer require audunru/memory-usage:^5.1.1
Publish the config, then read it end to end. Auto-discovery registers MemoryUsageServiceProvider, which conditionally pushes the two global middleware (LogMemoryUsage, LogSlowResponse) onto the HTTP kernel. The published file is the whole API surface:
php artisan vendor:publish --tag=memory-usage-config
Look at these three places first, in this order:
config/memory-usage.php — the enabled / slow_response_enabled flags and the paths rule array. Everything else is derived from this.src/Http/Middleware/LogMemoryUsage.php — note that handle() only sets the response header (and skips it for streamed responses) while terminate() does the logging.src/Support/MemoryHelper.php and TimeHelper.php — getPeakUsage() returns MiB as a float (bytes ÷ 1024 ÷ 1024); getResponseTime() is derived from REQUEST_TIME_FLOAT on the server bag. If you want ad-hoc numbers in tinker or an artisan command, call these directly.First use case — instrument one suspect endpoint in local. Enable the package, scope it to a single path rule, and switch on the response header for your dev environment:
// config/memory-usage.php (shape, not verbatim — read the published file)
'enabled' => true,
'header_name' => 'memory-usage',
'paths' => [
[
'patterns' => ['api/reports/*'],
'limit' => 10, // MiB
'slow_response_limit' => 2, // seconds
'channel' => 'stack',
'level' => 'warning',
'header' => ['environments' => ['local']],
],
],
Then hit the route and look at the memory-usage response header in the browser network tab or curl -i. That gives you a zero-dashboard feedback loop before you commit to any log pipeline.
Scope by path, not globally. The package is well-suited to targeted instrumentation and poorly suited to blanket capture across a high-QPS fleet, because log write volume scales linearly with request volume. Start with a rule for the handful of routes you suspect (api/reports/*, api/exports/*, PDF generation, large Eloquent eager-loads) and let ignore_patterns keep health checks, Horizon, and Telescope out of the log. There is a top-level ignore_patterns list that applies to both listeners, plus a per-rule ignore_patterns for finer control.
Use stacked rules for escalation instead of one threshold. Multiple path rules are evaluated independently per request, so the same pattern can produce a warning at 10 MiB to your default stack and an emergency at 100 MiB to a Slack channel. This is the strongest day-to-day pattern: cheap, always-on low-severity logging plus a high-severity signal that actually wakes someone up, without any extra code.
Bundle slow-response logging into the same rule. Both listeners consume the same paths array; limit drives memory logging and slow_response_limit drives slow-response logging. A rule with both set gives you a two-dimensional view of "expensive" — a route that's both slow and memory-hungry is a much better optimisation target than one that's merely slow.
Route to a dedicated channel, not the default stack. Point channel at a dedicated memory channel with daily rotation and JSON formatting so the lines ship cleanly to an aggregator and don't pollute application logs. Verify retention and rotation on that channel before enabling in production; log channel saturation is the realistic failure mode here.
Use the response header as a dev-time instrument only. Restrict header.environments to ['local'] (or a preview environment). It's the fastest way to eyeball a route's cost while refactoring, but it's a measurement artefact leaking into your API contract if it ships to production. Remember it is not injected for streamed responses.
Baseline before you compare. Establish a clean 5.1.1 baseline on a canary or staging environment, then compare release-over-release. Any numbers collected under 5.1.0 for a multi-matched-request setup are unusable — discard them rather than trying to normalise them. Only after the baseline is trustworthy should you build dashboards or thresholds.
Write feature tests that assert the log line. In your own app, $this->get(...) invokes Kernel::terminate(), so a normal feature test exercises the whole handle → send → terminate path. The package's own tests fake MemoryHelper, TimeHelper, and Log::channel() to assert channel, level, and message — mirror that approach to prove your config actually fires on the routes you care about. A test that hits a deliberately heavy route and asserts a warning was logged is your best protection against the silent-no-op failure mode.
Validate under your worker model. If you run Octane/Swoole or long-running queue workers, the process-scoped memory_get_peak_usage only gives a meaningful per-request number if the reset behaves correctly under worker reuse. Test it deliberately: fire the same matched request several times in one worker and confirm the reported figure is stable rather than monotonically climbing. This is a gating check, not a nice-to-have.
Never allow ^5.1.0. The 5.1.1 fix resets peak memory per matched request; in 5.1.0 peak accumulated across matched requests, so a route would appear to grow with each hit. This produces plausible numbers, which is the worst kind of wrong. Pin ^5.1.1 and treat any 5.1.0 output as void.
The enabled flags are read at boot. The service provider registers the middleware conditionally when it boots, so toggling enabled or slow_response_enabled at runtime — including in a test that configures it after the app has booted — has no effect. Run php artisan config:cache after config changes in any cached environment, and if you're writing a test around enabling the middleware dynamically, know that it will silently not work (the package has a commented-out test documenting exactly this).
A silent no-op is the most likely failure. Config not published, middleware not registered, or enabled false and you'll believe you have data when you have none. Smoke-test by hitting a known memory-heavy route and confirming a line lands on the expected channel.
Don't reimplement this with the Terminating event. The logging deliberately lives in middleware terminate() because the Terminating event carries no request/response pair and is also dispatched by the console kernel. Logging in terminate() also correctly includes streamed bodies sent after Kernel::handle() returns.
Units are easy to mix. limit is MiB, slow_response_limit is seconds, and getPeakUsage() returns MiB as a float. A limit of 1024 thinking it means bytes will never fire.
Peak memory is process-scoped and monotonic within a process. PHP does not reliably return freed memory to the OS, so "peak" is not "per-request cost". An engineer reading a high figure as the route's allocation cost will chase phantom regressions. Budget a short internal write-up defining what the metric is and isn't (allocator behaviour, opcache, boot cost, FPM vs. Octane semantics) — the ramp-up cost here is interpretive, not mechanical.
Middleware ordering and defensive wrapping. These are global middleware; verify they don't conflict with existing global middleware ordering, and confirm in staging that a logging failure cannot impact request handling. Also check config:cache works cleanly with your published config.
Overhead is small but nonzero, and storage is the real scaling vector. Compute cost is negligible; ingest and retention cost is not. Route matching and sampling are your levers. Keep the enabled scope tight and expand only with evidence.
Single-maintainer risk is real but cheap to mitigate. Assign an internal owner and note that the exit path is trivial — removing the middleware and the dependency leaves no data or schema behind. Acceptable for a diagnostic tool; not acceptable as a dependency of an on-call alerting path unless you own the wrapper and the thresholds.
Check overlap before adopting. If you already run Laravel Pulse or a commercial APM with memory graphs, this is largely duplicative — scope it to a targeted diagnostic rather than a permanent dependency. If you have no memory visibility at all, it's a credible, low-commitment gap-filler.
How can I help you explore Laravel packages today?