Kai Ole Hartwig — Blog
Mattschwarzer Schubladen-Block mit acht identischen Fächern und Brass-Schildern, eines leicht aufgezogen, weiches Tageslicht.
Extension · moselwal/keyvalue-store

keyvalue-store — Redis/Valkey, production-grade.

Redis/Valkey integration for TYPO3 14 with production-ready cache backend, session backend, and distributed locking strategy. Includes Sentinel discovery, TLS/mTLS, and phpredis 6.3+. The cache backend resolves four anti-patterns from TYPO3 Core’s RedisBackend (KEYS, blocking DEL, sequential tag flushing, missing Sentinel/TLS support).

  • Composer package:composer require moselwal/keyvalue-store
  • TYPO3: ^14.0 · PHP: 8.5+ · ext-redis: >= 6.3 · GPL-2.0-or-later
Das Problem

TYPO3 caching is standard — but really production-ready?

With keyvalue-store

  • TYPO3 caching framework backends for pages, hash, RootlinePath, ImageSizes, etc.
  • Session storage in Redis/Valkey with replication awareness
  • Distributed locking across all nodes
  • Sentinel support out of the box
  • TLS and mTLS as a configuration option

Until now

  • Standard Redis backend without Sentinel awareness
  • Sessions in the database — unnecessary load
  • Locking as file lock or DB lock — not cluster-capable
  • TLS/mTLS configuration as custom hack

Four building blocks

Sentinel & mTLS

Sentinel discovery enables automatic failover. mTLS configuration for encrypted inter-service communication in container setups.

Distributed locking

Lua-EVAL + BLPOP pattern — atomic SET key NX EX ttl for tryLock(), server-side BLPOP instead of client polling for wait(). Cluster-correct, no polling overhead.

Session storage

Frontend and backend sessions in Redis/Valkey — takes load off the database and makes multi-node setups seamless.

Caching backends

Drop-in backends for all TYPO3 caching framework caches: pages, hash, RootlinePath, ImageSizes — including tag-based flushing.

Architecture

Classes/
├── Cache/Backend/
│   └── KeyValueBackend.php          (extends TYPO3 Core RedisBackend)
├── Session/Backend/
│   └── KeyValueSessionBackend.php   (implements SessionBackendInterface)
├── Locking/
│   └── KeyValueLockingStrategy.php  (implements LockingStrategyInterface)
└── Connection/
    ├── KeyValueConnectionFactory.php
    ├── SentinelResolver.php
    ├── TlsContextBuilder.php
    └── ValueObject/

 

All three TYPO3-facing components route their phpredis instantiation through KeyValueConnectionFactory — Sentinel, TLS, mTLS, and the phpredis 6.x backoff are configured in exactly one place.

Requirements

Configuration

The easiest path is via moselwal/typo3-configautoconfigureCaching() wires all three components consistently. Manual configuration in config/config.php:

Cache backend

 

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages'] = [
    'backend' => \Moselwal\KeyValueStore\Cache\Backend\KeyValueBackend::class,
    'options' => [
        'hostname'             => 'valkey.internal',
        'port'                 => 6379,
        'database'             => 3,
        'password'             => 'secret',
        'persistentConnection' => true,
        'defaultLifetime'      => 2592000,
        'compression'          => true,
        // TLS/mTLS (optional)
        'tls'                  => true,
        'ca_file'              => '/run/tls/ca.crt',
        'cert_file'            => '/run/tls/httpd.crt',
        'key_file'             => '/run/tls/httpd.key',
        // Sentinel (optional)
        'sentinel'             => true,
        'sentinel_host'        => 'sentinel.internal',
        'sentinel_port'        => 26379,
        'sentinel_service'     => 'mymaster',
    ],
];

 

Session backend

 

$GLOBALS['TYPO3_CONF_VARS']['SYS']['session']['BE'] = [
    'backend' => \Moselwal\KeyValueStore\Session\Backend\KeyValueSessionBackend::class,
    'options' => [
        'hostname'             => 'valkey.internal',
        'database'             => 1,
        'password'             => 'secret',
        'persistentConnection' => true,
        'persistentId'         => 'typo3-session-be',
        'prefix'               => 'typo3:sess:be:',
        'sessionLifetime'      => 3600,
    ],
];

 

Locking strategy

 

$GLOBALS['TYPO3_CONF_VARS']['SYS']['locking']['strategies'][
    \Moselwal\KeyValueStore\Locking\KeyValueLockingStrategy::class
] = [
    'hostname'             => 'valkey.internal',
    'database'             => 0,
    'password'             => 'secret',
    'persistentConnection' => true,
    'ttl'                  => 10,
];

 

Dependencies

PackageTypePurpose
ext-redis >= 6.3Requiredphpredis with v6 constructor API
moselwal/devDevShared QA tooling

How v4.x differs from TYPO3 Core's Redis backend

KeyValueBackend extends TYPO3's own RedisBackend and overrides four operations that produce server-side anti-patterns. All other operations (get, has, remove, findIdentifiersByTag) are inherited unchanged — Core already pipelines tag tracking there correctly.

OperationTYPO3 CoreKeyValueBackend v4.3.0
set()SETEX + SMEMBERS + optional MULTI/PIPELINE tag diffSingle Lua EVAL — atomic, 1 roundtrip
flush()KEYS prefix* + DEL — blocks all clients in the event loopSCAN + UNLINK batches — server stays responsive
flushByTag() / flushByTags()N× sequential fan-outOne sUnion + one pipelined UNLINK
collectGarbage()KEYS identTags:*SCAN loop
Connectionpconnect() only, no Sentinel/TLSKeyValueConnectionFactory — Sentinel, mTLS, jitter backoff
SerializerPHP-native, hard-codedConfigurable: php / igbinary / none / auto

Lazy connect (lazy=true default) — the TCP/TLS handshake is deferred to the first command. Bootstrapping 11 cache backends drops from ~25 ms to ~0.07 ms on a real mTLS Valkey setup (381×). OPT_SCAN = SCAN_RETRY is set on every connection so phpredis 6.x retries empty SCAN pages internally instead of returning false.

Benchmarks (container-side against Valkey/mTLS, phpredis 6.3.0)

Measured container-side against Valkey with mTLS. Numbers compare TYPO3 Core / v4.0.x with v4.3.0.

OperationCore / v4.0.xv4.3.0Factor
Bootstrap 11 caches25.1 ms0.07 ms381×
getAll() 500 sessions37.2 ms1.5 ms24.6×
renew() (session fixation)360 µs161 µs2.2×
Retry-Backoff (2 failures)162 ms31 ms5.1×
set() 1 tag353 µs264 µs1.3×
set() 5 tags421 µs266 µs1.6×
set() 10 tags421 µs286 µs1.5×
set() 20 tags582 µs299 µs1.9×
flushByTags(10 tags)4.1 ms1.2 ms3.3×
flush(10 k keys)7.4 ms9.5 ms−30 % wallclock, no event-loop block
collectGarbage(5 k keys)1.4 ms2.8 ms−50 % wallclock, no event-loop block

flush() and collectGarbage() are intentionally slower wallclock-wise for the caller. The trade-off: KEYS blocks every other client in the Valkey instance while it runs. With SCAN, the server can interleave other clients between batches — for a multi-site or multi-pod setup that is the more important property. Neither is a hot path (flush() is BE/CLI-triggered; collectGarbage() runs in the scheduler tick).

Serializer

KeyValueBackend defaults to PHP-native serialization. The serializer option lets you change this per cache backend:

ValueBehaviour
'php'(default)PHP-native, BC-safe, identical to v4.2.0
'igbinary'igbinary when ext-igbinary is loaded; falls back to PHP-native with a notice otherwise
'none'No phpredis-layer serialization — the caller serializes
'auto'igbinary if loaded, php otherwise (not recommended, see below)

⚠️ Switching the serializer requires a full cache flush of all affected cache databases. Existing payloads remain in the previous format and will fail to deserialize. Recommended deploy sequence:

 

# 1. Flush each affected Valkey DB
valkey-cli -n 3 FLUSHDB    # pages
valkey-cli -n 4 FLUSHDB    # hash
# ... repeat for each cache DB

# 2. Restart workers so connections re-initialise with the new option

# 3. Deploy the new typo3-config with the serializer change

 

Why auto is the wrong default: an image update shipping ext-igbinary would silently switch the on-disk format for any cache using auto — the next read of an old PHP-serialised payload would throw. Pinning the value explicitly ('php' or 'igbinary') makes the contract observable.

When igbinary is worth it: only for caches storing deeply nested arrays/objects (e.g. Extbase ClassSchema, Fluid template reflection). For string-content caches (rendered pages, large text blobs) or flat key/value caches (hash, imagesizes), the igbinary encoder overhead dominates the marginal payload-size gain — keep the default.

The session backend (KeyValueSessionBackend) uses JSON internally for debuggability via valkey-cli — this option has no effect there.

Source code & docs

Packagist

composer require moselwal/keyvalue-store

Package on Packagist →
Packagist

GitHub

Source code, issues, and changelogs. GPL-2.0-or-later.

View on GitHub →
GitHub
Nächster Schritt

Cluster setup or migration from the standard backend?

For migration from the standard Redis backend, Sentinel setup or mTLS configuration in production, I am happy to support you as a service.

Cluster-Setup besprechen

Oder direkt schreiben: kontakt@moselwal.de

Where I use this …

This package carries the cache and session layer in TYPO3 Kubernetes — without a central cache store no multi-pod setup runs cleanly. In the managed variant it runs as part of my AI-Ready CMS as a Service.