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

           [  Contact](https://msaied.com#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://msaied.com)
- [ProjectsCase studies](https://msaied.com/projects)
- [ArticlesEngineering notes](https://msaied.com/articles)
- [CertificatesCredentials](https://msaied.com/certificates)
- [AboutHow I work](https://msaied.com#about)
- [ContactGet in touch](https://msaied.com#contact)

  [Start a conversation](https://msaied.com#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://msaied.com)
2. /
3. [Articles](https://msaied.com/articles)
4. /
5. [Laravel](https://msaied.com/articles?category=laravel)
6. /
7. Object Storage Migrations with Laravel's Read-Through Filesystem

   [Laravel](https://msaied.com/articles?category=laravel) 

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

 Laravel 13 introduces a read-through filesystem driver that lets you migrate from S3 to R2 without downtime. New writes land on the destination immediately while legacy objects are promoted on first access.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://msaied.com#person) Published 18 Aug 2026 · Updated 18 Aug 2026 · 4 min read

ShareCopy linkCopied

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

  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)

 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)*

- [Laravel 13](https://msaied.com/articles?search=Laravel%2013)
- [Object Storage](https://msaied.com/articles?search=Object%20Storage)
- [S3](https://msaied.com/articles?search=S3)
- [Cloudflare R2](https://msaied.com/articles?search=Cloudflare%20R2)
- [Filesystem](https://msaied.com/articles?search=Filesystem)
- [Migration](https://msaied.com/articles?search=Migration)

 Frequently asked questions 
---------------------------

  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.

   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.

   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.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

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

[About](https://msaied.com#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleLivewire v4.4.1 Released: Bug Fixes, Alpine 3.16.2, and Laravel 13 Compatibility](https://msaied.com/articles/livewire-v441-released-bug-fixes-alpine-3162-and-laravel-13-compatibility) [Next articleLaravel Read-Through Filesystem: Lazy Storage Migration Between Buckets](https://msaied.com/articles/laravel-read-through-filesystem-lazy-storage-migration-between-buckets)  

   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)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://msaied.com#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/731/7cabd03b86a18f1db2c9ff7de1510270.png)  · 3 min read### Laravel Octane + FrankenPHP: Request Lifecycle, Shared State, and Safe Singleton Patterns

3 Oct 2026 ](https://msaied.com/articles/laravel-octane-frankenphp-request-lifecycle-shared-state-and-safe-singleton-patterns) [ ![](https://cdn.msaied.com/728/b96439f5ef5f084b44d736951875f964.png)  · 3 min read### Eloquent Custom Relations: Polymorphic Pivots, Has-One-Of-Many, and Raw Join Relations

2 Oct 2026 ](https://msaied.com/articles/eloquent-custom-relations-polymorphic-pivots-has-one-of-many-and-raw-join-relations) [ ![](https://cdn.msaied.com/727/7873d7495db459e7a6e3bfbb8835852d.png)  · 3 min read### Laravel Caching Strategies: Tags, Stampede Prevention, and Cache-Aside at Scale

2 Oct 2026 ](https://msaied.com/articles/laravel-caching-strategies-tags-stampede-prevention-and-cache-aside-at-scale-2) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://msaied.com#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://msaied.com)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://msaied.com/sitemap.xml)
