In Laravel, why can Storage::put($path, file_get_contents($tmp)) exhaust memory on a 2 GB scan archive, and how do putFile, putFileAs and streams avoid it?
answer
- a string holds every byte
- put() with a resource calls writeStream
- putFile returns a path, random 40-char name
- putFileAs lets you choose the name
- copyToDisk streams across disks (13.32)
basics
~20 sPassing a string means the whole 2 GB file sits in PHP memory. putFile and putFileAs open the file as a stream and write it chunk by chunk; put() with a resource, readStream()/writeStream() and copyToDisk() stream the same way.
solid answer
~40 s`file_get_contents()` loads the entire file into one PHP string, so a 2 GB archive needs at least 2 GB of memory before `put()` even starts, far beyond a typical `memory_limit`. `put()` streams when it receives a resource or a PSR-7 stream: it calls Flysystem's `writeStream()` instead of `write()`. `putFile($dir, $file)` and `putFileAs($dir, $file, $name)` accept an `Illuminate\Http\File` or `UploadedFile`, open it with `fopen(..., 'r')`, stream it and close the handle; `putFile` names the file with `hashName()` (40 random characters plus a guessed extension) and both return the stored path, or `false` on failure. For disk-to-disk moves, `readStream()` plus `writeStream()` or, since Laravel 13.32, `copyToDisk()` and `moveToDisk()` stream without buffering; `get()` is the call to avoid for large files.
code
php · 16 lines<?php
use Illuminate\Http\File;
use Illuminate\Support\Facades\Storage;
$archive = new File(storage_path('app/private/tmp/scans-2026-09-29.zip'));
// Streams the file; returns 'archives/scans-2026-09-29.zip' or false
$path = Storage::disk('s3')->putFileAs('archives', $archive, 'scans-2026-09-29.zip');
if ($path === false) {
throw new RuntimeException('Archive upload failed');
}
// Disk to disk without buffering (Laravel 13.32+)
Storage::disk('local')->moveToDisk('s3', 'tmp/audit-export.csv', 'exports/audit-export.csv');go deeper
Recall that passing file contents as a string loads it all into memory, and that putFile and putFileAs stream a file and return its stored path.
Explain how put() chooses write or writeStream, how putFile names files with hashName(), and the directory trap when put() receives a File.
Move large files safely: stream between disks with copyToDisk or explicit streams, prefer server-side copy within a disk, and run transfers in queued jobs.
Set limits and pipelines for large-file handling, deciding where transfers run and how memory, time and storage costs are bounded.
## Why the string version fails A clinic's nightly job zips the day's scanned referral letters into one archive and ships it to S3. The first version looks harmless: ```php Storage::disk('s3')->put('archives/2026-09-29.zip', file_get_contents($tmp)); ``` `file_get_contents()` returns a **PHP string** holding every byte of the file. A 2 GB archive therefore needs a 2 GB string in the process's memory, plus whatever the HTTP client copies while sending it. PHP enforces `memory_limit` per process, and a fatal "Allowed memory size exhausted" error kills the job. The same problem appears in reverse with `Storage::get()` on a large file. The fix is to hand Flysystem a **stream** (a PHP resource it can read in chunks) instead of a string. ## How put() decides between write and writeStream `FilesystemAdapter::put($path, $contents, $options = [])` inspects `$contents`: | `$contents` is | What Laravel does | |---|---| | `Illuminate\Http\File` or `UploadedFile` | delegates to `putFile($path, $contents, $options)` | | a PSR-7 `StreamInterface` | detaches it and calls `writeStream()` | | a PHP resource | calls `writeStream()` | | a string | calls `write()`, the whole string in memory | `$options` may be a visibility string (`'public'`/`'private'`) or an array of options. ## putFile and putFileAs `putFileAs($path, $file, $name, $options = [])`: 1. opens the file with `fopen(..., 'r')` (from a path string or the file object's real path); 2. calls `put("{$path}/{$name}", $stream, $options)`, which streams through `writeStream()`; 3. closes the stream; 4. returns the stored path, or `false` if the write failed. `putFile($path, $file, $options = [])` does the same with `$file->hashName()` as the name: 40 random characters plus an extension guessed from the file's MIME type. Store the returned path in the database; it is the only record of the generated name. A trap worth knowing: `put('archives/a.zip', new File($tmp))` goes through `putFile`, so it treats `archives/a.zip` as a **directory** and stores the file as `archives/a.zip/<random>.zip`. Use `putFileAs('archives', new File($tmp), 'a.zip')` when you want an exact name. ## Streaming between disks Reading from one disk and writing to another has the same choice: - **Avoid:** `Storage::disk('s3')->put($p, Storage::disk('local')->get($p))`, which pulls the whole file into memory. - **Manual streaming:** `$in = Storage::disk('local')->readStream($p); Storage::disk('s3')->writeStream($p, $in);` then close the handle. - **Built-in (Laravel 13.32+):** `Storage::disk('local')->copyToDisk('s3', $p)` streams from `readStream()` into the destination's `writeStream()` and closes the handle; `moveToDisk()` does the copy and then deletes the source only if the copy succeeded. Both accept a disk name or a filesystem instance, and an optional destination path. Within one disk, `copy()` and `move()` let the adapter do the work: on S3, Flysystem issues a server-side copy (and a delete for `move`), so no bytes pass through PHP at all. ## What streaming does not fix - **Time.** A 2 GB upload still takes as long as the network allows; run it in a queued job, not a web request. - **Temporary disk space.** The source file must exist somewhere readable while it streams. - **Reads.** `get()` and `json()` always return whole strings; use `readStream()` for large files. - **Remote read buffering.** Laravel's `s3` driver passes `stream_reads` as `false` unless the disk sets it, so large reads from S3 should enable `'stream_reads' => true`. ## Visibility and options on streamed writes The third argument works the same way on every write method: - a string such as `'public'` or `'private'` sets the stored file's visibility; - an array passes Flysystem options through, for example `['visibility' => 'private']` or a `mimetype` for the S3 adapter to use as the object's content type. For medical archives, leave visibility private and share them later with signed links. If the upload succeeds but setting visibility fails, `put()` treats that as a failure too: `UnableToSetVisibility` is caught alongside `UnableToWriteFile` and turns into `false` (or an exception with `throw` on). ## Checklist for large files 1. Accept files as `Illuminate\Http\File`, `UploadedFile` or resources, never as strings. 2. Use `putFileAs()` when the name matters and `putFile()` when a random name is fine. 3. Check the return value (a path or `false`) or enable `throw` on the disk. 4. Move files between disks with `copyToDisk()`/`moveToDisk()` or explicit streams. 5. Do the work in a queued job so request timeouts do not cut the transfer.
- Where does Storage::put('archives/a.zip', new File($tmp)) actually store the file?`put()` hands `File` and `UploadedFile` instances to `putFile()`, which treats the first argument as a directory and generates a name with `hashName()`. The file lands at `archives/a.zip/<40 random characters>.zip`. Use `putFileAs('archives', $file, 'a.zip')` for an exact name.
- Why prefer copy() over copyToDisk() when source and destination are the same S3 bucket?`copy()` on one disk lets Flysystem issue a server-side copy, so no bytes travel through PHP. `copyToDisk()` always streams through the application, because it reads from one disk and writes to another; it is for crossing disks, not for copies within one.
saying these in an interview costs you the question
- put() with a string streams automatically once the file is large
- putFile() keeps the original client file name by default
- Raising memory_limit is the right fix for large uploads to S3
- copyToDisk() makes the storage service copy the file server-side
- get() returns a stream for large files