The Serializer Nobody Configures: igbinary for Laravel Cache and Queue Payloads

Switch Laravel's Redis cache to the igbinary serializer safely: install the extension, set it per connection, measure the saving, cut over without data loss.

Steven Richardson
Steven Richardson
· 9 min read

The Redis bill grew by forty quid a month and nobody could say why. The usual moves — shorter TTLs, a bigger instance — were already on the table before anyone looked at how the bytes were actually encoded. They were encoded with PHP's serialize(), because config/database.php has a serializer option that is unset by default, documented in one line, and has no obvious consequence.

Switching it to igbinary cut the cache keys by about a third. It also would have poisoned the queue if I had applied it everywhere, which is the part the one-line documentation does not cover.

Measure what PHP's serializer is costing you#

Before touching any config, look at what serialize() writes for a structure you actually cache. The text format spells out every type marker and repeats every property name once per element, which is exactly the wrong shape for a collection of models.

// tinker: compare the two encodings for a realistic payload
$rows = User::query()->select('id', 'name', 'email')->limit(50)->get()->toArray();

$php = strlen(serialize($rows));
$ig  = strlen(igbinary_serialize($rows));

printf("serialize: %d bytes\nigbinary:  %d bytes\nsaving:    %.1f%%\n",
    $php, $ig, (1 - $ig / $php) * 100
);

On a 50-row array of three string columns I get roughly serialize: 6,214 against igbinary: 3,980 — about 36%. The gain comes from igbinary storing each repeated string ("name", "email") once and referencing it afterwards, where serialize() writes s:5:"email"; fifty times.

Run this against three payloads you genuinely cache: a cached query result, a cached Eloquent collection, and whatever your heaviest Cache::remember() call stores. If those numbers come out within 10% of each other, your payloads are dominated by long strings or pre-encoded JSON, and nothing below is worth your afternoon.

Install igbinary and rebuild phpredis#

This is the step people get wrong, because there are two extensions involved and only one of them is obvious. Installing igbinary gives you igbinary_serialize() in userland. It does not give phpredis the ability to use it — phpredis has to be compiled with igbinary support, which is off by default.

# 1. the igbinary extension itself
pecl install igbinary

# 2. phpredis, rebuilt with igbinary support compiled in
pecl install --configureoptions="enable-redis-igbinary='yes'" redis

The phpredis install docs list the same flag for a source build (./configure --enable-redis-igbinary). If you are on PIE rather than pecl — and you should be, for the reasons in installing PHP extensions with PIE — the stable package is pie install phpredis/phpredis, and you pass configure options the same way.

Load order matters in the .ini files. igbinary has to be loaded before redis:

; /etc/php/8.4/mods-available/00-igbinary.ini
extension=igbinary.so

; /etc/php/8.4/mods-available/20-redis.ini
extension=redis.so

Debian-style installs number the conf.d symlinks, so prefixing igbinary with a lower number is enough. Load each extension exactly once — enabling it in both php.ini and a conf.d file produces an "already loaded" warning on every CLI invocation.

Verify the serializer is actually available#

Do this before you change any Laravel config. If phpredis was not built with igbinary support, setting the option does not throw — Redis just keeps storing bytes the old way and you spend an hour wondering why MEMORY USAGE did not move.

php --ri redis | grep -i 'serializers\|igbinary'

You want to see igbinary listed among the available serializers:

Available serializers => php, json, igbinary, msgpack

If that line reads php, json only, the rebuild did not take. Check that pecl install actually recompiled rather than reusing a cached build, and that the CLI and FPM SAPIs are loading the same redis.so.

Configure the serializer per Redis connection#

Laravel's documentation shows serializer in the top-level options array, which applies it to every Redis connection at once. That is not what you want here, because the cache and the queue need different answers.

The per-connection override works, but only if you know the merge order. PhpRedisConnector::connect() does this:

$formattedOptions = Arr::pull($config, 'options', []);

return $this->createClient(array_merge(
    $config, $options, $formattedOptions
));

$options is the global redis.options array and $formattedOptions is the connection's own options sub-array — merged last, so it wins. Put the serializer in the connection's options block, not alongside host and port:

// config/database.php
'redis' => [

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

    'options' => [
        'cluster' => env('REDIS_CLUSTER', 'redis'),
        'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
        // Deliberately NOT set globally — see the queue step below.
    ],

    'cache' => [
        'url' => env('REDIS_URL'),
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD'),
        'port' => env('REDIS_PORT', '6379'),
        'database' => env('REDIS_CACHE_DB', '1'),
        'options' => [
            // Per-connection options are merged last and override the global block.
            'serializer' => Redis::SERIALIZER_IGBINARY,
        ],
    ],

],

Note the use Redis; at the top of the config file — Redis::SERIALIZER_IGBINARY is the phpredis class constant, not the Laravel facade. If you have the facade aliased, the constant resolves to the wrong class and you get a silent 0, which is SERIALIZER_NONE.

Cut over the cache without breaking existing keys#

Every key currently in the cache database was written with the old encoding. phpredis will hand the new deserializer bytes it does not understand, and you get a false or a mangled value rather than a clean exception:

PHP Notice: igbinary_unserialize_header: unsupported version

Two safe orders. The blunt one: deploy with the cache flushed.

php artisan down
# deploy the config change
php artisan cache:clear
php artisan config:cache
php artisan up

The one I prefer, because it needs no downtime: rotate the prefix. Old keys become unreachable under the new prefix and expire out on their own TTLs.

REDIS_PREFIX=myapp-database-ig-

Rotating costs you one cold-cache period and nothing else. It also leaves the old keys readable if you have to roll back within the TTL window, which flushing does not. Fold it into your normal release — the mechanics are the same as any config change in a zero-downtime deployment.

A caveat on prefix rotation: anything with a very long or absent TTL sits in memory until it is evicted, so check your maxmemory-policy is an allkeys-* variant before you rely on orphans disappearing.

Leave the queue connection alone#

This is the step where the honest answer is "don't". Laravel does not hand the queue connection an object graph. Queue::createPayload() returns a JSON string, with the job itself serialize()d into the data.command field inside that JSON.

So the value phpredis stores is a single string. igbinary wraps a string in a short binary header and copies the bytes — there is no repeated structure to collapse, so the saving is somewhere between nothing and negative. Meanwhile the risk is entirely real: a worker still running the old serializer that pops a job written by the new one fails at decode, not at execution. Retries do not help, because the payload never becomes readable. On a multi-server setup the window is wider still, for the reasons covered in scaling Laravel queues in production.

If you have measured a genuine win on your queue payloads and want it anyway, the cutover is a drain, not a deploy:

# 1. stop producers — pause schedulers and anything dispatching
php artisan down

# 2. drain to zero and confirm
redis-cli -n 0 LLEN queues:default   # must read 0

# 3. deploy the config, then restart workers so they pick it up
php artisan queue:restart
php artisan up

Step 3 is the one that bites. queue:restart only sets a flag; workers finish their current job and exit, and it is your process supervisor that starts the replacements with the new config. If you are running queue workers under Supervisor, confirm the new processes are actually up before you let producers back in — a half-restarted pool is exactly the mixed-serializer state you are trying to avoid.

Measure compression as a separate change#

Compression is a different option from serialization and the two stack. Turn both on at once and you will not know which one bought you the memory, or which one is costing you the CPU.

'options' => [
    'serializer' => Redis::SERIALIZER_IGBINARY,
    'compression' => Redis::COMPRESSION_ZSTD,
    'compression_level' => 3,
],

phpredis supports COMPRESSION_LZF, COMPRESSION_ZSTD and COMPRESSION_LZ4, each needing its own --enable-redis-* build flag. Change one knob, warm the cache, record the numbers, then change the next:

# per-key, on a representative key
redis-cli -n 1 MEMORY USAGE myapp-database-ig-:some:cached:key

# instance-wide, after a full warm cycle
redis-cli -n 1 INFO memory | grep -E 'used_memory_human|used_memory_dataset'

Compression is usually the bigger lever on payloads that igbinary cannot help — long strings, rendered HTML fragments, pre-encoded JSON blobs. It costs CPU on every read and write, which matters more than the memory if your Redis is already CPU-bound.

Watch for these gotchas#

Predis ignores all of this. The serializer and compression options are phpredis features; if REDIS_CLIENT resolves to predis, your config change is inert and nothing in the boot process will tell you. Check which client you are actually running before you configure anything.

igbinary does not fix non-serializable properties. Closures, open resources and anything with a hostile __sleep() fail under both encodings — just with a different error message. A queued job holding a PDO connection was broken before and is still broken.

Session and Horizon data live on Redis connections too. If you set the serializer globally rather than per connection, you also change how sessions are stored, which logs everyone out on deploy. Horizon's own metrics keys are mostly strings and gain nothing.

Clusters apply options through a different code path. connectToCluster() merges the cluster options before the connection's own options, so verify your effective serializer against the live client rather than by reading the config file.

Wrapping Up#

Run the byte comparison first. If a representative cached collection does not shrink by at least a quarter in tinker, close the tab — this is not where your Redis memory is going. If it does, set the serializer on the cache connection only, rotate the prefix on deploy, and leave the queue on SERIALIZER_NONE where the payload is a JSON string anyway.

Once the encoding is as small as it is going to get, the next lever is caching less rather than caching smaller — Laravel's memo driver kills the repeat round trips that make payload size matter in the first place. And if the instance itself is the line item you are trying to shrink, migrating from Redis to Valkey changes the bill without changing a line of application code.

FAQ#

What is igbinary and should I use it with Laravel?

igbinary is a PHP extension that serialises values to a compact binary format instead of PHP's verbose text format. It is worth using with Laravel when you cache structured data — arrays of models, query results, collections — because it stores repeated keys and strings once rather than per element. It is not worth using if your cached values are mostly long strings or pre-encoded JSON, where there is no repeated structure to collapse.

How do I change the Redis serializer in Laravel?

Set the serializer key in the options array of your Redis configuration in config/database.php, using the phpredis constant Redis::SERIALIZER_IGBINARY. Putting it in the top-level options block applies it to every Redis connection; putting it in an individual connection's own options sub-array applies it to just that connection and overrides the global value, because Laravel's PhpRedisConnector merges the connection options last.

Does igbinary reduce Redis memory usage?

It reduces the size of the values Laravel writes, which reduces Redis memory in proportion to how much of your dataset is structured PHP data. In practice I see 30–40% on cached collections of Eloquent models. The honest caveat is that the saving applies to the payload bytes only — key names, TTL metadata and Redis's own per-key overhead are unchanged, so the instance-wide figure is always smaller than the per-key figure.

Is it safe to change the serializer on a running Laravel app?

Not as a hot config toggle. The change makes every existing key unreadable, so it needs either a cache flush or a prefix rotation as part of the deploy. For queues it is worse than unsafe, it is pointless — workers running the old serializer cannot decode jobs written by the new one, and the payload is a JSON string that gains nothing from igbinary in the first place.

What happens to existing Redis keys when I change the serializer?

They stay in Redis but become unreadable. phpredis hands the new deserializer bytes it does not recognise, and you get false or a corrupted value back, often with an unsupported version notice rather than a clean exception. Rotating the cache prefix is the cleanest fix — the old keys are orphaned under the previous prefix and expire out on their existing TTLs.

Should I use compression or a different serializer for Laravel queue payloads?

Compression, if anything, and only after measuring. Laravel pushes queue jobs as a JSON string, so a structural serializer like igbinary has nothing to compress and changing it introduces a real decode-failure risk during the cutover. If queue payload size is genuinely a problem, the usual cause is a job carrying a large model or array it should be reloading from the database instead.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.