Optimize Laravel performance by using PSR-7 streams and PHP stream filters. Learn to decompress payloads on-the-fly to save memory and scale your API.
Last month, our API started choking on large JSON payloads from a legacy partner integration. We were seeing memory usage spikes hitting 400MB per request, which quickly exhausted our PHP-FPM workers and caused intermittent 502 errors. After some profiling, it became clear that loading the entire request body into memory before decompression was the culprit.
If you’re running high-throughput APIs, relying on standard file_get_contents('php://input') for compressed payloads is a recipe for disaster. To solve this, we moved to an on-the-fly streaming approach.
When you handle a GZIP or Brotli compressed request in a standard controller, Laravel (via Symfony’s HttpFoundation) reads the entire stream into a string. You then decompress that string. This doubles your memory footprint: you hold the compressed bytes and the expanded payload simultaneously.
For a 50MB request, you’re looking at significant overhead. If you've previously explored Laravel Octane Memory Management: Implementing Custom Object Pooling, you know that keeping memory usage flat is non-negotiable for stable workers.
Instead of reading the whole body, we can tap into the stream directly using php://filter. PHP allows us to append a filter to the input stream, which processes chunks of data as they arrive.
Here is a simple implementation of a custom middleware that wraps the input stream:
PHPnamespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; class DecompressRequestMiddleware { public function handle(Request $request, Closure $next) { if ($request->header('Content-Encoding') === 'gzip') { $stream = $request->getContent(true); #6A9955">// Get the raw stream stream_filter_append($stream, 'zlib.inflate', STREAM_FILTER_READ); #6A9955">// Re-bind the stream to the request $request->setContent(stream_get_contents($stream)); } return $next($request); } }
Wait—the code above still calls stream_get_contents at the end, which loads everything into memory. To truly optimize Laravel performance, you need to keep the stream open as long as possible. If you are handling large files, consider passing the resource directly to your service layer instead of casting it to a string.
Laravel’s request object is a wrapper around Symfony, but under the hood, we are dealing with PHP streams. By leveraging PSR-7 streams and PHP stream filters, we can process data in 8KB chunks. This is critical for request optimization.
When you treat the request body as a resource rather than a string, you can pipe it directly into a parser. This is the same logic we use when we handle large file uploads or stream data to S3. If you’re interested in how this architecture scales to other parts of your app, Laravel Serialization: Architecting Deterministic Payloads for High-Performance Queues covers similar ground regarding memory-efficient data handling.
We initially tried using a custom php://filter class to handle Brotli compression. It failed because the brotli extension wasn't consistently available across our staging and production environments.
Always check for the existence of the filter before applying it:
PHP$filters = stream_get_filters(); if (!in_array('zlib.inflate', $filters)) { throw new \RuntimeException('Zlib extension not loaded.'); }
Another issue? stream_filter_append can be tricky with read-only streams. If the underlying stream isn't seekable, you might find yourself unable to rewind the stream if a middleware further down the chain tries to access the body again. We solved this by using a php://temp stream to buffer the decompressed output if we absolutely needed to reset the pointer.
By moving the decompression logic out of the controller and into a stream wrapper, we reduced our peak memory usage by roughly 60% for large payloads. This isn't just about saving RAM; it’s about making your system's behavior predictable.
When your application is deterministic, you can calculate the maximum number of concurrent requests a worker can handle without triggering the OOM (Out of Memory) killer. If you're building high-scale systems, you should also look into Laravel Middleware for Deterministic Request Pre-flight Optimization to ensure your early-exit strategies don't leak memory.
Q: Can I use this for Brotli?
A: Yes, but you need the brotli PECL extension. The filter name is usually brotli.decompression.
Q: Does this work with JSON decoding?
A: You can use json_decode on a stream if you read it piece-by-piece, but it’s complex. Most of the time, streaming the decompressed content to a temporary file and decoding that is safer and fast enough.
Q: Will this slow down small requests? A: The overhead of registering a stream filter is negligible—usually under 0.1ms. The memory gains far outweigh the CPU cost.
I’m still experimenting with whether it’s better to use an external Nginx module for decompression versus doing it in PHP. Nginx decompression is faster, but doing it in PHP gives you more control over the payload validation before it hits your application logic. For now, the stream filter approach gives us the best balance of safety and performance.
Laravel PSR-7 middleware provides a robust way to handle request sanitization. Learn to build custom decorators for high-performance, deterministic validation.