Migrating Object Storage Without Downtime in Laravel 13
Cutting over object storage is rarely instantaneous. New uploads need to land in the destination immediately, but thousands of existing objects still live in the source bucket. Laravel 13 solves this with a read-through filesystem driver that stacks two disks behind the standard Storage facade — no application rewrites required.
How the Read-Through Driver Works
The driver introduces two roles:
- Primary — receives all new writes from the moment you flip the switch.
- Fallback — serves reads only when the primary reports a path as missing.
When a file is found on the fallback, the driver can automatically promote it — copying it to primary during the same request — so every subsequent read is served from the destination.
Basic Configuration
Define the composite disk in config/filesystems.php:
'disks' => [
'r2' => [
'driver' => 's3',
// Cloudflare R2 credentials ...
],
'legacy-s3' => [
'driver' => 's3',
// AWS S3 credentials ...
],
'assets' => [
'driver' => 'read-through',
'primary' => 'r2',
'fallback' => 'legacy-s3',
],
],
Point your default disk at assets and the rest of your application code stays unchanged.
The Read Path in Detail
When your code calls Storage::disk('assets')->get('avatars/42.jpg'), Laravel:
- Checks primary (R2) for the path.
- On a miss, reads the file from fallback (S3).
- Checks primary again before writing — a concurrent request may have already promoted it.
- Writes the fallback contents to primary and returns them to the caller.
The double-check reduces duplicate promotions and prevents overwriting a file that arrived between steps 2 and 4. For truly concurrent workloads, immutable or versioned object keys eliminate the remaining race window.
Filesystem Operation Routing
| Operation | Disk used |
|---|---|
| get, read, readStream | Primary, then fallback on miss (promotes by default) |
| exists, size, mimeType | Primary, then fallback |
| put, writeStream | Primary only |
| delete, deleteDirectory | Fallback first, then primary |
| Public / temporary URLs | Whichever disk currently holds the path |
Directory listings report primary state only, which keeps the destination authoritative during the migration.
Memory and Streaming
get() loads the entire object into a PHP string. For large files, prefer readStream(): Laravel buffers the object in php://temp, writes the stream to primary, rewinds it, and returns it — keeping PHP memory usage low while still promoting the file.
Size temporary storage and request timeouts to match your largest objects, or pre-warm large files with a background job before they hit the request path.
Promotion Failures
By default, promotion is best-effort: if the write to primary fails, Laravel still returns the fallback contents and silently retries on the next request. To surface promotion failures as exceptions, set:
'throw_on_promotion_failure' => true,
The disk's throw option must also be true for application code to receive the UnableToReadFile exception.
Completing the Migration
- Configure destination as primary, current store as fallback.
- Route application traffic through the read-through disk.
- Enumerate remaining fallback keys and dispatch background copy jobs, skipping paths already on primary.
- Verify destination key counts, sizes, and checksums.
- Point the application directly at primary and retire the fallback disk.
For the cold tail of rarely accessed objects, tools like Cloudflare Super Slurper, rclone, or AWS DataSync can bulk-copy what application traffic never promoted.
Key Takeaways
- Laravel 13's read-through driver requires zero application code changes beyond disk configuration.
- Writes always go to primary; fallback is read-only from the application's perspective.
- Promotion copies a file to primary on first access, making all later reads free of fallback latency.
readStream()is preferable toget()for large objects to avoid high PHP memory usage.- Deletes remove the path from both disks, preventing ghost re-promotions.
- Background bulk tools should skip paths already present on primary to protect post-cutover uploads.
- Egress costs apply only once per object during promotion; subsequent reads come from the destination.
Source: Object storage migrations with Laravel's read-through filesystem