<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Fariz Prawira</title>
    <description>The latest articles on DEV Community by Fariz Prawira (@fariz_prawira).</description>
    <link>https://dev.to/fariz_prawira</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3828433%2F831fb9f5-28c9-43ae-9e7e-eb65f777f77b.png</url>
      <title>DEV Community: Fariz Prawira</title>
      <link>https://dev.to/fariz_prawira</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fariz_prawira"/>
    <language>en</language>
    <item>
      <title>How redis-packhash Helps You Leverage Listpack Hashes in Redis</title>
      <dc:creator>Fariz Prawira</dc:creator>
      <pubDate>Tue, 28 Jul 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/fariz_prawira/how-redis-packhash-helps-you-leverage-listpack-hashes-in-redis-117c</link>
      <guid>https://dev.to/fariz_prawira/how-redis-packhash-helps-you-leverage-listpack-hashes-in-redis-117c</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/fariz_prawira/cut-your-redis-memory-usage-by-up-to-90-1651"&gt;earlier post&lt;/a&gt;, I showed that storing a million small values as Redis keys is a waste of memory. It's because every key has its own overhead. To fix that, we need to group the keys into hashes and keep them small enough that Redis will keep them in the compact &lt;strong&gt;listpack&lt;/strong&gt; encoding. In my benchmark with 1M keys before, a 53.8 MB memory usage went down to 6.9 MB. I call it an 87% discount.&lt;/p&gt;

&lt;p&gt;The catch is you have to watch a lot of things. You decide how many hashes to split the keys across, and you need to spread the keys evenly across them. Those are some parts of the bookkeeping that you need to watch in order to get &lt;strong&gt;listpack&lt;/strong&gt; savings.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://www.npmjs.com/package/redis-packhash" rel="noopener noreferrer"&gt;redis-packhash&lt;/a&gt;, a client agnostic, no dependency npm package. You tell it how many keys you expect to store and that's it. It will decide how many hashes to use and spread every key across them evenly, behind a simple &lt;code&gt;get&lt;/code&gt;/&lt;code&gt;set&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;redis-packhash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No runtime dependencies. It works with &lt;a href="https://www.npmjs.com/package/ioredis" rel="noopener noreferrer"&gt;ioredis&lt;/a&gt;, &lt;a href="https://www.npmjs.com/package/redis" rel="noopener noreferrer"&gt;node-redis&lt;/a&gt;, and other Redis clients that expose &lt;code&gt;hset&lt;/code&gt;, &lt;code&gt;hget&lt;/code&gt;, and &lt;code&gt;hdel&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quickstart
&lt;/h2&gt;

&lt;p&gt;Bring your Redis client and hand it to &lt;code&gt;PackHash&lt;/code&gt;, and everything after that is a simple &lt;code&gt;get&lt;/code&gt;/&lt;code&gt;set&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PackHash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;redis-packhash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ioredis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Store each user's locale, one small value per user.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PackHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user:locale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedKeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;9f8c4a2b-1e77-4c3d-bd21-7a0e5f6c8d90&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Values are strings.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;//=&amp;gt; "en-US"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Initialize the store with your &lt;code&gt;expectedKeys&lt;/code&gt;, and you are ready to go.&lt;br&gt;
You need another dataset with a different use case? Spin up a new instance. Each one takes an optional &lt;code&gt;namespace&lt;/code&gt; that prefixes its Redis top level keys, so they will not collide.&lt;/p&gt;
&lt;h2&gt;
  
  
  The rest of the API
&lt;/h2&gt;

&lt;p&gt;redis-packhash only accepts string keys. You can pass a UUID, an email, or anything as long as it's string. It runs every key through the &lt;a href="https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function" rel="noopener noreferrer"&gt;FNV-1a hash function&lt;/a&gt; and buckets on the result, so the spread stays even whatever your ids look like. Values also need to be string. You pick the serialization, so it never silently reshapes your data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PackHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user:logins&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedKeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;logins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// stringify going in&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;logins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// and parse coming out =&amp;gt; 42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beyond &lt;code&gt;get&lt;/code&gt; and &lt;code&gt;set&lt;/code&gt;, you also get &lt;code&gt;has&lt;/code&gt; and &lt;code&gt;del&lt;/code&gt;, plus &lt;code&gt;mset&lt;/code&gt; and &lt;code&gt;mget&lt;/code&gt; for batches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4821&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;//=&amp;gt; false&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4822&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;//=&amp;gt; false&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mset&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4821&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4822&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fr-FR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mget&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4821&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4822&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;9999&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4821&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;//=&amp;gt; "en-US"&lt;/span&gt;
&lt;span class="nx"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;9999&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;//=&amp;gt; null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sizing is IMPORTANT
&lt;/h2&gt;

&lt;p&gt;The one decision that you make at the very beginning is IMPORTANT. Failed to fill the correct input means you are not save anything. That's the &lt;code&gt;expectedKeys&lt;/code&gt; param, which is how many keys you expected to store. Set it too low and buckets overflow and the encoding fallback to a hashtable, which means losing the saving entirely. Set it too high and you get more buckets than you need, which means cost you a little extra overhead. When in doubt, estimate high.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PackHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user:locale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedKeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;//=&amp;gt; 2605 total buckets&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;WARNING&lt;/em&gt;&lt;/strong&gt;: changing &lt;code&gt;expectedKeys&lt;/code&gt; on a store that already holds data will silently orphans every existing data. You either can migrate it manually before changing it, or spin a new instance if match your case.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Limits that you need to honor
&lt;/h2&gt;

&lt;p&gt;Listpack has two limits, and it will fallback from listpack to hashtable the moment it crosses either one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting (Redis 7.0+)&lt;/th&gt;
&lt;th&gt;Redis default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hash-max-listpack-entries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;Number of fields in the hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hash-max-listpack-value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;64 bytes&lt;/td&gt;
&lt;td&gt;Length of any single field name or value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;expectedKeys&lt;/code&gt; handles the first limit for you by compute the bucket count needed to achieve below the default 512. But the second limit is on you, at least for now.&lt;br&gt;
The 64 bytes limit applies to the field name and the value separately, not shared between them, and your top level key is not included.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Case #1: Value over 64 byte&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;9f8c4a2b-1e77-4c3d-bd21-7a0e5f6c8d90&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;America/New_York&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dateFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MM/DD/YYYY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// HSET user:locale:2312  "&amp;lt;36-byte key&amp;gt;"  &amp;lt;74-byte value&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;// bucket user:locale:2312 falls back to hashtable&lt;/span&gt;

&lt;span class="c1"&gt;// Case #2: Key over 64 byte&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tenant:acme-corp/region:eu-west-1/user:9f8c4a2b-1e77-4c3d-bd21-7a0e5f6c8d90&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// HSET user:locale:85  "&amp;lt;75-byte key&amp;gt;"  "&amp;lt;5-byte value&amp;gt;"&lt;/span&gt;
&lt;span class="c1"&gt;// bucket user:locale:85 falls back to hashtable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The entry limit, and your Redis version
&lt;/h3&gt;

&lt;p&gt;Rather than assuming your limits, just ask your own server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; CONFIG GET hash-max-listpack-&lt;span class="k"&gt;*&lt;/span&gt;
1&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="s2"&gt;"hash-max-listpack-entries"&lt;/span&gt;
2&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="s2"&gt;"512"&lt;/span&gt;
3&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="s2"&gt;"hash-max-listpack-value"&lt;/span&gt;
4&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="s2"&gt;"64"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before Redis 7.0 it was &lt;code&gt;hash-max-ziplist-*&lt;/code&gt; and the encoding was &lt;code&gt;ziplist&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you change your default &lt;code&gt;hash-max-listpack-entries&lt;/code&gt; or it has different value in your server for whatever reason, you can set it manually using &lt;code&gt;maxListpackEntries&lt;/code&gt; option.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PackHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user:locale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedKeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxListpackEntries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// match your config&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  That is the whole thing
&lt;/h2&gt;

&lt;p&gt;For now, that's what this library supports. I've some things in mind to improve it, and if do too, I'm more than happy to hear them.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>node</category>
      <category>npm</category>
      <category>performance</category>
    </item>
    <item>
      <title>Cut your Redis memory usage by up to 90%</title>
      <dc:creator>Fariz Prawira</dc:creator>
      <pubDate>Wed, 24 Jun 2026 06:07:24 +0000</pubDate>
      <link>https://dev.to/fariz_prawira/cut-your-redis-memory-usage-by-up-to-90-1651</link>
      <guid>https://dev.to/fariz_prawira/cut-your-redis-memory-usage-by-up-to-90-1651</guid>
      <description>&lt;p&gt;If you treat your Redis as a bag of single String keys, I have great news for you. By utilizing &lt;strong&gt;listpack&lt;/strong&gt;, you can cut your Redis memory usage by up to 90% without dropping a single byte of your actual data. Let's assume your key is look like this &lt;code&gt;user:1203:blocked 1&lt;/code&gt;, one key per user blocked status. It works, but most of your stored data in the memory is not your assigned data. There is plenty of overhead in a single key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why all those keys are expensive
&lt;/h2&gt;

&lt;p&gt;When you store your data by running &lt;code&gt;SET user:1203:blocked 1&lt;/code&gt;, it feels like you are storing 1 byte of data. But that's not true. In fact, most of the stored data is not yours, it's from Redis itself.&lt;/p&gt;

&lt;p&gt;Here's what a single key really cost you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redis wrap your value using &lt;a href="https://github.com/redis/redis/blob/7.4/src/server.h#L902" rel="noopener noreferrer"&gt;redisObject&lt;/a&gt;, which is about 16 bytes before your actual data.&lt;/li&gt;
&lt;li&gt;They key itself is &lt;a href="https://github.com/redis/redis/blob/7.4/src/sds.h" rel="noopener noreferrer"&gt;not your typical String&lt;/a&gt;. It has a small header of its own.&lt;/li&gt;
&lt;li&gt;Every key will lives in Redis's main &lt;a href="https://github.com/redis/redis/blob/7.4/src/dict.c#L45" rel="noopener noreferrer"&gt;dict entry&lt;/a&gt;, that mean each key needs to reserve more memory there (roughly 24 bytes).&lt;/li&gt;
&lt;li&gt;All of those above will also get rounds up by the &lt;a href="https://github.com/redis/redis/blob/unstable/src/zmalloc.c" rel="noopener noreferrer"&gt;memory allocator&lt;/a&gt; to a fixed size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One key is harmless, but remember that your keys are expanding as your users grow.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to save the memory
&lt;/h2&gt;

&lt;p&gt;I assume your keys are like these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SET user:10001:blocked 1
SET user:15089:blocked 1
...
SET user:10201:external_id AI27X
SET user:10590:external_id JO91P
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One key per use case per user. With a million of users, that is a million keys that have &lt;strong&gt;per-key overhead&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To reduce the number of keys that live in Redis's main dict entry, we can group them into hashes and bucket them by ID range, so the hash stays small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HSET user:blocked:1 0001 1
HSET user:blocked:1 5089 1
...
HSET user:external_id:1 0201 AI27X
HSET user:external_id:1 0590 JO91P
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bucket is the user ID divided by 1000, so one hash hold thousand users. To read a user you compute the bucket and use &lt;code&gt;HGET&lt;/code&gt; like usual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HGET user:blocked:1 0001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same data, but a million top level keys collapse into thousand.&lt;/p&gt;

&lt;h3&gt;
  
  
  The proof
&lt;/h3&gt;

&lt;p&gt;Don't put your faith on theory alone, let's test it your self.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First run&lt;/strong&gt;, one key per user (baseline):
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-cli FLUSHALL
redis-cli INFO memory | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"used_memory:"&lt;/span&gt;
&lt;span class="nb"&gt;seq &lt;/span&gt;1 1000000 | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{v=($1%7==0); print "SET user:"$1":blocked " v}'&lt;/span&gt; | redis-cli &lt;span class="nt"&gt;--pipe&lt;/span&gt;
redis-cli INFO memory | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"used_memory:"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy9m6avfta9jwmg9iqj1w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy9m6avfta9jwmg9iqj1w.png" alt="First run" width="800" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Second run&lt;/strong&gt;, 1000 users per bucketed hash using hashtable:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-cli FLUSHALL
redis-cli CONFIG SET hash-max-listpack-entries 1
redis-cli INFO memory | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"used_memory:"&lt;/span&gt;
&lt;span class="nb"&gt;seq &lt;/span&gt;1 1000000 | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{v=($1%7==0); b=int(($1-1)/1000); print "HSET user:blocked:"b" "$1" "v}'&lt;/span&gt; | redis-cli &lt;span class="nt"&gt;--pipe&lt;/span&gt;
redis-cli INFO memory | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"used_memory:"&lt;/span&gt;
redis-cli OBJECT ENCODING user:blocked:0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fglffwknz7ytbuu6a4ot1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fglffwknz7ytbuu6a4ot1.png" alt="Second run" width="798" height="203"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Third run&lt;/strong&gt;, 1000 users per bucketed hash using listpack:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-cli FLUSHALL
redis-cli CONFIG SET hash-max-listpack-entries 1024
redis-cli INFO memory | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"used_memory:"&lt;/span&gt;
&lt;span class="nb"&gt;seq &lt;/span&gt;1 1000000 | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{v=($1%7==0); b=int(($1-1)/1000); print "HSET user:blocked:"b" "$1" "v}'&lt;/span&gt; | redis-cli &lt;span class="nt"&gt;--pipe&lt;/span&gt;
redis-cli INFO memory | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"used_memory:"&lt;/span&gt;
redis-cli OBJECT ENCODING user:blocked:0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqthzb40lhl794a8j5ljf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqthzb40lhl794a8j5ljf.png" alt="Third run" width="800" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layout&lt;/th&gt;
&lt;th&gt;Encoding&lt;/th&gt;
&lt;th&gt;Used memory&lt;/th&gt;
&lt;th&gt;Savings vs baseline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1M individual keys&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;53.8 MB&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1M in 1000-field hashes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hashtable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;46.1 MB&lt;/td&gt;
&lt;td&gt;≈ 14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1M in 1000-field hashes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;listpack&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6.9 MB&lt;/td&gt;
&lt;td&gt;≈ 87%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the up to 90% we talked about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes the hash cheap
&lt;/h2&gt;

&lt;p&gt;It's obviously because we eliminate the per value redisObject. We also merge a repeated key. &lt;code&gt;user:10001:blocked&lt;/code&gt; shrinks into hash key &lt;code&gt;0001&lt;/code&gt;, and the &lt;code&gt;user:blocked:1&lt;/code&gt; become bucket key, paid once per thousand.&lt;br&gt;
But that's not mean just because we eliminate small portion of overhead, it suddenly cuts the memory significantly. It not vanish, it relocates. The hash also has its own overhead. Now you need to pay for a &lt;code&gt;dictEntry&lt;/code&gt; on the hash, SDS for the field and the value, and also lose the shared integer trick on the value. Sum it up and it's close to a zero. That is why the hashtable only saved us ≈ 14%.&lt;br&gt;
Then comes the &lt;strong&gt;listpack&lt;/strong&gt; to the spotlight. The per-field &lt;code&gt;dictEntry&lt;/code&gt; cost is gone, the bucket array cost is gone, the SDS cost for every field and every value is replace by a compact one. Now most of the overheads are  gone and you can free most of your memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch
&lt;/h2&gt;

&lt;p&gt;Listpack is an optimization Redis applies on hash when the hash stays small, and it goes away the moment your hash grow past either one of two limits: &lt;code&gt;hash-max-listpack-entries&lt;/code&gt; (&lt;a href="https://github.com/redis/redis/blob/7.4/redis.conf#L1937" rel="noopener noreferrer"&gt;default 512&lt;/a&gt;) and &lt;code&gt;hash-max-listpack-value&lt;/code&gt; (&lt;a href="https://github.com/redis/redis/blob/7.4/redis.conf#L1938" rel="noopener noreferrer"&gt;default 64&lt;/a&gt;). The first limits how many fields a hash can hold, and the second limit the size of the value or the field (it has separate limit for both not sum).&lt;br&gt;
Cross either one and Redis will turn your listpack into hashtable without warning. Even from a single value that is one byte too long, your hash will flip. The change is also one way, meaning that if you manage to update your value to under the limit, it will not go back to listpack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoffs
&lt;/h2&gt;

&lt;p&gt;There is no freebie, only tradeoffs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read and write to a listpack is linier O(n), that's why we need to keep it small.&lt;/li&gt;
&lt;li&gt;Separate key can have separate TTL, but when you merge them into hash, there is no per field TTL. Redis 7.4 added per field TTL (using HEXPIRE), but they carry their own memory cost and turn your listpack into &lt;a href="https://github.com/redis/redis/blob/7.4/src/t_hash.c#L300" rel="noopener noreferrer"&gt;listpackEx&lt;/a&gt; (at least not hashtable).&lt;/li&gt;
&lt;li&gt;More complex code to manage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Listpack is not a new tool you need to install, not a dependency to add, not even a switch to flip. It is already there, built in your Redis. You just need to tune your code to utilize it. Find the cases that fit and your memory usage drops several fold. Lower memory means lower bills. &lt;/p&gt;

</description>
      <category>redis</category>
      <category>backend</category>
      <category>database</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
