Shopware 6 & Redis: How Misconfigured Instances Hurt More Than They Help
Redis is the go-to tool in Shopware 6 for moving cache, sessions, and HTTP cache out of the database or file system, drastically improving performance. In practice, however, we regularly see setups where Redis becomes the bottleneck — not because Redis is slow, but because it's misconfigured.
This article covers the most common mistakes and how to avoid them.
Mistake 1: A Single Redis Instance for Everything
This is the classic. A single Redis instance on port 6379 handles cache, sessions, HTTP cache, cart persistence, number ranges, locks, increment storage, and the message queue.
Why this hurts:
- Cache data competes with session data for available memory. When
maxmemorykicks in and the eviction policy starts removing keys, those keys could be sessions or shopping carts — customers get kicked out of checkout or lose their cart. - A careless
FLUSHALLduring debugging wipes everything at once — sessions, carts, number range states. - Different data types require different persistence and eviction strategies. Cache data is volatile; carts and number ranges are not.
Solution: At least two separate Redis instances — ephemeral and persistent.
Since Shopware 6.6.8.0, named Redis connections have been supported directly in shopware.yaml:
# config/packages/shopware.yaml
shopware:
redis:
connections:
ephemeral:
dsn: '%env(REDIS_EPHEMERAL)%'
persistent:
dsn: '%env(REDIS_PERSISTENT)%'
REDIS_EPHEMERAL=redis://redis-cache:6379
REDIS_PERSISTENT=redis://redis-session:6380
These connection names can then be referenced across all subsystems — cache, sessions, cart, number ranges, locks, increment storage.
| Instance | Purpose | Persistence | Eviction Policy |
|---|---|---|---|
ephemeral | App cache, HTTP cache, locks, increment | Off | allkeys-lru |
persistent | Sessions, carts, number ranges | On (RDB + AOF) | volatile-lru |
Docker Compose excerpt:
services:
redis-cache:
image: redis:7-alpine
command: >
redis-server
--maxmemory 2gb
--maxmemory-policy allkeys-lru
--save ""
--appendonly no
volumes: [ ] # No volume — cache is volatile
redis-session:
image: redis:7-alpine
command: >
redis-server
--maxmemory 256mb
--maxmemory-policy volatile-lru
--appendonly yes
--appendfsync everysec
--save "3600 1" --save "300 100"
volumes:
- redis-session-data:/data
The Shopware docs recommend volatile-lru for the persistent instance — this only evicts keys that have a TTL set,
while keys without expiry (e.g. number ranges) are always preserved. This is the crucial difference from
allkeys-lru, which would also delete keys without a TTL.
Mistake 2: Persistence Enabled on the Cache Instance
By default, Redis starts with RDB snapshots enabled (save 3600 1 300 100 60 10000). This means Redis periodically
writes the entire dataset to disk — even if it's just volatile cache data.
Why this hurts:
- The
BGSAVEprocess forks the Redis process. With 2 GB of cache data, an additional 2 GB of RAM is briefly consumed through copy-on-write. On a VM with 4 GB of RAM, this can trigger an OOM kill. - Disk I/O from the dump puts unnecessary loads on the system, especially on shared hosting or small VPS instances with slow disks.
- On restart, Redis loads the dump and restores stale cache entries. This sounds beneficial but is actually counterproductive: after a deployment, the cache may contain outdated entries that Shopware serves instead of regenerating.
Solution:
# redis-cache.conf (ephemeral)
save ""
appendonly no
Two lines. No RDB, no AOF. The cache instance starts empty — exactly as it should. Shopware rebuilds the cache on demand.
For the persistent instance (sessions, carts, number ranges), persistence is mandatory:
# redis-session.conf (persistent)
appendonly yes
appendfsync everysec
save 3600 1
save 300 100
Mistake 3: No maxmemory Set
Without a maxmemory limit, Redis grows until the host's memory is exhausted. The Linux OOM killer then terminates the
Redis process — or worse: the MySQL process, because it consumes more RAM.
Why this hurts:
- Uncontrolled memory consumption leads to system instability.
- Swap usage makes Redis orders of magnitude slower. Redis and swap don't mix — a Redis instance that swaps is slower than a cache hitting MariaDB directly.
Solution:
Rules of thumb for a typical Shopware shop:
- Cache instance (ephemeral): 1–2 GB (depending on product count and category depth)
- Persistent instance: 128–256 MB (depending on concurrent users and carts)
Always set it explicitly:
maxmemory 2gb
maxmemory-policy allkeys-lru # for ephemeral
And monitor it. An INFO memory check belongs in your monitoring:
redis-cli -p 6379 INFO memory | grep used_memory_human
redis-cli -p 6379 INFO memory | grep maxmemory_human
Mistake 4: Wrong Eviction Policy
Redis offers several eviction policies. The choice directly affects shop behavior.
Common misconfigurations:
noevictionon the cache instance → Redis rejects write operations oncemaxmemoryis reached. Shopware throws errors, the shop goes down.allkeys-randominstead ofallkeys-lru→ Redis deletes random keys instead of the least recently used ones. Frequently accessed cache entries get deleted while rarely used ones survive.allkeys-lruon the persistent instance → Redis can delete number range keys that have no TTL. This leads to duplicate order numbers.
The correct mapping (per Shopware docs):
| Instance | Policy | Why |
|---|---|---|
| Ephemeral (cache) | allkeys-lru | Least recently used entries are evicted first, new writes always succeed |
| Persistent (sessions, carts, number ranges) | volatile-lru | Only keys with a TTL are evicted; keys without TTL (number ranges) are preserved |
The difference is subtle but critical: volatile-lru protects all keys without an explicit TTL from deletion. Sessions
and carts have TTLs and can be evicted under memory pressure. Number ranges have no TTL and remain safe.
Mistake 5: Incomplete Shopware-Side Configuration
Redis is running, the connection is established — but Shopware doesn't use it for all subsystems because the configuration was only partially done.
Typical mistakes:
- Only the app cache is on Redis, but session handler, cart storage, locks, and increment are forgotten.
- Lock DSN still points to
flockinstead of Redis → breaks in horizontal scaling setups. - Increment storage stays on MySQL → unnecessary DB load from locking queries.
Complete configuration with named connections (≥ 6.6.8.0):
# config/packages/shopware.yaml
shopware:
redis:
connections:
ephemeral:
dsn: '%env(REDIS_EPHEMERAL)%'
persistent:
dsn: '%env(REDIS_PERSISTENT)%'
cache:
invalidation:
delay: 0
count: 150
delay_options:
storage: redis
connection: 'ephemeral'
cart:
redis_url: '%env(REDIS_PERSISTENT)%'
number_range:
redis_url: '%env(REDIS_PERSISTENT)%'
increment:
message_queue:
type: redis
config:
url: '%env(REDIS_EPHEMERAL)%'
user_activity:
type: redis
config:
url: '%env(REDIS_EPHEMERAL)%'
# config/packages/prod/framework.yaml
framework:
cache:
default_redis_provider: '%env(REDIS_EPHEMERAL)%'
pools:
cache.object:
adapter: cache.adapter.redis
tags: true
cache.http:
adapter: cache.adapter.redis
tags: true
session:
handler_id: '%env(REDIS_PERSISTENT)%'
lock:
main: '%env(REDIS_EPHEMERAL)%'
If you don't need the increment storage (no use of admin live statistics), you can set it to array and eliminate the
load entirely:
shopware:
increment:
message_queue:
type: array
user_activity:
type: array
Mistake 6: Misunderstanding ?persistent=1
A classic seen in many tutorials:
shopware:
cart:
redis_url: 'redis://redis:6380?persistent=1'
The ?persistent=1 parameter has nothing to do with data persistence. It enables persistent connections —
connection pooling on the PHP side. This keeps the TCP connection to Redis open across multiple requests instead of
rebuilding it on every request.
Why the confusion hurts:
- Developers set
?persistent=1and assume data persistence is taken care of. The actual Redis persistence (RDB/AOF) is never configured. After a Redis restart, all carts are gone. - Conversely,
?persistent=1is omitted because persistence isn't wanted (cache instance). But connection pooling would have the most impact there, since cache requests are the most frequent.
Solution:
- Data persistence is configured on the Redis server side (
save,appendonly) - Connection pooling is configured in the DSN (
?persistent=1) — beneficial for both instances
Mistake 7: Forgetting Cart Migration
Shopware stores carts in MySQL by default. Switching to Redis requires an explicit migration, otherwise existing carts are gone after the config change.
# Migrate carts from MySQL to Redis
bin/console cart:migrate
The command reads carts from the database and writes them to the configured Redis instance. It automatically uses the
redis_url from shopware.yaml. The command is idempotent and can be included in your deployment script.
Without this step, all customers with active carts lose their items after the switch.
Mistake 8: No Monitoring
Redis hums along quietly. Nobody notices the hit rate is at 12%, memory is 98% full, or latency is at 50ms instead of 0.2ms.
Minimum monitoring:
# Check hit rate
redis-cli -p 6379 INFO stats | grep -E "keyspace_hits|keyspace_misses"
# Memory usage
redis-cli -p 6379 INFO memory | grep used_memory_peak_human
# Connected clients
redis-cli -p 6379 INFO clients | grep connected_clients
# Slow log
redis-cli -p 6379 SLOWLOG GET 10
The hit rate for the cache instance should be above 80%. If it's lower, either the TTL is wrong, the eviction policy is
misconfigured, or maxmemory is too low and Redis is constantly cleaning up.
For a proper setup: Prometheus with redis_exporter and a Grafana dashboard. The 30-minute setup investment saves hours
of debugging.
Mistake 9: TCP Configuration Ignored
Redis and Shopware run on the same host but communicate over TCP instead of Unix sockets. Or Redis runs on a separate host, but TCP keepalive and timeout are left by default.
When Redis runs on the same host:
Unix sockets instead of TCP eliminate the entire TCP stack overhead. At high throughput, these are measurable microseconds per request:
# redis-cache.conf
unixsocket /var/run/redis/redis-cache.sock
unixsocketperm 770
port 0
REDIS_EPHEMERAL=redis:///var/run/redis/redis-cache.sock
When Redis runs remotely:
tcp-keepalive 60
timeout 300
tcp-backlog 511
Without tcp-keepalive, dead connections can remain open for minutes and block the connection pool.
When using a Redis cluster, additionally set in php.ini:
redis.clusters.cache_slots=1
This skips the cluster node lookup on every connection.
Summary
| Mistake | Impact | Fix |
|---|---|---|
| Single instance for everything | Eviction deletes sessions/carts | Separate ephemeral/persistent instances |
| Persistence on cache | OOM kill, stale cache after restart | save "", appendonly no |
No maxmemory | OOM kill, swap usage | Set explicit limit |
| Wrong eviction policy | Shop errors or data loss | allkeys-lru (cache), volatile-lru (persistent) |
| Incomplete Shopware config | Redis running but not fully used | Configure all subsystems |
?persistent=1 confused | No data persistence despite assumption | Configure server-side persistence |
| Cart migration forgotten | Carts lost after Redis switch | bin/console cart:migrate |
| No monitoring | Problems go undetected | Monitor hit rate, memory, slow log |
| TCP instead of socket | Unnecessary overhead | Unix socket for co-located setups |
Redis is a powerful tool — but only when the configuration matches the use case. A misconfigured Redis is worse than no Redis at all because it introduces an additional point of failure while giving the impression that the performance work is already done.