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 maxmemory kicks 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 FLUSHALL during 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.

InstancePurposePersistenceEviction Policy
ephemeralApp cache, HTTP cache, locks, incrementOffallkeys-lru
persistentSessions, carts, number rangesOn (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 BGSAVE process 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:

  • noeviction on the cache instance → Redis rejects write operations once maxmemory is reached. Shopware throws errors, the shop goes down.
  • allkeys-random instead of allkeys-lru → Redis deletes random keys instead of the least recently used ones. Frequently accessed cache entries get deleted while rarely used ones survive.
  • allkeys-lru on 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):

InstancePolicyWhy
Ephemeral (cache)allkeys-lruLeast recently used entries are evicted first, new writes always succeed
Persistent (sessions, carts, number ranges)volatile-lruOnly 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 flock instead 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=1 and 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=1 is 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

MistakeImpactFix
Single instance for everythingEviction deletes sessions/cartsSeparate ephemeral/persistent instances
Persistence on cacheOOM kill, stale cache after restartsave "", appendonly no
No maxmemoryOOM kill, swap usageSet explicit limit
Wrong eviction policyShop errors or data lossallkeys-lru (cache), volatile-lru (persistent)
Incomplete Shopware configRedis running but not fully usedConfigure all subsystems
?persistent=1 confusedNo data persistence despite assumptionConfigure server-side persistence
Cart migration forgottenCarts lost after Redis switchbin/console cart:migrate
No monitoringProblems go undetectedMonitor hit rate, memory, slow log
TCP instead of socketUnnecessary overheadUnix 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.