Practical Guide to Fast Rollbacks for Laravel Deployments with Migrations and Feature Flags

By · Published on · 7 min read

Practical guide to quick rollback for Laravel deployments with migrations and feature flags
Photo: Marek Prášil / Pexels

This practical guide to fast rollbacks for Laravel deployments with migrations and feature flags shows how to reduce risk and revert changes in minutes. You’ll learn how to separate code and database changes, design reversible migrations, use toggles to safely enable features, and build a pipeline with versioned releases and an atomic symlink.

What “quick rollback” means and when to use it

Quick rollback is the ability to revert a deployment within minutes with minimal impact. It combines code, database, and feature flags to keep the system stable.

Small teams gain predictability: if something goes wrong, you can undo the change without panic. The secret is planning compatibility and having a clear playbook.

  • Critical error after releasing a new version.
  • Slow migration blocking requests.
  • Functional bug in a specific flow.
  • Unexpected data incompatibility.
  • Instability in external services after the change.

Safe strategy: separate code, schema, and data

Treat a deployment as three coordinated flows: code, database schema, and data. Reduce coupling so reverting code doesn’t require reverting data.

Apply the expand/contract pattern: first expand the schema in a compatible way, then change the code to use the new format, and finally contract by removing legacy parts.

Destructive changes require extra caution. Avoid drops on the initial deploy. Schedule removals only after old reads no longer exist.

Change Expand step Contract step Notes
Add column Create nullable column without a heavy default Make NOT NULL after backfill Backfill asynchronously or during low-traffic windows
Rename column Create new column and duplicate writes Remove old column Keep dual reads during migration
Change type Create compatible column, copy data and switch usage Remove old column Convert in batches to avoid long locks
Remove column Introduce a feature flag to stop reads Drop later Only contract when 100% of traffic no longer uses it

Rollback-tolerant migrations in Laravel

Use reversible migrations and compatible changes. The goal is that rolling back code won’t break the schema. Here’s an example of expand/contract.

The official migrations documentation covers the basics. Consult it when you have questions about types and compatibility.

For large changes, perform backfills in batches and outside the main transaction. Avoid locking tables for long periods.

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        // Expand: adicionar coluna compatível
        Schema::table('orders', function (Blueprint $table) {
            $table->string('checkout_token')->nullable()->after('id');
        });

        // Backfill assíncrono recomendado (job separado), não aqui.
        // Ex.: dispatch(new BackfillCheckoutTokenJob());
    }

    public function down(): void
    {
        // Contract reversível: só remova quando o código não depender mais
        Schema::table('orders', function (Blueprint $table) {
            $table->dropColumn('checkout_token');
        });
    }
};

Feature flags to decouple deploy from release

Feature flags let you release code and enable the feature later, with fine control and instant reversal.

To start, you can store toggles in cache or the database. The important parts are fast reads and an audit trail.

Deepen your modeling of flags, scopes, and progressive rollout on the page about feature flags in PHP monoliths.

  • Keep flags cached for fast reads.
  • Standardize names and domains (e.g., checkout.new_flow).
  • Log activations and deactivations.
  • Implement a clear fallback when the flag is off.
<?php
namespace App\Support;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;

class Feature
{
    public static function enabled(string $key): bool
    {
        $flags = Cache::remember('feature.flags', 30, function () {
            return DB::table('feature_flags')->pluck('enabled', 'key')->toArray();
        });
        return (bool)($flags[$key] ?? false);
    }
}

// Uso no código (exemplos):
// if (Feature::enabled('checkout.novo_fluxo')) {
//     return app(NewCheckout::class)->handle($request);
// }
// return app(LegacyCheckout::class)->handle($request);

Organize deployments as versioned releases with an atomic symlink to current. This allows switching back to the previous release immediately.

Run compatible migrations before switching traffic. Enable feature flags only after basic checks.

The Laravel deployment guide covers optimizations like config:cache and autoload optimization.

  • Create a release directory with code and dependencies.
  • Run checks and compatible migrations.
  • Atomically switch the current symlink.
  • Warm caches and run health checks.
  • Enable flags and monitor.
set -euo pipefail
RELEASE=$(date +%Y%m%d%H%M%S)
BASE=/var/www/app
NEW=$BASE/releases/$RELEASE
CUR=$BASE/current

# 1) Preparar release
rsync -az --delete ./ $NEW/
cd $NEW
composer install --no-dev --prefer-dist --no-progress
php artisan config:cache && php artisan route:cache

# 2) Pré-mudança de schema compatível
php artisan migrate --force

# 3) Troca atômica
ln -sfn $NEW $CUR

# 4) Pós-deploy
php artisan queue:restart || true
php artisan cache:clear || true

# Rollback manual rápido
# ln -sfn $BASE/releases/<release_anterior> $CUR

Rollback playbook in minutes

Have a concise runbook. The initial focus is stabilizing traffic. Afterwards, analyze the root cause and fix.

Avoid relying on migrate:rollback in production. Reversing destructive migrations can cause data loss. Prefer reverting code and toggles first.

  • Turn off flags related to the error.
  • Switch the symlink to the previous stable release.
  • Restart workers and clear caches.
  • Freeze new changes until diagnosis is complete.
  • Decide between a quick hotfix or a detailed post-mortem.
Incident Immediate action After stabilizing
Functional bug in a new flow Flag OFF and capture flow logs Fix and test with a subset of users
Timeout in a long migration Isolate traffic to the affected endpoint and monitor queues Split the backfill and reschedule the window
HTTP 500 after deploy Revert symlink and restart workers Review config and dependency diffs
Inconsistent data Stop problematic writes and isolate cases Run reconciliation and validation scripts

Post-deploy validation and CI/CD automation

Automate health checks and smoke tests after the switch. If they fail, roll back automatically to the previous release.

Include migration checks and basic latency tests. Expose an authenticated healthcheck endpoint.

Pipelines with manual gates help for sensitive changes. Combine them with feature flags for a safe rollout.

name: deploy
on: [workflow_dispatch]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Run migrations
        run: php artisan migrate --force
      - name: Switch symlink
        run: ./scripts/switch_release.sh
      - name: Smoke check
        run: curl -fsS https://example.com/health || echo FAIL > .failed
      - name: Auto-rollback if failed
        if: always()
        run: |
          if [ -f .failed ]; then
            ./scripts/rollback_release.sh
            exit 1
          fi

Useful references and next steps

Deepen your knowledge of migrations and deployments in the official Laravel documentation. Review your pipeline based on the expand/contract pattern.

See complementary content on deploys, CI/CD, and feature flags on the site to strengthen your process.

  • Laravel migrations (official documentation).
  • Laravel deployments (best practices).
  • Laravel queues for backfills.

Frequently asked questions

Should I run php artisan migrate:rollback in production?

Use extreme caution. In general, prioritize reverting code and disabling feature flags. Schema rollbacks can cause data loss. Prefer the expand/contract pattern so you can roll back code without undoing compatible changes.

How to handle slow migrations or ones that lock tables?

Break changes into steps. First create compatible structures, then backfill in batches outside the main transaction. Use queues to process data, monitor locks, and run during low-traffic windows to reduce impact.

Are feature flags in .env enough?

They work to start, but changing .env requires a new deploy. Flags stored in the database with caching allow real-time toggling, auditing, and progressive rollout without restarting the app.

How should I treat queues and Horizon during a rollback?

After switching the symlink back, restart workers so they load the old code. Pausing critical queues during the swap prevents processing messages with incompatible versions and reduces data inconsistencies.

Is zero-downtime rollback always possible?

Not always. Zero-downtime depends on code/schema compatibility, migration size, and traffic. Plan expand/contract, use feature flags, and validate in staging to increase the chance of a seamless rollback.

Want to strengthen your rollback plan?

If you need help reviewing your pipeline, migrations, and feature flags to reduce deployment risk, let’s talk about practical steps for your context.

Talk about safe rollback

See also