Laravel 13 Introduces Zero-Downtime Object Storage Migration: What Development Teams Should Know 

Laravel 13 Introduces Zero-Downtime Object Storage Migration: What Development Teams Should Know 

WRITTEN BY

Hiren Mansuriya

Director & CTO LinkedIn

Imagine a SaaS platform with five million customer files stored in Amazon S3. 

Users continue uploading invoices, profile images, reports, and videos every minute. Meanwhile, the development team wants to move those files to Cloudflare R2. 

A traditional migration creates an uncomfortable choice: stop new uploads during the transfer, build temporary dual-storage logic, or risk files being lost between the old and new systems. 

Laravel 13 now offers another approach. 

On August 18, 2026, Laravel introduced a read-through filesystem driver that allows an application to use two storage locations as one logical disk during migration. New files go to the destination immediately, while older files remain accessible from the source and can move when customers request them. 

The result is not a one-click migration. It is a safer way to separate application cutover from bulk data transfers. 

How Did Object-Storage Migration Work Before Laravel 13? 

Before this feature, development teams generally had to copy every object before changing the application’s storage configuration. Another option was to write custom fallback logic that checked the new bucket and then searched the old one when a file was missing. 

Both approaches increase operational risk. 

Migration concern Traditional cutover Laravel 13 read-through 
New uploads May require a write freeze or custom dual writes Go directly to the destination 
Existing files Usually copied before cutover Stay accessible from the source 
Downtime A maintenance window may be required Storage requests can continue 
Popular files Move with the entire dataset Can move when first requested 
Cold files Copied even if rarely used Can move later in a bulk transfer 
Rollback Difficult after the final switch Source remains available until retired 
Migration logic Often application-specific Uses Laravel’s filesystem abstraction 

The important difference is timing. The application can begin using the destination before every legacy object has been transferred. 

How Do You Configure Laravel’s Read-Through Filesystem? 

Laravel’s official pull request shows a new read-through driver with a primary and fallback disk. 

In an S3-to-R2 migration, the existing S3 bucket becomes the fallback, while R2 becomes the primary destination. 

A simplified config/filesystems.php setup could look like this: 

‘disks’ => [ 
 
    ‘legacy-s3’ => [ 
        ‘driver’ => ‘s3’, 
        ‘key’ => env(‘AWS_ACCESS_KEY_ID’), 
        ‘secret’ => env(‘AWS_SECRET_ACCESS_KEY’), 
        ‘region’ => env(‘AWS_DEFAULT_REGION’), 
        ‘bucket’ => env(‘AWS_BUCKET’), 
    ], 
 
    ‘r2’ => [ 
        ‘driver’ => ‘s3’, 
        ‘key’ => env(‘R2_ACCESS_KEY_ID’), 
        ‘secret’ => env(‘R2_SECRET_ACCESS_KEY’), 
        ‘region’ => ‘auto’, 
        ‘bucket’ => env(‘R2_BUCKET’), 
        ‘endpoint’ => env(‘R2_ENDPOINT’), 
    ], 
 
    ‘assets’ => [ 
        ‘driver’ => ‘read-through’, 
        ‘primary’ => ‘r2’, 
        ‘fallback’ => ‘legacy-s3’, 
    ], 
 
], 
 

The application can then use the composite disk as its default: 

FILESYSTEM_DISK=assets 
 

From this point, new writes go to R2. Existing objects can still be read from S3. 

The configuration is short, but teams should test it in a non-production environment and confirm that both filesystem adapters support every operation used by the application. 

What Happens When Laravel Reads an Old File? 

The application code remains familiar: 

use Illuminate\Support\Facades\Storage; 
 
$content = Storage::disk(‘assets’) 
    ->get(‘avatars/customer-42.jpg’); 
 

Behind this one call, Laravel performs a multi-stage process. 

First, it checks whether the file exists on the primary R2 disk. If found, Laravel returns it normally. 

If the file is missing, Laravel reads it from the fallback S3 bucket. Before copying it, the framework checks the primary disk again. This reduces duplicate promotions and helps prevent an older source object from overwriting a file created by another request. 

If the destination is still empty, Laravel copies the file to R2 and returns it. Future requests use the R2 version. 

Laravel calls this process promotion

Application request 
        ↓ 
Check primary storage 
        ↓ 
File missing? 
        ↓ 
Read from fallback 
        ↓ 
Check primary again 
        ↓ 
Copy to primary and return 
 

The second check reduces race conditions, but it does not make the check-and-write sequence atomic. Applications that frequently overwrite the same path should use versioned object keys or coordinate writes during the transition. 

How Does Laravel Handle Different File Operations? 

The read-through driver intentionally routes operations differently. 

Laravel operation Storage behaviour Does it promote the file? 
get(), read(), readStream() Primary, then fallback Yes, by default 
exists(), size(), mimeType() Primary, then fallback No 
put(), writeStream() Primary only Not applicable 
Directory listing Primary only No 
delete() Fallback, then primary No 
move() Primary, then fallback May require an initial promoted read 
copy() Primary Operates on destination state 
Temporary download URL Disk currently holding the object No 
Temporary upload URL Primary Not applicable 

Delete behaviour solves a subtle problem. 

If an object were deleted only from R2 but remained in S3, a future read could promote it again. Laravel therefore deletes from the fallback before deleting from the primary. 

That also means the fallback credentials need delete permission. A company keeping the source bucket read-only will need a separate deletion record or deferred cleanup process. 

How Much Could an S3-to-R2 Migration Cost? 

Laravel’s official engineering article published the following public rates as of August 2026: 

Item Public rate cited by Laravel 
R2 Standard storage $0.015 per GB-month 
R2 Class A operations $4.50 per million; first one million monthly operations free 
R2 Class B operations $0.36 per million; first 10 million monthly operations free 
R2 internet egress Free 
AWS internet data transfer out First 100 GB monthly free; common US paid tiers begin at $0.09 per GB 
S3 Standard GET requests in US East $0.0004 per 1,000 requests 

Consider an illustrative migration involving 5,000 GB and one million objects. Assume all data moves within one month, each object is read once from S3, and the cited common AWS transfer rate applies. 

Illustrative cost component Calculation Estimated amount 
AWS data transfer 4,900 chargeable GB × $0.09 $441.00 
One million S3 GET requests 1,000 × $0.0004 $0.40 
One million R2 writes Within first one million Class A operations $0.00 
Two million R2 existence checks Within first 10 million Class B operations $0.00 
First month of R2 storage 5,000 GB × $0.015 $75.00 
Illustrative first-month total Transfer + requests + storage $516.40 

This is not a quotation or a guaranteed migration cost. Actual charges depend on binary versus decimal storage measurement, AWS region, account-level free usage, storage class, retries, multipart operations, object count, negotiated pricing, and how much data moves during the billing period. 

The calculation is useful because it shows that data transfer may cost substantially more than the storage operations themselves. 

Read-through migration does not necessarily reduce the total bytes that eventually move. Its advantage is that the active working set can migrate first, while the business postpones the cost and effort of moving rarely accessed objects. 

How Should Large Files Be Handled? 

Using get() loads the complete object into a PHP string before promotion. 

$file = Storage::disk(‘assets’)->get($path); 
 

Memory use therefore grows with the size of the object. That may be acceptable for thumbnails or documents, but it is risky for large videos, backups, or archives. 

Laravel supports streamed reads: 

$stream = Storage::disk(‘assets’)->readStream($path); 
 
if ($stream !== null) { 
    // Process or return the stream safely. 

 

According to Laravel’s official explanation, the stream uses php://temp. PHP keeps the content in memory until it exceeds 2 MiB, then moves it to a temporary file. 

Streaming limits PHP memory pressure, but the server still needs enough temporary-disk capacity. The first request also includes the time required to download the object from the source and upload it to the destination. 

Large or latency-sensitive files may be better moved through background jobs before users request them. 

What Happens if Promotion Fails? 

Promotion is best effort by default. 

If Laravel successfully reads an object from S3 but cannot write it to R2, it can still return the source content to the user. The file remains in S3, and a later request can attempt promotion again. 

Applications that need the read itself to fail when promotion fails can enable strict behaviour: 

‘assets’ => [ 
    ‘driver’ => ‘read-through’, 
    ‘primary’ => ‘r2’, 
    ‘fallback’ => ‘legacy-s3’, 
    ‘throw_on_promotion_failure’ => true, 
    ‘throw’ => true, 
], 
 

Best-effort mode prioritizes application availability. Strict mode prioritizes immediate visibility of migration failures. 

Neither choice is universally correct. A public-media application may prefer to serve the source file even when promotion fails. A compliance-sensitive document system may need the failure reported immediately. 

Does Traffic-Driven Promotion Finish the Migration? 

No. 

Popular files will move as customers request them. Rarely accessed objects form a “cold tail” and may remain in the fallback bucket indefinitely. 

Laravel recommends completing the migration through background jobs. Teams should track transferred bytes, completed keys, skipped keys, retry counts, and failed objects. 

Verification should compare more than the number of files. Where the providers support it, teams should compare object keys, sizes, and checksums. 

A simple Laravel verification step might use the storage checksum API: 

$sourceChecksum = Storage::disk(‘legacy-s3’) 
    ->checksum($path); 
 
$destinationChecksum = Storage::disk(‘r2’) 
    ->checksum($path); 
 
if ($sourceChecksum !== $destinationChecksum) { 
    report(“Checksum mismatch: {$path}”); 

 

Provider checksum behaviour can differ, especially for multipart uploads. The comparison method must therefore be validated against the selected storage providers. 

Only after the bulk transfer, verification, and an observation period should the application point directly to the destination and retire the fallback credentials. 

What Are the Main Risks? 

Laravel’s new driver reduces cutover risk, but development teams still need to manage concurrency, large-file latency, provider-specific metadata, storage permissions, direct URLs, retries, and billing. 

Cache-control headers, content disposition, storage classes, custom metadata, and original modification times may not move through Laravel’s generic write contract. A provider-aware transfer or metadata pass may be necessary. 

Direct public URLs also do not trigger promotion. When a browser downloads directly from S3, the request bypasses Laravel’s read-through adapter. Applications relying heavily on direct URLs will need a background migration or an application-controlled download route. 

How Spaculus Supports Laravel Storage Migration 

A production storage migration is not only a configuration change. It involves Laravel architecture, cloud permissions, background queues, file integrity, cost modelling, monitoring, rollback planning, and customer-facing performance. 

As a Laravel development company, Spaculus Software can help businesses assess existing storage patterns, upgrade Laravel applications, configure incremental migrations, build background-copy workflows, validate migrated data, and monitor the transition before the original storage is retired. 

The right approach depends on file volume, object size, traffic distribution, metadata requirements, compliance obligations, and the cost of a failed migration. 

Key Takeaways 

Final Thought 

Laravel 13 turns object-storage migration from one major cutover into a controlled transition. 

That is the real value of the feature. 

Teams can redirect new writes, keep old files available, migrate active data first, verify the destination, and retire the source only when the evidence shows that the migration is complete. 

Would your team trust one large storage cutover, or prefer to move gradually while both providers remain available?

Best Laravel Development

Author

Hiren Mansuriya

Director & CTO

Hiren, a visionary CTO, drives innovation, delivering 300+ successful web/mobile apps. Leading a 70+ tech team, Hiren excels in DevOps, cloud solutions, and more. With a top-performing IT Engineering background, Hiren ensures on-time, cost-effective projects, transforming businesses with strategic expertise.

LinkedIn

What to Read Next

Leave A Reply

Your email address will not be published. Required fields are marked *


Get a Free Consultation Today!