Skip to content

Build1 publisher3 min readPublished

Zstd compression shrinks a Laravel full-page cache entry in Redis from 1.47MB to 169KB

Laravel apps caching whole pages in Redis can store a 1.47MB response in 169KB by turning on zstd in phpredis, a dev.to benchmark found. Every cache hit moves the full value from Redis to PHP, so the saving applies to each read as well as to memory.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying Zstd compression shrinks a Laravel full-page cache entry in Redis from 1.47MB to 169KB
Generated illustration

What happened

  • The authors' Redis monitor showed response-cache entries of about 25MB, while ordinary listing pages took 1.6 to 2.8MB each.
  • phpredis can compress every value on a connection with one config option, while predis has none, so predis apps have to compress inside the cache serializer.
  • On a compressed phpredis connection, Cache::increment() silently returns false unless pack_ignore_numbers is set, so the authors give the response cache its own connection.
  • Zstd at its default level added about 2ms to each write and about 0.3ms to each read of the article-page payload.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure On a cache store shared with the rest of the app, a few hundred uncompressed page variants can push Redis into maxmemory eviction, and the evicted keys can be sessions or locks.
  • constraint Compression is cheap to switch on and costly to back out of, because an uncompressed client cannot read compressed entries and disabling it means flushing the cache.
  • decision Apps on hosts that forbid PHP extensions are left with predis, where compressing in the serializer with gzcompress() matches zstd's size at about seven times the write cost.

The rendered page is only part of what the cache stores [10]. The write-up's examples use spatie/laravel-responsecache, the open-source full-page cache that Spatie maintains, though the Redis side applies to any cache that stores rendered HTML [4]. The package serializes each response to JSON with its status, headers and body [10]. The body is everything the browser receives: inline SVG icons, JSON-LD blocks, inline scripts, and a Livewire snapshot for every component on the page [10]. JSON escapes every quote and slash, so the stored string is larger than the HTML [11].

Key design multiplies the size. The authors' hasher keys each page by locale, currency and user, so one URL can hold dozens of guest entries, and every query-string variant gets its own set [12]. The 25MB entries came from a URL that no page links to [13]. It was still rendered and cached for every visitor who reached it [13]. To find the same problem in another keyspace, `redis-cli --memkeys` samples keys and reports the largest of each type by memory, and `MEMORY USAGE` gives the size of a single key [14].

How much compression saves depends on the markup [23]. The zstd result on the article page is a cut of about 89% [1]. That matches the post's own description of a ninth of the space at zstd's default level [19]. The listing pages did better under zlib, going from 2.71MB to 0.08MB, a cut of about 97% [2]. They repeat the same card markup many times and compressed about 33 times, where the article page compressed about 9 times [23]. For pages without that repetition, I'd size memory from the article figure.

The write penalty lands only on a miss, which already pays for a full render, and zstd and lz4 both decompress quickly enough that a hit stays under a millisecond [20]. lz4 fits better where write time matters more than memory [20]. Level 9 saves another 15% for five times the write cost, and the authors judged it not worth it [21]. The measurements assume Redis sits close to PHP, which is where uncompressed reads cost least [22]. With a managed Redis in another zone, or any link where bandwidth is the limit, uncompressed reads slow in proportion to their size and compressed ones barely change [22].

The client is one setting, `database.redis.client`, read from `REDIS_CLIENT`, and the framework default is phpredis [17]. According to the post, the two clients perform close enough on small values that the choice rarely matters, and for values the size of a page phpredis's built-in compression is the main reason to pick it [18]. Setting `compression` on a phpredis connection enables lzf, lz4 or zstd, but only for algorithms the extension was compiled with [6]. The PECL build asks about lzf and zstd when it compiles [24].

phpredis handles the rollout well. A compressed connection still reads old plain entries, so turning compression on needs no flush [8]. The post does not explain why increments fail on a compressed connection. I'd keep the response cache on its own connection even with `pack_ignore_numbers` set, so the compression setting applies only to the store whose values are whole pages.

What to watch

  • A published cause for the Cache::increment() failure on compressed phpredis connections, or a change in how pack_ignore_numbers is set by default.
  • A predis release with its own compression option, removing the need to compress in the cache serializer.
  • Measurements against a managed Redis in another zone, where the post expects uncompressed reads to slow in proportion to their size.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories