DEV Community

Cover image for Stop Crashing Production: Zero-Downtime Database Migrations in Laravel πŸ›‘
Prajapati Paresh
Prajapati Paresh

Posted on • Originally published at smarttechdevs.in

Stop Crashing Production: Zero-Downtime Database Migrations in Laravel πŸ›‘

The Table Lock Trap

In the early days of your B2B SaaS, running php artisan migrate on your production server takes milliseconds. But as your platform scales at Smart Tech Devs, your database tables grow. When your activity_logs or invoices table hits 50 million rows, a simple migration can suddenly become a catastrophic event.

Imagine your queries are slowing down, so you decide to add a new index to the status column. You write a standard Laravel migration and deploy. Instantly, your application goes offline. API requests timeout, users get 500 errors, and background jobs crash. Why? Because by default, PostgreSQL requires an exclusive lock on the table to build an index. It physically blocks all INSERT, UPDATE, and DELETE operations until the index finishes buildingβ€”which on a 50 million row table, could take 10 minutes.

The Solution: Concurrent Index Creation

To architect zero-downtime databases, we must build indexes in the background without locking the table. PostgreSQL provides a brilliant feature for this: CREATE INDEX CONCURRENTLY.

When you build an index concurrently, PostgreSQL does two separate scans of the table. It takes longer to build the index overall, but it allows your application to continue reading and writing to the table normally while the index is being constructed.

Implementing Concurrent Indexes in Laravel

Standard Laravel Blueprint methods (like $table->index('status')) do not support concurrent creation natively, because the feature requires raw SQL and cannot be run inside a database transaction.

Here is the architectural pattern for deploying a zero-downtime index on a massive table.


use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

class AddStatusIndexToInvoicesTable extends Migration
{
    /**
     * CRITICAL: Concurrent indexes cannot be built inside a transaction.
     * We must disable the migration wrapper transaction.
     */
    public $withinTransaction = false;

    public function up(): void
    {
        // Execute the raw PostgreSQL command to build the index in the background
        DB::statement('
            CREATE INDEX CONCURRENTLY IF NOT EXISTS invoices_status_idx 
            ON invoices (status);
        ');
    }

    public function down(): void
    {
        // Safely drop the index concurrently if we roll back
        DB::statement('
            DROP INDEX CONCURRENTLY IF EXISTS invoices_status_idx;
        ');
    }
}

Handling Migration Failures

Because concurrent indexes run outside of a transaction, if the index build fails (e.g., due to a unique constraint violation), it will leave behind an "invalid" index in PostgreSQL. The index exists, but the database ignores it for queries.

If this happens, you cannot simply retry the migration. You must first DROP INDEX CONCURRENTLY invoices_status_idx to clear the invalid artifact, fix the underlying data issue, and then run the migration again.

Conclusion

Building durable software means respecting production data. You cannot lock your core B2B tables for 10 minutes in the middle of a workday. By mastering PostgreSQL's concurrent operations and safely bypassing Laravel's migration transactions, you guarantee absolute uptime while optimizing your database for scale.

Top comments (0)