Or: Why “just use Redis” isn’t always the answer
“We’re scaling to multiple servers. Should we switch to Redis sessions?”
I’ve heard this question dozens of times. The answer is almost always “it depends”—but not in the frustrating, non-committal way. There are clear criteria that make the choice obvious once you know them.
The Four Options
Permalink to "The Four Options"Symfony supports four session handlers out of the box:
| Handler | Storage | Scaling | Persistence |
|---|---|---|---|
| NativeFileSessionHandler | Local filesystem | Single server | Survives restarts |
| PdoSessionHandler | Database (MySQL, PostgreSQL) | Multi-server | Survives restarts |
| MemcachedSessionHandler | Memcached server | Multi-server | Lost on restart |
| RedisSessionHandler | Redis server | Multi-server | Configurable |
Each has trade-offs.
File Sessions: The Reliable Default
Permalink to "File Sessions: The Reliable Default"# config/packages/framework.yaml
framework:
session:
handler_id: null # Uses PHP's default file handler
save_path: "%kernel.project_dir%/var/sessions/%kernel.environment%"
When to use:
- Single-server deployments
- Development environments
- Applications where simplicity matters more than scale
The good:
- Zero configuration
- Battle-tested reliability
- Built-in session locking (no race conditions)
- Survives server restarts
The bad:
- Can’t share sessions across multiple app servers
- Filesystem I/O can become a bottleneck
- Session files accumulate (needs garbage collection)
Production tip: If you’re on a single server and it handles your load, file sessions are perfectly fine. Don’t over-engineer.
Database Sessions: ACID Guarantees
Permalink to "Database Sessions: ACID Guarantees"# config/packages/framework.yaml
framework:
session:
handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler
// config/services.yaml
services:
Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
arguments:
- '%env(DATABASE_URL)%'
- lock_mode: 1 # LOCK_ADVISORY
When to use:
- Multi-server deployments where you already have a database
- Applications requiring transactional consistency
- When session data must survive infrastructure changes
The good:
- Works across multiple app servers
- ACID guarantees (sessions won’t corrupt)
- Configurable locking modes
- Sessions survive restarts and deployments
The bad:
- Adds database load
- Lock contention under high concurrency
- Slower than in-memory solutions
Lock Modes Explained
Permalink to "Lock Modes Explained"PdoSessionHandler offers three lock modes:
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
// No locking - fastest, but race conditions possible
PdoSessionHandler::LOCK_NONE
// Advisory locking - balanced approach
PdoSessionHandler::LOCK_ADVISORY
// Transactional locking - safest, but slowest
PdoSessionHandler::LOCK_TRANSACTIONAL
My recommendation: Start with LOCK_ADVISORY. Only drop to LOCK_NONE if
you’ve profiled and confirmed it’s a bottleneck—and you understand the race
condition risks.
Redis Sessions: The Scalable Choice
Permalink to "Redis Sessions: The Scalable Choice"# config/packages/framework.yaml
framework:
session:
handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler
// config/services.yaml
services:
Redis:
class: Redis
calls:
- connect: ['%env(REDIS_HOST)%', '%env(int:REDIS_PORT)%']
Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler:
arguments:
- '@Redis'
When to use:
- High-traffic applications
- Horizontal scaling with multiple app servers
- When you need sub-millisecond session reads
The good:
- Blazing fast (in-memory)
- Scales horizontally
- Can persist to disk (RDB/AOF)
- Supports clustering
The bad:
- No native session locking (this is a big one)
- Additional infrastructure to maintain
- Memory costs for large session data
The Locking Problem
Permalink to "The Locking Problem"This deserves its own article (coming next), but here’s the summary:
Redis does not perform session locking. You can face race conditions when accessing sessions. For example, you may see an “Invalid CSRF token” error. — Symfony Documentation
If your app makes concurrent AJAX requests that modify session data, Redis sessions can corrupt. The symptom: random logouts, lost flash messages, CSRF failures.
Workarounds:
- Implement application-level locking
- Use
session_write_close()early - Consider a locking Redis session handler (like the one from
snc/redis-bundle)
Memcached Sessions: Simple Caching
Permalink to "Memcached Sessions: Simple Caching"# config/packages/framework.yaml
framework:
session:
handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\MemcachedSessionHandler
// config/services.yaml
services:
Memcached:
class: Memcached
calls:
- addServer: ['%env(MEMCACHED_HOST)%', '%env(int:MEMCACHED_PORT)%']
Symfony\Component\HttpFoundation\Session\Storage\Handler\MemcachedSessionHandler:
arguments:
- '@Memcached'
When to use:
- Simple distributed caching needs
- When you already have Memcached for other caching
- Stateless-ish applications where session loss is acceptable
The good:
- Fast (in-memory)
- Simple protocol
- Works across multiple servers
The bad:
- No persistence (sessions lost on restart)
- No native locking (same race condition issues as Redis)
- LRU eviction can randomly delete sessions under memory pressure
My take: Unless you already have Memcached in your stack, prefer Redis. It does everything Memcached does, plus persistence and more data structures.
The Decision Matrix
Permalink to "The Decision Matrix"| Requirement | Best Choice |
|---|---|
| Single server, simple app | Files |
| Multi-server, need reliability | Database (PDO) |
| Multi-server, need speed | Redis (with locking awareness) |
| Already using Memcached | Memcached (but consider Redis) |
| ACID guarantees required | Database (PDO) |
| Sub-millisecond reads | Redis or Memcached |
| Session data must survive restarts | Files, Database, or Redis with persistence |
Migrating Between Handlers
Permalink to "Migrating Between Handlers"Switching session handlers usually logs everyone out. Symfony’s
MigratingSessionHandler solves this:
use Symfony\Component\HttpFoundation\Session\Storage\Handler\MigratingSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
// Read from old handler, write to both
$handler = new MigratingSessionHandler(
new PdoSessionHandler($pdo), // Old handler (reads from here first)
new RedisSessionHandler($redis) // New handler (writes go to both)
);
Run this for a session lifetime (e.g., 24 hours), then switch to Redis-only:
$handler = new RedisSessionHandler($redis);
Zero-downtime migration without logging anyone out.
My Recommendations
Permalink to "My Recommendations"For Most Applications
Permalink to "For Most Applications"Start with file sessions. If you’re not running multiple app servers, you don’t need distributed session storage. File sessions are reliable, fast enough, and have proper locking.
When You Scale
Permalink to "When You Scale"Move to database sessions first. You already have a database. The added load is usually negligible, and you get proper locking. Only switch to Redis if you’ve measured and confirmed the database is a bottleneck.
For High-Traffic Applications
Permalink to "For High-Traffic Applications"Use Redis, but:
- Understand the locking limitations
- Implement proper safeguards (more on this in the next article)
- Configure persistence (RDB snapshots at minimum)
The “best” session handler depends on your constraints. File sessions work fine for most applications. When you need to scale, database sessions are a sensible middle ground. Redis is the right choice for high-traffic apps—but only if you handle the locking problem.
Next up: Redis Session Locking Pitfalls—the race conditions that catch everyone off guard.
