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.
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.
- If it must go through PHP, stream it chunk-by-chunk. In Laravel, open the file with
Storage::readStream(), return aStreamedResponse(orresponse()->streamDownload), and inside the callback loop onfread, write, and callflush(). That way only one chunk (a few MB) sits in memory at a time. SetContent-LengthandContent-Dispositionso the browser shows the download dialog and a correct progress bar. - Disable output buffering for that route. This is the real trap: if PHP’s
ob_*or Nginx/FastCGI’sfastcgi_buffering/proxy_bufferingis on, the layer re-accumulates the file in memory and cancels your stream. Turn buffering off on that endpoint; theX-Accel-Buffering: noheader tells Nginx “don’t buffer this.” - With Octane, don’t hold the response in memory. In a persistent process (Octane/Swoole), accumulating a giant response body in the worker means memory isn’t reclaimed between requests and the process bloats. Stream straight to the output, don’t stash it in an intermediate variable.
- The best option: keep the bytes out of the app entirely. At scale, the right answer is to take PHP out of the data path. If the file lives in S3/object storage, mint a short-lived
presigned URLand let the user download directly from there. If it’s on disk, let the web server serve it viaX-Accel-Redirect(Nginx) orX-Sendfile(Apache); the app only checks authorization and returns the header.
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.