How do I stream 2-5 GB file downloads without blowing up PHP's memory?
Question
Users download 2-5 GB log archives as ZIPs. When I try to read the file with `file_get_contents()` or `Storage::get()` in PHP and return it, `memory_limit` is exceeded and the request blows up. How do I build something that pins memory at a few megabytes, reads the file from disk chunk-by-chunk, and pushes it straight to the HTTP response stream?
Answer
Short answer: file_get_contents/Storage::get pulls the WHOLE file into memory; for 2-5 GB you need to stream it — but better still, never pipe the bytes through PHP at all.
Short answer
The problem is conceptual: you’re treating an HTTP response as “build it all, then send it.” For a large file the right model is “read-emit-read-emit”; you transfer as you produce, and memory stays flat. I covered the decision to get files off the app’s own disk in the local-disk-to-S3 question.
Why
-
The memory limit isn’t a ceiling, it’s a consequence of the design. Raising
memory_limitturns a 5 GB file into a 5 GB RAM problem; until the model changes, the limit keeps coming back. -
Intermediate layers cancel the stream silently. Even with correct PHP, if FastCGI/Nginx buffering is on the file re-accumulates in memory.
-
The most scalable path is taking the app out of the data path. PHP authorizes, the web server or object storage carries the bytes.
What to do
-
Best option: keep the bytes out of the app entirely. If the file lives in S3/object storage, mint a short-lived
presigned URLand let the user download directly. If it’s on disk, let the web server serve it viaX-Accel-Redirect(Nginx) orX-Sendfile(Apache). -
If it must go through PHP, stream it chunk-by-chunk. Open the file with
Storage::readStream(), return aStreamedResponse(orresponse()->streamDownload), and inside the callback loop onfread, write, and callflush(). SetContent-LengthandContent-Disposition. -
Disable output buffering for that route. PHP’s
ob_*and Nginx’sfastcgi_buffering/proxy_bufferingmust be off; theX-Accel-Buffering: noheader tells Nginx “don’t buffer this.” -
With Octane, don’t hold the response in memory. In a persistent process, accumulating a giant response body in the worker means memory isn’t reclaimed between requests. Stream straight to the output.
Bottom line: I’d reach for a presigned URL or X-Sendfile/X-Accel-Redirect first — PHP authorizes, it doesn’t carry the bytes; cheapest and most scalable. If you genuinely have to serve through PHP, chunk it with readStream + StreamedResponse and don’t forget to disable output buffering. One rule: never load 5 GB into memory at once — not in PHP, not in the layer in front of it.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.