Laravel Read-Through Filesystem: Lazy Storage 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. Laravel Read-Through Filesystem: Lazy Storage Migration Between Buckets

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

 Laravel Read-Through Filesystem: Lazy Storage Migration Between Buckets
========================================================================

 Laravel 13.26 ships a read-through filesystem driver that transparently checks a primary disk, falls back to a legacy disk on a miss, and promotes the file automatically — letting hot files migrate themselves on first access.

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

ShareCopy linkCopied

 ![Laravel Read-Through Filesystem: Lazy Storage Migration Between Buckets](https://cdn.msaied.com/569/7759742f96ca5582353a70049ec950e1.png) 

  On this page +1. [Laravel Read-Through Filesystem: Lazy Storage Migration](#laravel-read-through-filesystem-lazy-storage-migration)
2. [Configuring a Read-Through Disk](#configuring-a-read-through-disk)
3. [How Reads, Writes, and Deletes Are Routed](#how-reads-writes-and-deletes-are-routed)
4. [Sharp Edges to Know](#sharp-edges-to-know)
5. [Reading Without Promoting](#reading-without-promoting)
6. [A Four-Step Migration Playbook](#a-four-step-migration-playbook)
7. [Key Takeaways](#key-takeaways)

 Laravel Read-Through Filesystem: Lazy Storage Migration
-------------------------------------------------------

Moving millions of files between storage buckets is expensive and slow. A bulk `aws s3 sync` copies every stale file, costs real money in egress, and takes days. Scattering `Storage::disk('old')` fallbacks through your codebase is the other common approach — and it never quite gets cleaned up.

Laravel 13.26 solves this with a first-class `read-through` filesystem driver. You compose it from a primary disk and a fallback disk, and the driver handles the two-disk dance internally. Application code never knows two buckets exist.

Configuring a Read-Through Disk
-------------------------------

Add the driver to `config/filesystems.php` and reference your existing disks by name:

```php
'disks' => [
    'r2' => [
        'driver' => 's3',
        // Cloudflare R2 credentials...
    ],
    'legacy-s3' => [
        'driver' => 's3',
        // the bucket you are leaving...
    ],
    'assets' => [
        'driver' => 'read-through',
        'primary' => 'r2',
        'fallback' => 'legacy-s3',
    ],
],

```

Controllers and jobs call `Storage::disk('assets')` as normal. Both `primary` and `fallback` also accept an inline config array if you prefer not to register the underlying disks separately. The manager validates the pair at resolution time — a missing side, duplicate disks, or a self-referencing disk throws an `InvalidArgumentException` immediately.

How Reads, Writes, and Deletes Are Routed
-----------------------------------------

Understanding the routing rules before going to production matters:

- **Reads** (`get()`, `readStream()`) check the primary first, then the fallback. A fallback hit copies the file to the primary and returns the contents. Streamed reads buffer through `php://temp` to avoid loading large files into memory.
- **Writes, deletes, moves, and copies** target the primary only.
- **Directory listings** (`files()`) reflect the primary only — unpromoted fallback content is invisible to directory iteration.
- **Existence checks and metadata** (`exists()`, `size()`, `mimeType()`, `lastModified()`, `url()`, `temporaryUrl()`) query whichever disk holds the file without triggering a copy.

### Sharp Edges to Know

Two behaviours can surprise you. First, `files()` only lists primary content, so anything that iterates a directory to discover files will miss unpromoted objects. Second, `delete()` only removes the file from the primary. If the file still exists on the fallback, the next read will resurrect it. During a migration this is usually fine — the fallback is going away — but it is worth knowing.

Promotion is best-effort by default: if copying to the primary fails, the read still succeeds from the fallback and the exception is swallowed. Set `'throw_on_promotion_failure' => true` to surface those errors immediately.

Reading Without Promoting
-------------------------

Sometimes you want the layered reads without the automatic migration. Pass `'copy' => false` to disable promotion:

```php
'assets' => [
    'driver' => 'read-through',
    'primary' => 'local-assets',
    'fallback' => 'production-s3',
    'copy' => false,
],

```

This is ideal for local development seeded from a production database snapshot: file references in the database resolve against the production bucket without slowly mirroring gigabytes onto your laptop. It also works as a cautious first phase of a real migration — cut reads over and watch error rates before allowing promotion to write to the new bucket.

A Four-Step Migration Playbook
------------------------------

1. Create the new bucket and add its disk config alongside the old one.
2. Repoint your existing disk name at a read-through pair (new bucket primary, old bucket fallback). New uploads land in the new bucket immediately; every requested file promotes itself on first read.
3. After normal access patterns have cycled, backfill the remaining long tail with a one-off sync or a batched background job — you only move what nobody asked for.
4. Swap the read-through config for a plain disk pointing at the new bucket and decommission the old one.

At no point does a single deploy flip all traffic at once, and rolling back is a config change because the old bucket remains complete throughout.

Key Takeaways
-------------

- The `read-through` driver shipped in Laravel 13.26 (PR #61140).
- Hot files migrate themselves on first access; cold files stay put until you decide.
- Writes, deletes, and directory listings always target the primary only.
- Set `throw_on_promotion_failure => true` to surface copy failures instead of swallowing them.
- Use `copy => false` for read-only layering in development or cautious migration phases.
- Rolling back is a config change — the fallback bucket stays intact throughout.

---

*Source: [Laravel Read-Through Filesystem: Lazy Storage Migration](https://laravel-news.com/laravel-read-through-filesystem) — Laravel News, August 19, 2026.*

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

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

  What does the Laravel read-through filesystem driver do?It composes two disks — a primary and a fallback — into a single named disk. Reads check the primary first; on a miss, the file is served from the fallback and automatically copied to the primary. Writes and deletes always target the primary only.

   What happens if promotion to the primary disk fails during a read?By default, the exception is swallowed and the read still succeeds from the fallback. Set 'throw\_on\_promotion\_failure' =&gt; true in the disk config to surface the error immediately instead.

   Can I use the read-through driver without migrating files — for example in local development?Yes. Set 'copy' =&gt; false in the disk config. Fallback hits are served directly and nothing is promoted to the primary. This is useful for local environments seeded from a production database where files only exist in the production bucket.

   ![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 articleObject Storage Migrations with Laravel's Read-Through Filesystem](https://msaied.com/articles/object-storage-migrations-with-laravels-read-through-filesystem) [Next articleRead-Through Disks and Debounced Listeners in Laravel 13.26](https://msaied.com/articles/read-through-disks-and-debounced-listeners-in-laravel-1326)  

   On this page
-------------

1. [Laravel Read-Through Filesystem: Lazy Storage Migration](#laravel-read-through-filesystem-lazy-storage-migration)
2. [Configuring a Read-Through Disk](#configuring-a-read-through-disk)
3. [How Reads, Writes, and Deletes Are Routed](#how-reads-writes-and-deletes-are-routed)
4. [Sharp Edges to Know](#sharp-edges-to-know)
5. [Reading Without Promoting](#reading-without-promoting)
6. [A Four-Step Migration Playbook](#a-four-step-migration-playbook)
7. [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/735/2b11b2815a5984a8e3ab654b67f9af60.png)  · 4 min read### Laravel New in 12: First-Class Typed Config, Fluent Routing, and Upgrade Notes

3 Oct 2026 ](https://msaied.com/articles/laravel-new-in-12-first-class-typed-config-fluent-routing-and-upgrade-notes) [ ![](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) 

  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)
