Object Storage Migrations with Laravel's Read-Through Filesystem
Laravel #Laravel 13 #Object Storage #S3 #Cloudflare R2 #Filesystem #Migration

Object Storage Migrations with Laravel's Read-Through Filesystem

4 min read Mohamed Said Mohamed Said

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:

  1. Checks primary (R2) for the path.
  2. On a miss, reads the file from fallback (S3).
  3. Checks primary again before writing — a concurrent request may have already promoted it.
  4. 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

  1. Configure destination as primary, current store as fallback.
  2. Route application traffic through the read-through disk.
  3. Enumerate remaining fallback keys and dispatch background copy jobs, skipping paths already on primary.
  4. Verify destination key counts, sizes, and checksums.
  5. 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 to get() 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

Found this useful?

Frequently Asked Questions

3 questions
Q01 What happens if the promotion write to primary fails in Laravel's read-through driver?
By default, promotion is best-effort. If the write to primary fails, Laravel still returns the file contents read from the fallback disk, and a later request can attempt promotion again. If you need the failure to be visible, set `throw_on_promotion_failure => true` and `throw => true` on the disk; the driver will then throw an `UnableToReadFile` exception instead of silently returning the fallback contents.
Q02 Do directory listings in the read-through disk show files from both the primary and fallback disks?
No. Directory listings report primary state only. To enumerate objects that still exist only on the fallback, you must query the fallback disk directly and filter out paths already present on primary.
Q03 Will deleting a file through the read-through disk remove it from both S3 and R2?
Yes. The driver deletes from the fallback disk first, then from primary. This prevents a 'ghost delete' scenario where a file deleted from primary gets re-promoted from the fallback on the next read. If the fallback delete fails (for example, due to a read-only credential), the primary delete is also skipped and an error is surfaced according to the disk's `throw` setting.

Continue reading

More Articles

View all