Reader Stacks

Rolling Back Migrations in Laravel: All, Batch, and Specific

Rollback only ever runs a migration's down() method — if that method was never written correctly (or at all), rollback either does nothing or fails outright, regardless of how the up() method itself behaved.

Rolling Back Migrations in Laravel: All, Batch, and Specific

Rolling back a migration runs its down() method to reverse whatever up() did — Laravel offers several levels of rollback granularity, from undoing everything to reversing just one specific migration file.

Rolling back the last batch of migrations

php artisan migrate:rollback

Migrations run together in the same migrate command execution are grouped into a single "batch" — rollback by default undoes the entire most recent batch, not just the single most recently created migration file, which matters if several migrations were run together in one deployment.

Rolling back a specific number of batches

php artisan migrate:rollback --step=3

Rolling back everything

php artisan migrate:reset

migrate:reset rolls back every migration that has ever run, all the way back to an empty schema — genuinely more destructive than a normal rollback, worth reserving for a full local reset rather than routine rollback needs.

Rolling back and immediately re-running everything

php artisan migrate:refresh
php artisan migrate:refresh --seed

migrate:refresh is reset followed immediately by migrate in one command — genuinely useful during active local development when the schema keeps changing and a clean rebuild is wanted without two separate command invocations.

Checking migration status before rolling back anything

php artisan migrate:status

This lists every migration and whether it has run (and which batch it belongs to) — worth checking before any rollback operation, to confirm exactly what will actually be undone before running a command that can't easily be undone if the wrong batch gets reversed.

Rolling back one specific migration file, out of order

Laravel's Artisan commands have no direct "rollback this one specific named migration regardless of batch" option — rolling back one specific out-of-sequence migration typically requires either targeting the exact batch it belongs to (via migrate:status to find its batch number, then a scoped rollback), or manually writing and running a one-off script calling that migration's own down() method directly.

Why a broken down() method blocks a rollback entirely

public function down(): void
{
    Schema::table('posts', function (Blueprint $table) {
        $table->dropColumn('nonexistent_column'); // throws if this column was never actually added
    });
}

Rollback only ever executes a migration's own down() method — if it was written incorrectly, left as an empty stub, or references a column/table that doesn't actually match what up() created, the rollback either silently does nothing or throws an error outright, regardless of how correctly up() itself behaved.

Why rollback is genuinely risky on a production database

Rolling back a migration that dropped a column, for instance, only recreates an empty column — any data that existed in it before the original migration ran is permanently gone and cannot be restored by the rollback itself; on production, a full database backup before running any migration or rollback operation is the real safety net, not the rollback mechanism itself.

Testing a migration's rollback locally before deploying it

php artisan migrate
php artisan migrate:rollback
php artisan migrate

Running this sequence locally during development — migrate, then immediately roll back, then migrate again — is a simple, worthwhile habit that catches a broken or missing down() method before the migration is ever deployed to a shared or production environment where a broken rollback would actually matter.