illuminate/session
Official Laravel session management package. Provides a unified API for storing user data across requests with drivers for file, database, Redis, Memcached, and arrays, plus flash data, session middleware, and CSRF token support.
If you're in a Laravel app, do nothing — illuminate/session is already installed, auto-discovered, and wired up via SessionServiceProvider, the StartSession middleware in the web group, and the Session facade alias. Your first stop is config/session.php, where you pick your driver (file by default, database/redis for multi-server) and cookie settings. The one file to touch before production is 'secure' => env('SESSION_SECURE_COOKIE') and same_site.
If you're outside full Laravel, resist installing this and instead evaluate Symfony's HttpFoundation session — this package is a subtree split and drags in illuminate/collections, contracts, filesystem, and Symfony 7.2+ components. If you must, register SessionServiceProvider, bind a SessionManager, and start the session in middleware yourself (or use $manager->driver() directly).
Your first real use case is almost always flash messaging on redirect:
return redirect()->route('posts.index')->with('status', 'Post created.');
and consuming it in Blade via @if (session('status')). Once you've shipped that, the natural next step is auth-adjacent flows (regenerate, invalidate) and storing wizard/cart state across multi-step forms.
Flash lifecycle: with() / flash() shows data on the next request only. Use flash()->keep() or reflash() to survive one more request (e.g., reshowing a form after validation failure), and now() to show on the current request too. Don't confuse these — most "my flash didn't show" bugs are a flash where a now was needed.
Auth flow boundaries: On login call $request->session()->regenerate() (new ID, fresh data) and on logout invalidate() + regenerateToken(). Store the pre-login URL with session(['url.intended' => $request->url()]) and use redirect()->intended() — this is the framework's own idiom and works with StartSession automatically.
Atomic read-and-remove: Prefer pull('key'), forget('key'), and increment()/decrement() over get-then-put. These keep the store's internal state consistent and avoid the classic double-submit bug.
Structured state: Use dot notation for nested data (session(['user.prefs.theme' => 'dark'])) or push() to append to arrays without fetching first. For carts/wizards, store a single well-shaped array under one key and mutate it — this makes serialization predictable.
Middleware integration: Session is available in web middleware (it starts after StartSession). If you write custom middleware that touches session, make sure it's registered after StartSession in the web group, or call $request->session() lazily inside handle() after $next.
Custom drivers: Implement SessionHandlerInterface and register it in a service provider:
Session::extend('custom', fn ($app) => new CustomHandler(...));
This is the cleanest extension point for DynamoDB, encrypted backends, or shared-memory stores that aren't in Laravel core. Add the driver name to config/session.php.
Driver selection by environment: array for tests, file for single-server dev, database or redis for anything behind a load balancer. Set this via SESSION_DRIVER in .env per environment; never share a file driver across nodes.
Testing: Feature tests can assert with assertSessionHas, assertSessionHasErrors, assertSessionMissing. For unit tests use the array driver and $this->withSession([...]) on the request. Laravel resets state per test, but if you're manually instantiating the Store, call flush() between cases.
Livewire / Inertia / API-adjacent flows: Session still gates CSRF and flash data for these. Inertia specifically reads flash out of the shared props — keep flash keys consistent (status, error) across controllers so your layout component can read them uniformly.
The array driver in production is the most common self-inflicted wound. It's designed for tests, holds nothing across requests, and fails silently — no exception, just "my session is empty on the next page."
has() vs exists() vs filled(): has() returns false when a key exists with a null value; exists() returns true for the same key; filled() returns true only for non-empty strings/arrays. Picking the wrong one causes subtle "the flag is there but I can't see it" bugs. When in doubt, use exists() for presence and filled() for "usable value."
regenerate() vs migrate(): regenerate() destroys the session data and issues a new ID (correct on login). migrate() keeps data with a new ID (correct when changing privileges but preserving state). Using migrate() on login leaves pre-auth data alive — a fixation risk.
Concurrency and locking: Long-running requests holding a file or database session block other requests for the same session until release. Laravel's handlers support blocking (block()), but if you're spawning AJAX polls against the same session, consider Redis or a stateless token for those endpoints. Calling Session::save() mid-request is a valid escape hatch for slow operations.
Serialization surprises: The default serializer is serialize(); complex objects must be autoloadable on the next request. Anonymous classes and closures blow up on unserialize(). If you're moving data between deploys or environments, set 'serialization' => 'json' in config/session.php — but then keep stored values JSON-safe (no objects, no resources).
CSRF is bound to the session: flushing the session or calling invalidate() drops the CSRF token, so the next form submission fails with a 419. This is expected on logout, but a footgun if you flush() in a middleware for "cleaning up." Regenerate the token only when appropriate, not on every request.
Session isn't available in queue jobs, artisan commands, or scheduled tasks — there's no request context. Pass the data you need into the job's constructor rather than trying to read it from session inside handle().
Octane / long-lived workers: session stores are request-scoped, but any static or singleton you build around them isn't. If you cache SessionManager or driver instances in a custom class, reset them in the Octane RequestReceived/RequestTerminated listeners. Symptom: session data bleeding between users on the same worker.
Cookie config trumps driver config: SESSION_DOMAIN, SESSION_SECURE_COOKIE, and SESSION_SAME_SITE control whether the browser sends the cookie at all. A correctly configured Redis driver with a wrong domain will look like "sessions are broken" for a full afternoon. Check the cookie in DevTools before debugging handlers.
Debugging checklist when a session "isn't persisting": (1) request is in the web group, (2) driver isn't array, (3) storage/framework/sessions is writable for file, (4) sessions table exists and migrated for database, (5) cookie domain/secure flags match the URL, (6) response didn't call invalidate() or flush() on the way out.
How can I help you explore Laravel packages today?