Database Session creates another record after logging out
Chron
Test Engineer · 2024-01-09
I'm using Laravel w/ Fortify Here's my test:
Logout in Laravel is supposed to destroy the current session and invalidate the authenticated user. When using the database session driver, however, tests sometimes reveal that a new session record appears immediately after logout rather than the old one being removed. This behavior is not a Fortify bug so much as a consequence of how session IDs rotate and how garbage collection runs. Understanding the lifecycle lets you write tests that assert the right invariants and configure production cleanup without surprises.
Session Regeneration on Logout
Laravel regenerates the session ID during authentication state changes to prevent session fixation. When you call Auth::logout(), the framework clears the user identifier from session data but typically does not immediately delete the session row. Instead, a subsequent request from the browser carries a new session cookie, which creates a new database row. The old row persists until the session garbage collector prunes expired records, which by default runs on only a small percentage of requests.
Testing Session Behavior
Use RefreshDatabase to keep test state isolated and count session rows before and after logout. Assert that the authenticated session exists, then assert that after logout the authenticated record is cleared. Do not assert that the old row is deleted within the same request, because that is not how the database driver works. If you need immediate cleanup, call Session::where('id', '<>', $newId)->delete() in a test helper, but avoid doing that in production.
Garbage Collection
The database session driver relies on PHP's session.gc_maxlifetime and Laravel's session expiration configuration. Records older than the lifetime window are candidates for deletion, but actual deletion is probabilistic. To make cleanup deterministic, schedule an Artisan command that runs session:prune or deletes expired rows directly. This prevents the sessions table from growing without bound in long-running applications.
Authentication and Session Interactions
Fortify handles login and logout response events that other packages may observe. If custom middleware or event listeners recreate session data during logout, you can end up with an additional row even without a new browser request. Examine global middleware and event subscribers to ensure they do not write to session after Auth::logout(). Related debugging work for authentication lifecycle issues is discussed in Laravel 13 + Sanctum + Fortify: API Routes Redirecting, which covers how route middleware and session state interact when SPA and API concerns share the same Laravel installation.
Conclusion
A new session row after logout is expected when the session ID rotates. Test for user identifier removal rather than row deletion, schedule regular pruning, and audit event listeners that might write back to session during authentication transitions.
Related Posts
- Laravel 13 + Sanctum + Fortify: API Routes Redirecting
- Database Session creates another record after logging out
- Laravel Precog always returns success
These related posts cover authentication, testing precision, and Laravel session lifecycle behaviors.
Investigating Duplicate Session Records
When a duplicate session record appears after logout, enable Laravel's query log to inspect what happens during the logout request. The session driver's logout method should destroy the current session and regenerate the ID. If your Fortify logout route redirects too quickly, the session destroy may be deferred, and a new request before the redirect completes could write a new session row.
Also review your session configuration: SESSION_DRIVER=database stores data in the sessions table; SESSION_LIFETIME controls the cookie expiration. If the lifetime is very short, you may see many session rows being created and garbage collected, which is normal but can look like a leak in tests. Run your tests with DatabaseTransactions or DatabaseMigrations to isolate session behavior from test persistence.
Best Practices for Session Testing
Avoid creating custom Session models unless you have a specific reason. Use Laravel's session helper and assertSessionHas methods for tests. If you must inspect the database directly, use the session store to get the current ID and query by that ID, rather than assuming a model class. For authentication flows, Laravel 13 + Sanctum + Fortify: API Routes Redirecting and Laravel Precog always returns success cover related auth and middleware behavior.
Session Garbage Collection
Laravel's database session driver relies on PHP's garbage collection or a scheduled job to delete expired sessions. The probability of cleanup on each request is controlled by the probability/math.div settings. If cleanup never runs, the sessions table grows indefinitely. This is not a leak but a maintenance task.
In tests, RefreshDatabase rolls back migrations or re-runs them between tests. If your test asserts session count, the timing matters: a logout request may not have triggered garbage collection yet, so the old session row persists until the next request with the random cleanup trigger. Use Artisan::call('session:prune') in tests to force cleanup if needed.
Fortify Logout Nuances
Fortify's logout route invalidates the session via Auth::logout() and regenerates the token. If your test creates its own Session model and relates it to a User model, you may be conflating two different concepts: the session record Laravel manages internally versus a domain-specific Session model. Rename your domain model if it conflicts with Illuminate\Session\Store.
For broader Laravel testing topics, see Laravel Precog always returns success and Laravel 13 + Sanctum + Fortify: API Routes Redirecting.
Laravel Session Driver Behavior
Laravel's session configuration determines how sessions are stored and invalidated. When using the database driver, Laravel stores session data in a sessions table. The Session model (if you created one) is unrelated to Laravel's internal session handling unless you explicitly bound it. Creating a custom Session model in your App\Models namespace can cause confusion and conflicts with Laravel's core session management.
When a user logs out via Laravel Fortify, the session should be invalidated and the database record deleted. If a new record appears, it's likely that session regeneration is creating a new session ID but the old session record is not being properly garbage-collected. Ensure your sessions table has a proper index on the id column and that the session lifetime configuration matches your application's needs.
Testing Session Behavior with RefreshDatabase
In tests, RefreshDatabase migrates your database before each test, which can interfere with session assertions. If you're testing logout behavior, consider using DatabaseTransactions instead, or assert against the session store directly. Sometimes the issue is that your test is not actually logging out through Fortify's controller but through a custom route that bypasses session invalidation.
Make sure your SessionControllerTest uses the correct middleware and that the user is authenticated before attempting logout. Check that Fortify's logout response is not redirecting before the session is fully cleared.