Home About Me

Redis and PHP: The Layer Between Your Code and the Server

A lot of PHP developers never get past "install it, make it run." The extension goes in, a couple of values land in .env, the app boots, and that's the end of it. What Redis actually is, how PHP reaches it, and where to start digging when something breaks tends to stay fuzzy.

The usual mistake is treating Redis as a black box: an error shows up, restart the service, and if the service didn't die, you're out of ideas. In reality, the Redis path in a PHP project has at least three layers: the Redis server itself, the PHP extension or client that speaks to it, and the framework configuration that turns that connection into cache, queues, and sessions. A problem in any of these layers produces more or less the same symptoms, and the root cause is frequently not Redis at all.

Start with the concepts, then walk through a typical broken-link scenario — once the mechanics are clear, the direction to look becomes obvious.

What Redis is, beyond "fast"

Redis (Remote Dictionary Server) is, at its core, an in-memory data structure server. Three words matter here:

  • In-memory: data lives in RAM by default, which is why reads and writes take microseconds — and why a restart wipes everything unless persistence is enabled.
  • Data structure: it stores far more than string key-values. Strings, hashes, lists, sets, sorted sets, and streams are all built in, each with its own atomic operations.
  • Server: it runs as a separate process and serves clients over the RESP protocol. Any language that wants to talk to it needs a client that implements RESP.

Redis's speed comes from three design decisions: everything happens in memory, a single-threaded event loop avoids context switching and locking, and I/O multiplexing lets one thread handle a large number of concurrent connections. Those same choices set a high performance ceiling — and also mean Redis is a bad fit for heavy computation, because one slow query blocks the entire thread.

Once that sinks in, the boundaries become clear. Redis suits workloads that are read/write heavy, latency sensitive, and tolerant of data loss or rebuild. It does not suit complex queries, and it is not a system of record.

Choosing the right type beats optimizing later

Redis isn't a database where you store something carelessly first and optimize afterward. The data type you pick decides which operations you can perform and how fast they run.

<table> <thead> <tr> <th>Type</th> <th>Underlying structure</th> <th>Typical use</th> </tr> </thead> <tbody> <tr> <td>String</td> <td>Simple dynamic string (SDS)</td> <td>Caching serialized objects, counters, distributed locks</td> </tr> <tr> <td>Hash</td> <td>Hash table / ziplist</td> <td>Object fields (user profiles, config values); more compact than storing JSON</td> </tr> <tr> <td>List</td> <td>Doubly linked list / quicklist</td> <td>Message queues, recent-activity timelines</td> </tr> <tr> <td>Set</td> <td>Hash table / intset</td> <td>Deduplication, tagging, mutual connections</td> </tr> <tr> <td>Sorted Set</td> <td>Skiplist + hash table</td> <td>Leaderboards, delayed queues, feeds ordered by time</td> </tr> </tbody> </table>

A common habit is stuffing JSON into Strings for every cache. Take a user profile: storing user:1001 → {"name":"Zhang San","age":28} works, but you can't update the age field on its own — you overwrite the whole blob. With a Hash, user:1001 → {name: Zhang San, age: 28}, you can read and write single fields, and the memory footprint is smaller, since a Hash with few fields uses a ziplist instead of duplicating keys across redundant JSON.

Picking the right type often does more for you than adding machines or tuning parameters.

Persistence: how much data are you actually willing to lose

Redis keeps data in memory — so what happens when the power goes out?

Two persistence mechanisms exist, usable alone or together:

  • RDB (snapshots): periodically writes the full in-memory dataset to dump.rdb. Restores are fast and the file is compact, but anything written between two snapshots can be lost, and the fork required for a full write is not free.
  • AOF (append-only file): appends every write command to a log and replays it on restart. Data is safer (flushing can be configured per command), but the file is larger than an RDB and recovery is slower.
  • Hybrid persistence (Redis 4.0+): during an AOF rewrite, the current snapshot is written as RDB data and subsequent increments are appended as AOF, balancing recovery speed against durability.

In production, enabling both RDB and AOF is the usual recommendation: RDB for cold backups, AOF so writes aren't lost. But if Redis only serves as pure cache — page fragments, temporary sessions — you can skip persistence entirely and reload from the database when data disappears. Which loops back to the earlier point: when the data in Redis can be lost and rebuilt, the system design holds up.

How PHP connects: a link you never see

Redis is a separate service. PHP has no native understanding of it and needs a connector. There are two mainstream options.

The phpredis extension (written in C, compiled into PHP)

  • Exposes a native Redis class
  • Fast, low memory overhead, supports nearly the full command set
  • Downside: it must be compiled and installed, it's tied to a PHP version, and upgrading PHP means reinstalling it

Predis (pure PHP, installed via Composer)

  • No extension dependency, trivial to deploy
  • Supports automatic reconnection, clustering, and Sentinel
  • Downside: inherently slower, since every Redis command is a PHP function call

Neither is universally better. Choose phpredis for performance, Predis for deployment convenience. Laravel's default config contains this line:

'client' => env('REDIS_CLIENT', 'phpredis'),

Meaning: prefer phpredis, fall back to Predis if it isn't installed. The catch — if the config says phpredis but the extension isn't loaded in that environment, the moment execution reaches the Redis connection you get Class "Redis" not found. The Redis server is fine; PHP simply can't find the class definition. The broken link is between PHP and the extension, not the Redis service.

A real case: the full trace of a false alarm

Back to that Class "Redis" not found.

The situation was odd. The site worked normally, and both Redis caching and sessions were functioning. But the moment a deploy script ran, it complained that the Redis class didn't exist. At first glance the Redis service looked dead. It wasn't remotely.

Start with the basics:

# 1. Is the Redis service running
systemctl is-active redis

# 2. Does PHP CLI have the phpredis extension loaded
/usr/local/php/8.3/bin/php -r 'var_dump(class_exists("Redis"));'

# 3. Does the application framework start normally
/usr/local/php/8.3/bin/php artisan about --only=environment

The second step returned bool(false) — problem located: the command-line PHP had no redis extension loaded. But why was the website fine?

Here's the key: PHP-FPM and PHP CLI may read different configuration files.

PHP CLI reads:   /usr/local/php/8.3/etc/php-cli.ini
PHP-FPM reads:   /usr/local/php/8.3/etc/php.ini

On this server, the FPM php.ini had extension=redis.so loaded, so web requests worked. The CLI php-cli.ini did not, so every php artisan command run from the shell died during startup.

A sensible order for this class of problem:

  1. Confirm the Redis service is active.
  2. Confirm whether the current PHP entry point (CLI or FPM) has the redis extension loaded.
  3. Confirm whether the application config points at phpredis or Predis.
  4. Only then look at which line of business code triggers the Redis connection.

Don't restart Redis the instant you see an error mentioning it. Most of the time the service is healthy and the broken piece is the link from PHP to Redis.

Day-to-day maintenance habits

Pre-flight check in the deploy script. One line, almost no cost, and it blocks a whole category of silly incidents:

/usr/local/php/8.3/bin/php -r 'var_dump(class_exists("Redis"));'

This matters especially on servers with multiple PHP versions, control panels, or manually compiled PHP builds — which version has the extension and which ini file is actually in effect are not things to guess at.

Think about the data type before writing code. Is this value a single field updated frequently (Hash)? Does it need ordering (Sorted Set)? Does it only need deduplication (Set)? Getting the type right up front shrinks the maintenance burden considerably.

Be explicit about what Redis is for. Cache, queues, transient state, counters — yes. System of record — no. Articles, user profiles, orders, and comments belong in MySQL or PostgreSQL; Redis sits in front of them as an accelerator.

Keep FPM and CLI configuration in sync. This is one of the easiest traps to fall into. Have deploy scripts name the PHP binary and ini file explicitly rather than relying on environment variables.

Closing

Redis on its own is just an in-memory data structure service. PHP reaches it through an extension or a client, and the framework wraps that connection into cache, queue, session, and counter capabilities. Understanding that chain — Redis server → PHP extension or client → framework wrapper → application code — is worth more than memorizing any particular error message.

The next time a Redis-related error appears, ask three questions first:

  • Is the Redis service running?
  • Does the current PHP entry point have the Redis extension loaded?
  • Is the application configured for phpredis or Predis?

Answer those and you'll be looking in the right place.