Search by

publicala / laravel-resilient-redis

fgilio

Retry transient phpredis connect failures in Laravel, and recover commands that lose their connection on every Laravel version.

Package info

github.com/publicala/laravel-resilient-redis

pkg:composer/publicala/laravel-resilient-redis

Statistics

Installs: 679

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v3.0.2 2026-10-14 21:18 UTC

README

Latest Version on Packagist CI License

A managed Redis or Valkey drops connections now and then: a pooled socket goes stale, a TLS handshake gets reset, a failover moves the primary. Laravel retries a read that loses its connection mid-command (on 13.28 and later), but it never retries the connect itself, so the request, job or Horizon worker that happened to open the connection fails.

This package retries a phpredis connect that fails fast and transiently, and recovers a command that loses its connection on every Laravel version it supports. Install it and every standalone phpredis connection uses it. No code changes.

composer require publicala/laravel-resilient-redis

What it does

Retries a connect that fails fast

Every connect Laravel's phpredis connector makes goes through the package: the first one, and each rebuild after a connection loses the server. A failed connect gets up to 2 more attempts, after a random wait of 100–200 ms, then 100–400 ms, when the failure is one that ends within milliseconds and usually clears by itself:

Failure Typical cause
read error on connection, Connection lost, went away during AUTH or SELECT The server dropped a pooled (persistent) socket
SSL: Connection reset by peer, SSL: Broken pipe A TLS frontend reset the handshake
Connection refused, a missing Unix socket The server is restarting
Temporary failure in name resolution The resolver failed for now

In one 30-day production sample, all 18 requests whose connect failed on a pooled socket the server had dropped succeeded on the second attempt, 110–170 ms later.

The forms above are matched by their message, so a lasting misconfiguration that produces one (a wrong port answers Connection refused) fails only after the retries: the time of two more attempts, plus at most 0.6 seconds of waiting between them.

The package never retries these failures:

  • A timeout. A connect or TLS handshake that timed out means the server or the network is overloaded. Another attempt doubles the wait and adds a handshake to the pile.
  • A stall. A failure that took a second or more (a server that accepts the socket and never answers) would only stall again. A DNS failure is the exception: the resolver takes its own timeout to fail, so it may be retried whatever its duration, within the window below.
  • A configuration error. Wrong credentials (WRONGPASS, NOAUTH), a certificate that does not verify, a server that does not speak TLS, a host name that does not exist.
  • Anything else. A failure the package cannot name is rethrown as is.

No attempt starts 6 seconds or more after the first one began. A resolver that times out after 5 seconds gets one retry, a slower one gets none, and a resolver that fails fast gets up to two.

Gives every connection a connect timeout

A connection with no timeout gets one of 2 seconds. Without it, a connect to an unreachable host waits for PHP's default_socket_timeout, 60 seconds by default, and holds the worker that long. A timeout you set, 0 included, is kept.

Recovers a lost connection

Laravel 13.28 and later rebuild a phpredis client that loses its connection mid-command and run a read once more. The package does the same on every Laravel version, in place of Laravel's own recovery, and more conservatively:

  • Reads only. A read (GET, HGETALL, ZRANGE and the like) runs once more on the rebuilt client. A write (SET, INCR, EVAL, a queue push) is never replayed, because the server may have run it before the reply was lost. The client is still rebuilt, so the next command works.
  • Never inside a WATCH, a MULTI or a pipeline. The watch dies with the old socket, and a queued command would run outside its batch, so the error is rethrown after the rebuild. When the package cannot tell whether a WATCH is still open (one a pipeline sent, or an UNWATCH a pipeline queued), it treats the watch as open, which only costs the replay.
  • The rebuilt client keeps its state. The prefix, serializer and compression a caller set at runtime, and a database chosen with SELECT, carry over. withoutSerializationOrCompression() stays raw across a rebuild.
  • The original error wins. When the rebuild itself fails, the command's own error is rethrown, since for a write it is the one that says the server may have applied it.

TLS resets that phpredis reports as a PHP warning (Redis::get(): SSL: Connection reset by peer) count as a lost connection, as they do on Laravel 13.31 and later.

A protocol error (protocol error, got 'X' as reply type byte) counts as a lost connection too. phpredis raises it when the reply stream goes out of step, typically when the connection breaks mid-reply, and it leaves the socket open. A connect that fails this way is not retried, since a server that does not speak Redis answers the same.

A lost client that still holds its socket is closed before it is replaced. Otherwise phpredis returns a persistent client's socket to its pool, where the next connect would reuse it.

Laravel's command_retries is not used while the recovery is on, so no write is replayed. Turn recover_lost_connections off to keep Laravel's own recovery instead.

Reports each recovery

Each recovered connect dispatches Publicala\ResilientRedis\Events\ConnectRecovered, and each read the package runs again after a lost connection dispatches Publicala\ResilientRedis\Events\CommandRecovered. A pipeline or transaction opened again is not reported, and neither is a recovery inside a recovery listener. By default the package also logs one warning per recovery:

redis.connect.recovered {"host":"tls://cache.example.com","port":6379,"database":0,"persistent":true,"attempts":2,"milliseconds":143,"reason":"read_error"}
redis.command.recovered {"connection":"cache","command":"get","milliseconds":12,"reason":"connection_reset"}

The events carry a reason code, never the exception text, arguments or credentials. A failing listener is reported and never changes the outcome of the Redis call.

Replacing a custom connector

If your application registers its own phpredis connector (Redis::extend('phpredis', ...) or a RedisManager::extend() call in a service provider), remove it when you install the package. Both register the same driver, and the one registered last wins.

Configuration

The defaults need no configuration. To tune the connect retry, set these keys on a connection in config/database.php:

'redis' => [
    'client' => 'phpredis',

    'default' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        // ...
        'timeout' => (float) env('REDIS_TIMEOUT', 2.0),
        'connect_retries' => 2,          // attempts after the first one, 0 turns the retry off
        'connect_retry_window' => 6.0,   // seconds after the first attempt within which a retry may start
    ],
],

Laravel merges a connection's keys with redis.options and then with the connection's own options, in that order, so a key in redis.options applies to every connection and overrides the connection's top-level value. To override it for one connection, set it in that connection's options.

The package's own switches live in config/resilient-redis.php. Each one reads an environment variable, so you rarely need to publish the file:

Key Environment variable Default Effect
enabled RESILIENT_REDIS_ENABLED true When false, phpredis uses Laravel's stock connector, as if the package were not installed
recover_lost_connections RESILIENT_REDIS_RECOVER_LOST_CONNECTIONS true The lost-connection recovery. When false, phpredis connections keep Laravel's own handling
log_recoveries RESILIENT_REDIS_LOG_RECOVERIES true One warning per recovery
php artisan vendor:publish --tag=resilient-redis-config

A changed switch takes effect once the config cache is rebuilt and long-running workers (queue, Horizon, Octane) restart.

Recommended connection settings

  • timeout: keep it short (2 seconds). It bounds the TCP connect and the TLS handshake. It does not bound name resolution or the replies to AUTH and SELECT.
  • read_timeout: set one (for example 5 seconds) when a request has a hard deadline, such as Octane's max_execution_time, and the connection runs no blocking commands (BLPOP with a timeout, a queue with block_for). Without it, a reply that never comes waits for default_socket_timeout.
  • persistent: reusing sockets across requests saves a TLS handshake per request. A pooled socket the server dropped is the failure this package retries best.

Limits

  • phpredis only. Predis connections are left alone.
  • Redis Cluster connections use Laravel's stock path.
  • Outside the recovery: scan and its variants, subscribe, calls made on client() directly, and a pipeline or transaction opened without a callback (it returns the raw client). executeRaw() rebuilds the client but is never replayed.
  • phpredis retries on its own. It reconnects an idle socket it finds closed before sending a command (max_retries, 10 by default), without going through Laravel's connector, so the package neither retries nor reports those reconnects. A max_retries you configure is applied after the connect, which Laravel 11 does not do.
  • A SELECT sent inside a pipeline or transaction whose socket drops while it runs is not carried over. phpredis reports no database once the socket is gone, so the rebuilt client keeps the last database confirmed before the batch.
  • A SELECT inside a transaction that runs inside a pipeline is not tracked by phpredis, which keeps reporting the database from before the pipeline. A rebuilt client returns to that database, as phpredis's own reconnect does.
  • A failed SELECT or AUTH that phpredis reports as false instead of an exception still goes unnoticed, as it does in Laravel.

Compatibility

The package supports the versions publica.la runs in production, CI and local development, and adds a version when one of them adopts it.

Supported
PHP 8.3, 8.4, 8.5
Laravel 11.48 and later 11.x, 12.64 and later 12.x, 13.23 and later 13.x
phpredis 6.0.1, 6.3.0
Server Valkey 8.1.2, 9.0.0

PHP 8.3 runs with Laravel 11 and 12, PHP 8.4 with Laravel 12 and 13, and PHP 8.5 with Laravel 13. CI tests each of these pairs, the oldest Laravel release of each major, and the latest.

phpredis 5 is not supported: after a read error it still reports the client as connected, and close() reconnects it. Ubuntu 24.04 packages phpredis 5.3.7, so on its stock PHP install 6.3 with pecl install redis or from the ondrej/php PPA.

Testing

The suite drives real phpredis clients against a scripted RESP server it starts itself, and against a real Valkey or Redis on port 6390:

docker run --rm -d -p 6390:6379 valkey/valkey:9.0.0
composer test

Changelog

See CHANGELOG.md.

License

MIT. See LICENSE. The lost-connection recovery adapts code from the Laravel framework, also MIT.