Laravel Tutorials

Delete Expired Sessions in Laravel With a Scheduled Command

Published
Delete Expired Sessions in Laravel With a Scheduled Command image

If your Laravel app uses the database or file session driver, expired sessions stay in storage until Laravel deletes them. On Laravel 13.39 or later, you can delete them with the session:garbage-collect Artisan command on a schedule:

// routes/console.php
use Illuminate\Support\Facades\Schedule;
 
Schedule::command('session:garbage-collect')->hourly();

The command runs the same cleanup Laravel already runs during some web requests. With the command on a schedule and the lottery turned off, no web request waits for the cleanup.

Note: The redis, memcached, dynamodb, apc, and cookie drivers expire sessions without a cleanup step, so apps that use them don't need the command.

Turn Off the Session Lottery

Without a scheduled command, Laravel deletes expired sessions during web requests. The lottery option in config/session.php sets how often:

'lottery' => [2, 100],

The two numbers are odds. The default [2, 100] gives each request that starts a session a 2 in 100 chance of running the cleanup. That request runs the delete before it reaches your route and waits for it to finish.

The odds stay the same whatever your traffic. At 1,000 requests a minute, about 20 requests a minute run the delete, and some of them can overlap. At 40 requests a day, cleanup runs less than once a day, and expired sessions stay in storage until it does.

Once the scheduler deletes expired sessions, set the first number to 0. A 0 in 100 chance means no web request runs the cleanup:

// config/session.php
'lottery' => [0, 100],

If you leave it at [2, 100], the lottery and the command delete the same expired sessions, and the only cost is the extra queries.

Which Session Drivers Need Cleanup

Driver What cleanup does
database Deletes rows where last_activity is older than the lifetime
file Deletes session files older than the lifetime
redis, memcached, dynamodb, apc Nothing. The cache store expires each session with a TTL
cookie Nothing. The session lives in the browser

New Laravel 13 applications use the database driver by default, and the default sessions migration indexes last_activity, which is the column the delete query filters on.

Users can't load an expired session before cleanup runs, because both drivers check a session's age when they read it. Cleanup frees the storage that expired sessions take up.

Running session:garbage-collect

Run the command by hand to check it before you schedule it:

php artisan session:garbage-collect
INFO Session garbage collector ran successfully.

The command uses your default session driver and session.lifetime, with no options to change either. It doesn't print how many sessions it deleted. With a cache-based driver, it prints the same success message and deletes nothing.

Hourly is a reasonable schedule for the default 120-minute lifetime. An expired session then stays in storage for an hour at most.

Run Session Cleanup on Multiple Servers

With the database driver, every server that runs the scheduler runs the same delete. Add onOneServer() so only one server runs it. It needs a shared cache store for its lock:

Schedule::command('session:garbage-collect')
->hourly()
->onOneServer();

If your app calls Schedule::alwaysOnOneServer(), you don't need to add it. See Running Laravel's Scheduler on Multiple Servers for that setup.

With the file driver, each server keeps its own session files, so run the command on every server. Only add onOneServer() if the sessions directory is on shared storage.

Delete Expired Sessions Before Laravel 13.39

On Laravel 13.38 and earlier, the command fails with Command "session:garbage-collect" is not defined. Schedule a closure that makes the same call as the command:

// routes/console.php
use Illuminate\Support\Facades\Schedule;
 
Schedule::call(function () {
app('session')->driver()->getHandler()->gc((int) config('session.lifetime') * 60);
})->name('delete-expired-sessions')->hourly()->onOneServer();

The closure gets the handler for your default session driver and calls its gc() method. The session.lifetime value is in minutes and gc() expects seconds, which is why the lifetime is multiplied by 60.

This works on Laravel 11, 12, and 13. A scheduled closure needs name() before onOneServer(), or Laravel throws an exception. On Laravel 10, add the closure to the schedule() method in app/Console/Kernel.php with $schedule->call().

Then set 'lottery' => [0, 100] as shown above.

Further Reading

Andrew Brown contributed session:garbage-collect in #62104, and Volodya Kurshudyan registered it with Artisan in #62150.

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Sponsored

acquaintsoft logo
Acquaint Softtech

Hire Laravel developers with AI expertise at $20/hr. Get started in 48 hours.

Visit Acquaint Softtech

The latest

View all →
Pollora: Run WordPress Inside a Laravel Application image

Pollora: Run WordPress Inside a Laravel Application

Read article
Scheduled Session Garbage Collection in Laravel 13.39 image

Scheduled Session Garbage Collection in Laravel 13.39

Read article
Deferred Schema Loading in Filament v5.8 image

Deferred Schema Loading in Filament v5.8

Read article
Sloppy: Static Analysis for AI-Written PHP image

Sloppy: Static Analysis for AI-Written PHP

Read article
Mastering Laravel Testing: A Video Course on Laravel Tests image

Mastering Laravel Testing: A Video Course on Laravel Tests

Read article
NativePHP Mobile Security Advisories: Update to 4.6.1 image

NativePHP Mobile Security Advisories: Update to 4.6.1

Read article