#4656·civitai

redis.packed codec: ~63% of a decompress is threadpool dispatch, not compression — make sync/async size-conditional

Author: ZacxDevCreated Sep 6, 2026Updated Sep 6, 2026

Summary

redis.packed's brotli codec runs every value through util.promisify(zlib.brotli*), i.e. one libuv threadpool round trip per value. For the small values the compressed caches actually hold, the dispatch costs more than the compression does.

Measured on real cached values (n=48, compressed p50 883 B / uncompressed p50 1,671 B), timed through the same promisified shape packed-compression.ts uses:

p50 p90
dispatch floor (1-byte payload — the round trip with ~no codec work) 17.7 µs 38.6 µs
decompress, async (what ships today) 21.9–30.2 µs 41.9–66.2 µs
decompress, sync (identical work, no dispatch) 11.1 µs 17.6 µs
compress q6, async 111.6 µs 216.3 µs

~63% of every async decompress sample is threadpool dispatch, not codec work. At production read volume that is roughly 2.7× more wall time than the compression itself costs, and it is paid per keypacked.mGet is Promise.all(keys.map(get)), so a single cache call fans out to ~90 independent threadpool round trips.

Why not simply make it sync

The async choice is deliberate and packed-compression.ts documents why: a worst-case tensor-metadata value is ~335 KB and measures ~36 ms compress / ~5 ms decompress. Running that synchronously would block the event loop, which is exactly what the current design avoids. That reasoning is correct and should not be reverted.

Proposal

Pick sync or async by payload size, not globally:

  • below a threshold (~64 KB is a safe starting point; the real caches sit at ~1–4 KB), use brotliDecompressSync / brotliCompressSync — at 11 µs p50 / 17.6 µs p90 this is orders of magnitude below any long-task threshold and removes ~19 µs of dispatch per call;
  • at or above it, keep the promisified path exactly as it is today, so the large-value protection is untouched.

Both branches produce identical bytes, so this is not a format change and needs no sentinel work or key-bust.

Worth checking while in here

The threshold should be chosen against a measured distribution rather than assumed — the two caches that opt into compress today have very different value-size profiles, and only one of them motivated the async design.

Closing condition

Closed when either a merged PR makes the codec path size-conditional and a follow-up measurement shows the decompress p50 at or below the sync figure above, or a written decision in this issue declines it with the reason. Checked by whoever next edits packages/civitai-redis/src/packed-compression.ts.

Context

Found while measuring the cost of #4649 (brotli on the image-meta cache) for #4654 (the codec duration histogram). Neither PR is blocked on this, and it should not be folded into either — #4654 is an observability change and this is a hot-path optimisation.