Stateless vs stateful — what each means for HTTP and your API
Concept
Stateless vs stateful — two fundamentally different models for how a server handles a series of requests from the same client.
Stateless: Each request is SELF-CONTAINED. The server processes it without reference to any previous requests. No stored context between requests. Every request carries everything needed: authentication, session ID, user preferences, etc.
Stateful: The server remembers context between requests. A client establishes a "session" — the server stores data about that client's state across requests. The server and client share a continuous context.
HTTP is inherently stateless: Each HTTP request is independent. The server doesn't know or care that you sent a request 2 seconds ago. Stateful behavior must be layered on top (sessions, cookies).
Stateless APIs (REST):
- Auth: JWT token in every request (not stored on server).
- Context: All parameters in every request.
- Benefits: Horizontal scaling — any server can handle any request (no sticky sessions). Simpler servers. Easier to cache.
- Drawbacks: Larger requests (must include auth every time). Token revocation is hard (can't invalidate a JWT without a denylist).
Stateful APIs:
- Auth: Session cookie (server stores session).
- Context: Server remembers what you were doing.
- Benefits: Smaller requests (session ID is tiny). Easy revocation (delete session).
- Drawbacks: Requires shared session storage for horizontal scaling (Redis). All servers need access to the session store. Sticky sessions or shared storage required.
WebSockets: Stateful by nature — a persistent connection where the server maintains state about connected clients.
"Stateless" in microservices: Each service must be stateless (no shared state between instances). State is externalized to Redis, databases, or event stores.
Code Example
<?php
// STATEFUL — server stores session, client sends session ID
// Traditional web app (not RESTful)
// config/session.php: 'driver' => 'redis' (shared store for all servers)
Route::post('/login', function (LoginRequest $request) {
if (!Auth::attempt($request->only('email', 'password'))) {
return back()->withErrors(['email' => 'Invalid credentials']);
}
$request->session()->regenerate();
// Server stores: session_id → {user_id: 42, csrf_token: 'abc', cart: [...]}
// Client gets: Set-Cookie: laravel_session=base64(session_id)
return redirect('/dashboard');
});
Route::get('/dashboard', function () {
$user = auth()->user(); // found via session lookup
// Server: read session by cookie → get user_id → query DB → return user
return view('dashboard');
});
// STATELESS — each request self-contained
// API — each request includes JWT with user ID
Route::post('/api/login', function (LoginRequest $request) {
$user = User::where('email', $request->email)->first();
if (!$user || !\Hash::check($request->password, $user->password)) {
return response()->json(['error' => 'Invalid credentials'], 401);
}
// No session created! Client stores the token.
$token = \Tymon\JWTAuth\Facades\JWTAuth::fromUser($user);
return response()->json(['token' => $token]);
});
Route::middleware('auth:api')->get('/api/profile', function (Request $request) {
// Token decoded from Authorization header — no server session lookup
return response()->json($request->user());
});
// STATELESS HORIZONTAL SCALING
// Server 1 handles request → no session to sync
// Server 2 handles next request → JWT validated independently
// Any server handles any request — no sticky sessions needed!
// STATEFUL HORIZONTAL SCALING PROBLEM
// Server 1 stores session in memory → client must hit Server 1 again!
// Solution: shared session store (Redis)
// config/session.php: 'driver' => 'redis'
// config/database.php: 'redis' → shared Redis cluster
// Now any server reads the session from Redis
// WebSocket — inherently stateful
// \Ratchet\WebSocket\WsServer handles persistent connections
// Each connected client has state (user ID, room, message history)
// Scaling WebSockets requires sticky sessions or a pub/sub broker (Redis)