Laravel Octane observability requires custom OpenTelemetry exporters to track PHP memory management. Learn to detect leaks in long-running workers effectively.
Last month, our primary worker pool started hitting memory limits around 4:00 AM every day. We were running Laravel Octane on PHP 8.3, and despite our best efforts to monitor standard metrics, the "sawtooth" pattern of memory growth remained invisible until the OOM killer stepped in. We needed more than just standard uptime alerts; we needed a way to correlate specific request lifecycle stages with heap consumption.
When you're running long-lived processes, standard memory_get_usage() calls aren't enough. We initially tried wrapping our request lifecycle in a simple middleware that logged memory usage to a file. It was a disaster. The overhead of writing to disk for every request added about 12ms of latency, and the log files grew so fast they filled the partition within hours.
We realized that for true Laravel Octane observability, we needed to treat memory as a first-class metric within our existing tracing pipeline.
If you've already implemented Laravel OpenTelemetry instrumentation: a practical guide, you're likely capturing spans for database queries and external API calls. However, OpenTelemetry’s default PHP SDK doesn't natively export internal Zend VM engine metrics.
By writing a custom exporter, we can hook into the worker's tick-based lifecycle. Instead of sampling every request, we sample at the end of the RequestTerminated event, pushing the delta of memory usage directly to our collector.
To get this working, you’ll need the opentelemetry/sdk package. We create a custom SpanExporter that intercepts the memory footprint before the worker resets the state.
PHPnamespace App\Observability; use OpenTelemetry\SDK\Trace\SpanExporterInterface; use OpenTelemetry\SDK\Trace\SpanDataInterface; class MemoryLeakExporter implements SpanExporterInterface { public function export(iterable $batch): bool { foreach ($batch as $span) { $memory = memory_get_usage(true); #6A9955">// Push to collector via OTLP/gRPC $this->reportToCollector('php.memory.usage', $memory); } return true; } }
This approach allows us to track PHP memory management trends without adding significant overhead. Since we’re only reporting at the end of the request, the performance impact is negligible—roughly 0.8ms per request in our staging environment.
Integrating this into your worker lifecycle is the final piece of the puzzle. You need to ensure that your observability container is resolved once and reused across requests. If you're struggling with object persistence, I’d suggest reviewing our previous work on Laravel Octane memory management: implementing custom object pooling to keep your container clean.
Once you have your exporter set up, you can start correlating memory spikes with specific routes. If you see a climb that doesn't reset, you're likely dealing with a circular reference. In those cases, I often look back at Laravel Octane memory management: solving circular reference leaks to ensure my services are being properly destructed.
Once your custom exporter is live, you need to visualize the data. We use a Grafana dashboard to track the delta between the start and end of a request. If the delta is consistently positive over a 10-minute window, we trigger an alert.
Here is what we look for:
gc_collect_cycles() calls increasing in frequency?If you find that your memory usage is drifting even with perfect code, check your opcache settings. Sometimes, tuning your environment is just as important as the code itself; we’ve had success using Laravel Octane JIT: tuning PHP performance for deterministic results to stabilize the VM's behavior.
This isn't a silver bullet. We still occasionally find edge cases where a third-party package holds onto a static reference we didn't account for. However, by moving memory tracking into our OpenTelemetry pipeline, we’ve shifted from "guessing why the worker died" to "knowing exactly which endpoint triggered the leak."
Next time, I want to experiment with using Xdebug in production-like environments to generate automated flame graphs when memory usage crosses a threshold. It’s a bit risky, but the data would be invaluable. For now, our custom exporter is keeping the workers stable, and that’s a win in my book.
Laravel Octane performance profiling is essential for stable production. Learn to implement custom Xdebug-based flame graphs to debug long-running worker latency.