When working with remote storage such as S3, Cloudflare R2 or another S3-compatible CDN, file management can quickly become problematic if it is not properly controlled.
A common case appears when deleting files linked to a database.
In this article, we will look at:
- the common problem with Laravel
- why it can create inconsistencies
- a strategy to secure file deletion
The problem: Laravel does not always report deletion errors
Laravel provides a very convenient abstraction for file management with Laravel through Laravel Filesystem, which relies on Flysystem.
When using remote storage such as Amazon S3, it can happen that a file deletion fails silently.
For example:
Storage::disk('s3')->delete($path);
Code language: PHP (php)
If the file is not deleted for any reason (network latency, permissions, API error…), no exception is necessarily thrown.
The problem appears in the following scenario:
- The row is deleted in the database
- The file remains on the CDN
- The relationship between the database and the file is lost
Result:
- orphaned files on the storage
- data inconsistencies
- unnecessary storage costs
- impossible to retrieve the files
The case of Spatie Media Library
The Spatie Laravel Media Library library is widely used to manage files associated with Eloquent models.
It automatically handles:
- uploads
- conversions
- relationships with the database
However, deletion still depends on the Laravel filesystem. Additionally, the deletion is only executed when the related database model fires the deletion event via an observer (deleted).
Therefore the same risks remain if deletion fails on the storage side.
A safer strategy: verify before deleting the database
A simple approach is to:
- use an SQL transaction
- retrieve the file
- delete the model
- verify that the file has actually been deleted
- rollback if necessary
Example:
// 1. Start a transaction
DB::beginTransaction();
try {
// 2. Retrieve the associated media
// In case the model contains only one media
$media = $model->media->first();
// 3. Delete the model
$media->forceDelete();
// 4. Verify that the file has been removed
if (Storage::disk($media->disk)->exists($media->getPath())) {
throw new \Exception("File {$media->getPath()} still exists");
}
} catch (\Exception $e) {
// 5. Rollback if necessary
DB::rollBack();
throw $e;
}
DB::commit();
Code language: PHP (php)
This solution guarantees:
✔ consistency between the database and the storage
✔ immediate detection of a CDN issue
✔ automatic rollback
✔ no orphaned files
Possible improvement: explicitly delete the file
An even clearer approach is to explicitly delete the file before deleting the model:
Storage::disk($media->disk)->delete($media->getPath());
Code language: PHP (php)
Then verify:
if (Storage::disk($media->disk)->exists($media->getPath())) { throw new Exception("File deletion failed"); }
Code language: PHP (php)
And only then delete the database row.
This reverses the logic:
- delete the file
- verify
- delete the database record
Conclusion
When using Amazon S3 with Laravel and Spatie Laravel Media Library, it is important not to assume that file deletion will always succeed.
Without verification, you may quickly end up with:
- orphaned files
- database / storage inconsistencies
- unnecessary storage costs
The solution is to:
- Delete the database row only after ensuring that the file has been removed from the CDN, or use transactions when deleting the row afterward is not possible (for example when using Spatie)
- explicitly verify the deletion
- centralize the logic in a dedicated service
This simple approach helps guarantee integrity between your database and your remote storage.

