Read-Only Database Fallback Strategy in Laravel

- Andrés Cruz - ES En español

Video thumbnail

In software architecture design, the term fallback refers to a backup mechanism or alternative path that automatically activates when the primary system experiences a failure, thus ensuring service continuity.

In this section, we will explain how to implement a fallback system focused exclusively on read operations, maintaining a replica or mirror of the primary data. This strategy proves extremely useful in web applications where traffic is heavily query-based (such as blogs or educational platforms), where the bulk of the interaction consists of reading posts, listings, or course details.

Context and Infrastructure Limitations

In shared hosting environments or servers with limited resources, production databases (such as MySQL or PostgreSQL) often enforce a strict limit on simultaneous connections (for example, a maximum of 20 concurrent connections). If this threshold is exceeded, the server rejects additional connections by throwing a system error (such as error code 2002: Operation not permitted).

To mitigate this bottleneck without immediately incurring higher infrastructure costs, we can implement a fallback mechanism using SQLite. Since SQLite manages data directly in a local file without going through a network socket or remote database service, connection concurrency limits from the primary server are eliminated.

SQLite Database Configuration

The first step consists of configuring the secondary connection in Laravel's config/database.php file, disabling foreign key checks to facilitate bulk data synchronization:

config/database.php

'sqlite_fallback' => [
    'driver' => 'sqlite',
    'database' => database_path('fallback.sqlite'),
    'prefix' => '',
    'foreign_key_constraints' => false, // Prevents foreign key conflicts in fallback
],

To initialize the file and create the table schema, run the migrations specifying the alternative connection:

$ php artisan migrate:fresh --database=sqlite_fallback

Data Synchronization for the Replica

Since the goal is to back up only critical query tables (such as categories, posts, or lessons), a synchronization process is implemented that cleans the local database and inserts updated data from the primary database in chunks to optimize memory:

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
***
public array $availableTables = [
    'categories' => 'Categories',
    'posts' => 'Posts',
    ***
];

$syncedCount = 0;
$totalRecords = 0;
foreach ($this->selectedTables as $table) {
    if (Schema::connection('mysql')->hasTable($table)) {
        // Extract from MariaDB/MySQL
        $data = DB::connection('mysql')->table($table)->get()->map(fn($item) => (array) $item)->toArray();

        // Clean and insert into SQLite
        DB::connection('sqlite_fallback')->table($table)->truncate();

        if (!empty($data)) {
            // Insert in chunks of 500 records to optimize memory in SQLite
            foreach (array_chunk($data, 500) as $chunk) {
                DB::connection('sqlite_fallback')->table($table)->insert($chunk);
            }
            $totalRecords += count($data);
        }

        $syncedCount++;
    }
}

Automatic Failure Detection via an Eloquent Trait

To make this connection switch completely transparent to controllers and UI components (such as Livewire or Inertia), we leverage the lazy nature of Eloquent connections by overriding the getConnection() method through a restructured Trait:

app/Traits/HasSqliteFallback.php

<?php

namespace App\Traits;

use Illuminate\Support\Facades\DB;
use PDOException;
use PDO;

trait HasSqliteFallback
{
    /**
     * Overrides the Eloquent connection retrieval method for this model.
     */
    public function getConnection()
    {
        try {
            // test failure, forces an exception
            throw new PDOException("SQLSTATE[HY000] [1040] Too many connections", 1040);

            $connection = parent::getConnection();

            // Force a ping or light test on the PDO socket
            $connection->getPdo();

            return $connection;

        } catch (PDOException $e) {
            // If the socket failed, gave "Too many connections" (1040) or connection error
            if (in_array($e->getCode(), [1040, 2002]) || str_contains($e->getMessage(), '2002') || str_contains($e->getMessage(), 'Too many connections')) {
                \Log::warning("MySQL out of service. Switching model [" . static::class . "] to SQLite Fallback.");
                // 1. We register polyfills on the SQLite connection before returning it
                //$this->registerMysqlSqlitePolyfills('sqlite_fallback');

                // 2. We return the fallback connection
                return DB::connection('sqlite_fallback');
            }

            throw $e;
        }
    }
}

It is important to note the use of:

$connection->getPdo();

Because we perform a connection check BEFORE the query to see if we can connect; if connecting is not possible, the error occurs right inside the trait, where we can HANDLE the query and send it to the read-type database.

PDO is the interface implemented in PHP for database connection and management.

Usage in Eloquent Models

Once the Trait is defined, simply include it in those models representing read-only or high-query tables:

namespace App\Models;

use App\Traits\HasSqliteFallback;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasSqliteFallback;

    // Model logic...
}

With this implementation, upon any overload on the MySQL database, Eloquent will intercept the failure in the getConnection() method and dynamically switch to the local SQLite replica without interrupting the user experience or requiring modifications in the controller or view layer.

Technical Considerations

  • SQL function alignment: It is important to ensure queries are compatible with both engines, avoiding MySQL-specific methods or functions that are not available in SQLite.
  • Restricted to read operations: This architecture is not designed for immediate bidirectional synchronization. If persisting writes during an outage is required, a deferred transactions table or queue system must be incorporated.

Learn how to implement a database fallback strategy in Laravel using SQLite. Prevent overload errors and ensure high availability in read-only mode.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

I agree to receive announcements of interest about this Blog.