Laravel 13 Read-Through Filesystem for S3 to R2 Migration | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Object Storage Migrations with Laravel's Read-Through Filesystem        On this page       1. [  Migrating Object Storage Without Downtime in Laravel 13 ](#migrating-object-storage-without-downtime-in-laravel-13)
2. [  How the Read-Through Driver Works ](#how-the-read-through-driver-works)
3. [  Basic Configuration ](#basic-configuration)
4. [  The Read Path in Detail ](#the-read-path-in-detail)
5. [  Filesystem Operation Routing ](#filesystem-operation-routing)
6. [  Memory and Streaming ](#memory-and-streaming)
7. [  Promotion Failures ](#promotion-failures)
8. [  Completing the Migration ](#completing-the-migration)
9. [  Key Takeaways ](#key-takeaways)

  ![Object Storage Migrations with Laravel's Read-Through Filesystem](https://cdn.msaied.com/565/f830d15d4a1287d381fa05e631ea2aba.png)

 [  Laravel ](https://msaied.com/articles?category=laravel)  #Laravel 13   #Object Storage   #S3   #Cloudflare R2   #Filesystem   #Migration  

 Object Storage Migrations with Laravel's Read-Through Filesystem 
==================================================================

     18 Aug 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

  9 sections  

1. [  01   Migrating Object Storage Without Downtime in Laravel 13  ](#migrating-object-storage-without-downtime-in-laravel-13)
2. [  02   How the Read-Through Driver Works  ](#how-the-read-through-driver-works)
3. [  03   Basic Configuration  ](#basic-configuration)
4. [  04   The Read Path in Detail  ](#the-read-path-in-detail)
5. [  05   Filesystem Operation Routing  ](#filesystem-operation-routing)
6. [  06   Memory and Streaming  ](#memory-and-streaming)
7. [  07   Promotion Failures  ](#promotion-failures)
8. [  08   Completing the Migration  ](#completing-the-migration)
9. [  09   Key Takeaways  ](#key-takeaways)

       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`:

```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:

```php
'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](https://laravel.com/blog/object-storage-migrations-with-laravels-read-through-filesystem)*

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fobject-storage-migrations-with-laravels-read-through-filesystem&text=Object+Storage+Migrations+with+Laravel%27s+Read-Through+Filesystem) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fobject-storage-migrations-with-laravels-read-through-filesystem) 

 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 =&gt; true` and `throw =&gt; 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    ](https://msaied.com/articles) 

 [ ![Livewire v4.4.1 Released: Bug Fixes, Alpine 3.16.2, and Laravel 13 Compatibility](https://cdn.msaied.com/564/c64b65959ad8ade491c78f3482f22996.png) Livewire Laravel Alpine.js 

### Livewire v4.4.1 Released: Bug Fixes, Alpine 3.16.2, and Laravel 13 Compatibility

Livewire v4.4.1 ships 16 fixes and improvements including Alpine.js bumped to 3.16.2, cached computed property...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 18 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/livewire-v441-released-bug-fixes-alpine-3162-and-laravel-13-compatibility) [ ![MySQL EXPLAIN and Query Profiling in Laravel: Finding Slow Queries Before They Hit Production](https://cdn.msaied.com/563/f2d4a7fb0ab45706cf9330746f7b2588.png) laravel mysql performance 

### MySQL EXPLAIN and Query Profiling in Laravel: Finding Slow Queries Before They Hit Production

Learn how to read MySQL EXPLAIN output, use query profiling tools, and integrate them into a Laravel workflow...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 18 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/mysql-explain-and-query-profiling-in-laravel-finding-slow-queries-before-they-hit-production) [ ![Laravel Lock: Distributed Locks for Models and Routes](https://cdn.msaied.com/562/7649de72113e99332a9f7e25015f9397.png) Laravel Distributed Locks Composer Package 

### Laravel Lock: Distributed Locks for Models and Routes

Laravel Lock is a package by Md Mahedi Zaman Zaber that wraps distributed locking behind a fluent builder, a H...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 17 Aug 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-lock-distributed-locks-for-models-and-routes) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
