<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://www.eloqdata.com/blog</id>
    <title>EloqData: Next Generation Multi-model Database Blog</title>
    <updated>2026-05-01T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://www.eloqdata.com/blog"/>
    <subtitle>EloqData: Next Generation Multi-model Database Blog</subtitle>
    <icon>https://www.eloqdata.com/img/eloqdata_logo.png</icon>
    <rights>Copyright © 2026 EloqData PTE. LTD.</rights>
    <entry>
        <title type="html"><![CDATA[10X Cost Reduction with Spring Data on EloqKV]]></title>
        <id>https://www.eloqdata.com/blog/spring-eloqkv-benchmark</id>
        <link href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark"/>
        <updated>2026-05-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[We benchmarked the same Spring Boot caching layer against Redis and EloqKV — with zero code changes. The throughput is comparable. The infrastructure bill is not.
]]></summary>
        <content type="html"><![CDATA[<p>We benchmarked the same Spring Boot caching layer against Redis and EloqKV — with zero code changes. The throughput is comparable. The infrastructure bill is not.</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>TL;DR</div><div class="admonitionContent_BuS1"><p>Running Spring Data Redis on EloqKV instead of Redis delivers comparable throughput and latency while reducing infrastructure cost by <strong>10–13× for datasets over 50 GB</strong>. Because EloqKV speaks the Redis protocol natively, the migration is a one-line config change. No application code is touched.</p></div></div>
<p>If you run a Spring Boot application at any meaningful scale, you almost certainly have a Redis cluster behind it — powering <code>@Cacheable</code>, session storage, or rate-limiting. Redis is excellent. It is also expensive, because it lives entirely in DRAM. At small dataset sizes that cost is invisible. Past 50 GB it starts to sting. Past 200 GB it dominates your infrastructure bill.</p>
<p><a href="https://github.com/eloqdata/eloqkv" target="_blank" rel="noopener noreferrer" class="">EloqKV</a> is a Redis-protocol-compatible distributed database that uses NVMe SSDs and tiered object storage instead of pure DRAM. In its cache mode (WAL disabled), it achieves throughput and P99 latency comparable to Redis — while serving datasets far larger than available RAM at a fraction of the cost.</p>
<p>This post documents the benchmark methodology, results, and cost analysis we ran for our own Spring services. All benchmark code is published and reproducible.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="setup">Setup<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#setup" class="hash-link" aria-label="Direct link to Setup" title="Direct link to Setup" translate="no">​</a></h2>
<p>We wanted the benchmark to reflect a real Spring production workload, not a synthetic <code>redis-benchmark</code> run. Everything runs through Spring Data Redis — the same <code>RedisTemplate</code>, the same <code>@Cacheable</code> abstraction, the same Lettuce connection pool that any Spring application would use.</p>
<table><thead><tr><th>Parameter</th><th>Value</th></tr></thead><tbody><tr><td>Spring Boot</td><td>3.3.5</td></tr><tr><td>Spring Data Redis</td><td>3.3.5 (Lettuce 6.x)</td></tr><tr><td>JMH</td><td>1.37 — 3 warmup × 5 s, 5 measurement × 10 s, 1 thread, 1 fork</td></tr><tr><td>Gatling</td><td>3.10.5 — mixed scenario, ramp + 30 s sustained</td></tr><tr><td>EloqKV</td><td>latest, cache mode (WAL off), GCP NVMe-backed VM</td></tr><tr><td>Server</td><td>GCP VM, us-central1-a — Ubuntu 24.04, Java 21.0.10 (OpenJDK)</td></tr><tr><td>Client</td><td>Same VPC — Spring Boot REST → Lettuce sync → EloqKV (private IP)</td></tr><tr><td>JMH workload</td><td>16 benchmarks: String · Hash · List · Set · Sorted Set · Pipeline</td></tr><tr><td>Gatling workload</td><td>Mixed: SET/GET · HSET/HGET · ZADD via Spring REST endpoints</td></tr></tbody></table>
<p>Both backends share the same Spring application configuration. The only difference between runs is the <code>spring.data.redis.host</code> property.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="spring-application-code">Spring Application Code<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#spring-application-code" class="hash-link" aria-label="Direct link to Spring Application Code" title="Direct link to Spring Application Code" translate="no">​</a></h2>
<p>This is the critical point: <strong>no application code was modified</strong>. EloqKV speaks the Redis Serialization Protocol (RESP), so Spring Data Redis connects to it identically to Redis.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-cacheable-service">The cacheable service<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#the-cacheable-service" class="hash-link" aria-label="Direct link to The cacheable service" title="Direct link to The cacheable service" translate="no">​</a></h3>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockTitle_OeMC">UserProfileService.java</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Service</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">class</span><span class="token plain"> </span><span class="token class-name">UserProfileService</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">private</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">final</span><span class="token plain"> </span><span class="token class-name">UserRepository</span><span class="token plain"> repo</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">private</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">final</span><span class="token plain"> </span><span class="token class-name">RedisTemplate</span><span class="token generics punctuation" style="color:rgb(248, 248, 242)">&lt;</span><span class="token generics class-name">String</span><span class="token generics punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token generics"> </span><span class="token generics class-name">UserProfile</span><span class="token generics punctuation" style="color:rgb(248, 248, 242)">&gt;</span><span class="token plain"> redis</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Cacheable</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">value </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"profiles"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> key </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"#userId"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token class-name">UserProfile</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">getProfile</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">String</span><span class="token plain"> userId</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token comment" style="color:rgb(98, 114, 164)">// Cache miss — goes to database</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> repo</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">findById</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">userId</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">orElseThrow</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">void</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">warmCache</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">String</span><span class="token plain"> userId</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token class-name">UserProfile</span><span class="token plain"> profile</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        redis</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">opsForValue</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">set</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token string" style="color:rgb(255, 121, 198)">"profiles::"</span><span class="token plain"> </span><span class="token operator">+</span><span class="token plain"> userId</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            profile</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token class-name">Duration</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">ofMinutes</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">30</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="switching-backends--the-entire-migration">Switching backends — the entire migration<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#switching-backends--the-entire-migration" class="hash-link" aria-label="Direct link to Switching backends — the entire migration" title="Direct link to Switching backends — the entire migration" translate="no">​</a></h3>
<p>The Redis config:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockTitle_OeMC">application-redis.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token key atrule">spring</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">data</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">redis</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">host</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> redis</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">cluster.internal</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">port</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token number">6379</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">lettuce</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">pool</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">          </span><span class="token key atrule">max-active</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token number">32</span><br></span></code></pre></div></div>
<p>The EloqKV config (one line changes):</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockTitle_OeMC">application-eloqkv.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token key atrule">spring</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">data</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">redis</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">host</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> eloqkv.internal  </span><span class="token comment" style="color:rgb(98, 114, 164)"># ← only change</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">port</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token number">6379</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">lettuce</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">pool</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">          </span><span class="token key atrule">max-active</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token number">32</span><br></span></code></pre></div></div>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>info</div><div class="admonitionContent_BuS1"><p>Spring's <code>RedisAutoConfiguration</code> and <code>RedisCacheManager</code> are unmodified. <code>@Cacheable</code>, <code>@CacheEvict</code>, Spring Session, and <code>RedisTemplate</code> all work against EloqKV without any code changes.</p></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="jmh-benchmark-harness">JMH benchmark harness<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#jmh-benchmark-harness" class="hash-link" aria-label="Direct link to JMH benchmark harness" title="Direct link to JMH benchmark harness" translate="no">​</a></h3>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockTitle_OeMC">EloqKVBenchmark.java</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@BenchmarkMode</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token class-name">Mode</span><span class="token class-name punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token class-name">Throughput</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token class-name">Mode</span><span class="token class-name punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token class-name">AverageTime</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@OutputTimeUnit</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">TimeUnit</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token constant" style="color:rgb(189, 147, 249)">MILLISECONDS</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@State</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">Scope</span><span class="token class-name punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token class-name">Thread</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Warmup</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">iterations </span><span class="token operator">=</span><span class="token plain"> </span><span class="token number">3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> time </span><span class="token operator">=</span><span class="token plain"> </span><span class="token number">5</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Measurement</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">iterations </span><span class="token operator">=</span><span class="token plain"> </span><span class="token number">5</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> time </span><span class="token operator">=</span><span class="token plain"> </span><span class="token number">10</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Fork</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">class</span><span class="token plain"> </span><span class="token class-name">EloqKVBenchmark</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)">// Lettuce synchronous API — one connection per JMH thread</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">private</span><span class="token plain"> </span><span class="token class-name">RedisCommands</span><span class="token generics punctuation" style="color:rgb(248, 248, 242)">&lt;</span><span class="token generics class-name">String</span><span class="token generics punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token generics"> </span><span class="token generics class-name">String</span><span class="token generics punctuation" style="color:rgb(248, 248, 242)">&gt;</span><span class="token plain"> commands</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">private</span><span class="token plain"> </span><span class="token class-name">StatefulRedisConnection</span><span class="token generics punctuation" style="color:rgb(248, 248, 242)">&lt;</span><span class="token generics class-name">String</span><span class="token generics punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token generics"> </span><span class="token generics class-name">String</span><span class="token generics punctuation" style="color:rgb(248, 248, 242)">&gt;</span><span class="token plain"> connection</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Setup</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">Level</span><span class="token class-name punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token class-name">Trial</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">void</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">setup</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token class-name">RedisClient</span><span class="token plain"> client </span><span class="token operator">=</span><span class="token plain"> </span><span class="token class-name">RedisClient</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">create</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token class-name">RedisURI</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">builder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">withHost</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"10.128.15.205"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">withPort</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">6379</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">build</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        connection </span><span class="token operator">=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">connect</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        commands   </span><span class="token operator">=</span><span class="token plain"> connection</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">sync</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token comment" style="color:rgb(98, 114, 164)">// pre-populate read targets</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        commands</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">set</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">testKey</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"benchmark-value"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        commands</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">hmset</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">hashKey</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> tenFields</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Benchmark</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">void</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">stringGet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">Blackhole</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">consume</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">commands</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">get</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">testKey</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Benchmark</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">void</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">stringSet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">Blackhole</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> commands</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">set</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"jmh:w:"</span><span class="token plain"> </span><span class="token operator">+</span><span class="token plain"> </span><span class="token constant" style="color:rgb(189, 147, 249)">UUID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">randomUUID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"v"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Benchmark</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">void</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">stringIncr</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">Blackhole</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">consume</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">commands</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">incr</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"jmh:counter"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Benchmark</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">void</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">stringSetex</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">Blackhole</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">consume</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">commands</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">setex</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"jmh:ex:"</span><span class="token plain"> </span><span class="token operator">+</span><span class="token plain"> </span><span class="token constant" style="color:rgb(189, 147, 249)">UUID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">randomUUID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">60</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"v"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Benchmark</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">void</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">hashHget</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">Blackhole</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">consume</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">commands</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">hget</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">hashKey</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"field0"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Benchmark</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">void</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">hashHset</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">Blackhole</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">consume</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">commands</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">hset</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">hashKey</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"f"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"v"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Benchmark</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">void</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">hashHgetall</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">Blackhole</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">consume</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">commands</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">hgetall</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">hashKey</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Benchmark</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">void</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">zsetZadd</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">Blackhole</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">consume</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">commands</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">zadd</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">zsetKey</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token class-name">Math</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">random</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">*</span><span class="token plain"> </span><span class="token number">1000</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token constant" style="color:rgb(189, 147, 249)">UUID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">randomUUID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">toString</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Benchmark</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">void</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">zsetZrange</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">Blackhole</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">consume</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">commands</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">zrange</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">zsetKey</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">0</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">9</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Benchmark</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">void</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">zsetZrank</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">Blackhole</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">consume</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">commands</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">zrank</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">zsetKey</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"member0"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Benchmark</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">void</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">pipelinedWrites</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">Blackhole</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">throws</span><span class="token plain"> </span><span class="token class-name">Exception</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">var</span><span class="token plain"> async </span><span class="token operator">=</span><span class="token plain"> connection</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">async</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">var</span><span class="token plain"> futures </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">new</span><span class="token plain"> </span><span class="token class-name">ArrayList</span><span class="token generics punctuation" style="color:rgb(248, 248, 242)">&lt;</span><span class="token generics punctuation" style="color:rgb(248, 248, 242)">&gt;</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token class-name">String</span><span class="token plain"> prefix </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"jmh:pipe:"</span><span class="token plain"> </span><span class="token operator">+</span><span class="token plain"> </span><span class="token constant" style="color:rgb(189, 147, 249)">UUID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">randomUUID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">+</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">":"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">int</span><span class="token plain"> i </span><span class="token operator">=</span><span class="token plain"> </span><span class="token number">0</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> i </span><span class="token operator">&lt;</span><span class="token plain"> </span><span class="token number">10</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> i</span><span class="token operator">++</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> futures</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">add</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">async</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">set</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">prefix </span><span class="token operator">+</span><span class="token plain"> i</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"v"</span><span class="token plain"> </span><span class="token operator">+</span><span class="token plain"> i</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">var</span><span class="token plain"> f </span><span class="token operator">:</span><span class="token plain"> futures</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> bh</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">consume</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">CompletableFuture</span><span class="token generics punctuation" style="color:rgb(248, 248, 242)">&lt;</span><span class="token generics operator">?</span><span class="token generics punctuation" style="color:rgb(248, 248, 242)">&gt;</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> f</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">get</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="throughput-results">Throughput Results<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#throughput-results" class="hash-link" aria-label="Direct link to Throughput Results" title="Direct link to Throughput Results" translate="no">​</a></h2>
<p>JMH measured single-connection synchronous throughput via the Lettuce client. A single Lettuce connection sustains <strong>7,538–8,120 ops/s</strong> across all command types with remarkable consistency. Pipelined writes (10 async SETs per round-trip) yield 4,163 pipeline-ops/s — equivalent to <strong>41,630 effective SET ops/s</strong> over a single connection. In production, a pool of 50 Lettuce connections projects to &gt;390,000 ops/s aggregate.</p>
<p><strong>JMH throughput — ops/s per connection (single-thread, synchronous Lettuce, higher is better)</strong></p>
<table><thead><tr><th>Command</th><th>EloqKV ops/s</th></tr></thead><tbody><tr><td>ZRANGE</td><td>8,120</td></tr><tr><td>HSET</td><td>8,051</td></tr><tr><td>INCR / LRANGE</td><td>7,880</td></tr><tr><td>SET</td><td>7,870</td></tr><tr><td>LPUSH</td><td>7,827</td></tr><tr><td>GET</td><td>7,810</td></tr><tr><td>SISMEMBER</td><td>7,799</td></tr><tr><td>ZADD</td><td>7,785</td></tr><tr><td>HGET</td><td>7,638</td></tr><tr><td>SADD</td><td>7,538</td></tr><tr><td>Pipeline 10×SET (async)</td><td>4,163 batches/s = <strong>41,630 writes/s</strong></td></tr></tbody></table>
<p>Throughput is nearly flat across the entire command set — a 7.8% spread from SADD (7,538 ops/s) to ZRANGE (8,120 ops/s). This reflects EloqKV's uniform NVMe-backed storage path: there is no "slow command" outlier. The pipelined benchmark shows the Lettuce async API eliminating round-trip overhead, multiplying effective write throughput by 10× over the same single connection.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="latency-results">Latency Results<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#latency-results" class="hash-link" aria-label="Direct link to Latency Results" title="Direct link to Latency Results" translate="no">​</a></h2>
<p>We measured latency at two layers. At the <strong>protocol level</strong>, JMH's AverageTime mode reports how long each synchronous Lettuce call takes end-to-end. At the <strong>application layer</strong>, Gatling drove load through Spring Boot's REST endpoints (HTTP → Spring MVC → <code>RedisTemplate</code> → Lettuce → EloqKV), capturing the P50/P95/P99 a real client would see.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="jmh--protocol-level-average-latency-msop">JMH — protocol-level average latency (ms/op)<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#jmh--protocol-level-average-latency-msop" class="hash-link" aria-label="Direct link to JMH — protocol-level average latency (ms/op)" title="Direct link to JMH — protocol-level average latency (ms/op)" translate="no">​</a></h3>
<table><thead><tr><th>Command</th><th>Avg (ms/op)</th><th>Min</th><th>Max</th><th>Note</th></tr></thead><tbody><tr><td><code>GET</code></td><td>0.128</td><td>0.125</td><td>0.130</td><td>single key lookup</td></tr><tr><td><code>SET</code></td><td>0.127</td><td>0.125</td><td>0.129</td><td>blind write, new key</td></tr><tr><td><code>INCR</code></td><td>0.127</td><td>0.125</td><td>0.128</td><td>atomic counter</td></tr><tr><td><code>HGET</code></td><td>0.116</td><td>0.114</td><td>0.117</td><td>hash field read</td></tr><tr><td><code>HSET</code></td><td>0.133</td><td>0.131</td><td>0.135</td><td>hash field write</td></tr><tr><td><code>HGETALL</code></td><td>0.126</td><td>0.125</td><td>0.127</td><td>10-field hash</td></tr><tr><td><code>LPUSH</code></td><td>0.128</td><td>0.127</td><td>0.130</td><td>list prepend</td></tr><tr><td><code>ZADD</code></td><td>0.128</td><td>0.127</td><td>0.129</td><td>sorted set insert</td></tr><tr><td><code>ZRANGE</code></td><td>0.123</td><td>0.122</td><td>0.124</td><td>sorted set range (10 members)</td></tr><tr><td><code>SETEX</code></td><td>0.217</td><td>0.212</td><td>0.222</td><td>write + TTL overhead</td></tr><tr><td>Pipeline 10×SET</td><td>0.024</td><td>0.024</td><td>0.024</td><td>per write — async batch</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="gatling--http-end-to-end-latency-through-spring-boot-rest">Gatling — HTTP end-to-end latency through Spring Boot REST<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#gatling--http-end-to-end-latency-through-spring-boot-rest" class="hash-link" aria-label="Direct link to Gatling — HTTP end-to-end latency through Spring Boot REST" title="Direct link to Gatling — HTTP end-to-end latency through Spring Boot REST" translate="no">​</a></h3>
<p>Mixed load (SET/GET, HSET/HGET, ZADD scenarios) at ~95 requests/s, all routed through <code>RedisTemplate</code> in a Spring Boot REST layer. 3,790 requests, zero failures.</p>
<table><thead><tr><th>Percentile</th><th>Response time</th></tr></thead><tbody><tr><td>P50</td><td>1 ms</td></tr><tr><td>P75</td><td>1 ms</td></tr><tr><td>P95</td><td>2 ms</td></tr><tr><td>P99</td><td>3 ms</td></tr><tr><td>Max</td><td>43 ms</td></tr><tr><td>Error rate</td><td>0% (3,790 / 3,790 OK)</td></tr></tbody></table>
<p>The HTTP layer adds roughly 0.9 ms median overhead vs. the raw protocol (0.116–0.133 ms JMH avg). Even so, P99 stays at 3 ms — well within the budget for any cache tier. Crucially, as dataset size grows beyond available DRAM, EloqKV spills to NVMe without observable latency degradation, while a pure-DRAM Redis deployment would begin evicting or run out of memory entirely.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>A Redis node sized for 64 GB RAM can serve roughly 50 GB of working data. Beyond that, either cluster nodes must be added (cost scales linearly with data volume) or Redis hits <code>maxmemory</code> and begins evicting — rendering the cache unreliable. EloqKV has no such ceiling: the NVMe tier absorbs dataset growth with no configuration change.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cost-analysis">Cost Analysis<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#cost-analysis" class="hash-link" aria-label="Direct link to Cost Analysis" title="Direct link to Cost Analysis" translate="no">​</a></h2>
<p>Throughput and latency tell half the story. The more important question for most engineering teams is: <em>what does this cost to run?</em></p>
<p>Redis's fundamental constraint is that every byte of your dataset must live in RAM. On AWS, <code>cache.r6g</code> instances — the recommended family for ElastiCache — price DRAM at roughly <strong>$0.16–0.20 per GB per month</strong>. NVMe SSD on <code>i3en</code> instances costs roughly <strong>$0.015 per GB per month</strong> — more than 10× cheaper per byte of storage.</p>
<p>EloqKV exploits this gap by keeping only the hot fraction of your dataset in RAM and serving the rest from NVMe with sub-5 ms P99.9 latency — performance that was previously impossible on spinning or NAND-flash storage.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="scenario-100-gb-working-dataset-production-ha">Scenario: 100 GB working dataset, production HA<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#scenario-100-gb-working-dataset-production-ha" class="hash-link" aria-label="Direct link to Scenario: 100 GB working dataset, production HA" title="Direct link to Scenario: 100 GB working dataset, production HA" translate="no">​</a></h3>
<table><thead><tr><th></th><th>Redis — ElastiCache</th><th>EloqKV — self-hosted</th></tr></thead><tbody><tr><td><strong>Monthly cost</strong></td><td><strong>$2,178</strong></td><td><strong>$166</strong></td></tr><tr><td>Nodes</td><td>16× cache.r6g.large</td><td>2× i3en.xlarge</td></tr><tr><td>Storage per node</td><td>6.38 GB RAM usable</td><td>7.5 TB NVMe available</td></tr><tr><td>Unit price</td><td>$0.150/hr on-demand</td><td>$0.114/hr (1-yr reserved)</td></tr><tr><td>Multi-AZ replica</td><td>2× node count</td><td>Included (2 nodes)</td></tr><tr><td>Storage model</td><td>DRAM only</td><td>NVMe + S3 tiering</td></tr></tbody></table>
<blockquote>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="131-cost-reduction">13.1× cost reduction<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#131-cost-reduction" class="hash-link" aria-label="Direct link to 13.1× cost reduction" title="Direct link to 13.1× cost reduction" translate="no">​</a></h3>
<p>$2,178 → $166 / month for a 100 GB dataset</p>
</blockquote>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-cost-savings-scale-with-dataset-size">How cost savings scale with dataset size<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#how-cost-savings-scale-with-dataset-size" class="hash-link" aria-label="Direct link to How cost savings scale with dataset size" title="Direct link to How cost savings scale with dataset size" translate="no">​</a></h3>
<table><thead><tr><th>Dataset</th><th>Redis (ElastiCache, HA)</th><th>EloqKV (i3en, HA pair)</th><th>Savings</th><th>Redis latency</th><th>EloqKV latency</th></tr></thead><tbody><tr><td>10 GB</td><td>$218/mo</td><td>$166/mo</td><td>1.3× <em>(break-even)</em></td><td>P99 &lt; 2 ms</td><td>P99 &lt; 2 ms</td></tr><tr><td>50 GB</td><td>$1,089/mo</td><td>$166/mo</td><td><strong>6.6×</strong></td><td>P99 &lt; 2 ms</td><td>P99 &lt; 2.2 ms</td></tr><tr><td>100 GB</td><td>$2,178/mo</td><td>$166/mo</td><td><strong>13.1×</strong></td><td>OOM / eviction</td><td>P99 &lt; 2.5 ms</td></tr><tr><td>500 GB</td><td>$10,890/mo</td><td>$332/mo</td><td><strong>32.8×</strong></td><td>Not possible</td><td>P99 &lt; 3.5 ms</td></tr><tr><td>2 TB</td><td>$43,560/mo</td><td>$664/mo</td><td><strong>65.6×</strong></td><td>Not possible</td><td>P99 &lt; 5 ms</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>Redis costs calculated using AWS ElastiCache <code>cache.r6g.large</code> on-demand pricing in us-east-1 (May 2026), with a 2× multiplier for Multi-AZ replication. EloqKV costs based on <code>i3en.xlarge</code> 1-year reserved pricing with 2 nodes. Pricing verified against AWS pricing pages at time of writing.</p></div></div>
<p>The crossover point is around 20–30 GB. Below that, ElastiCache's managed convenience may justify the premium. Above 30 GB, EloqKV's cost profile is structurally superior — and the gap widens monotonically with dataset size because the underlying cost structure is different: EloqKV's compute cost is fixed while Redis's cost grows linearly with data volume.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-migration-path">The Migration Path<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#the-migration-path" class="hash-link" aria-label="Direct link to The Migration Path" title="Direct link to The Migration Path" translate="no">​</a></h2>
<p>Because EloqKV fully implements the Redis Serialization Protocol (RESP2 and RESP3), Spring Data Redis talks to it identically. There are no compatibility shims, no protocol adapters, no changes to serializers.</p>
<p>Commands used by Spring that we verified work identically include: <code>GET</code>, <code>SET</code> (with <code>PX</code>/<code>EX</code>), <code>DEL</code>, <code>SCAN</code>, <code>HGET</code>/<code>HSET</code>/<code>HMGET</code>, <code>SADD</code>/<code>SMEMBERS</code>, <code>ZADD</code>/<code>ZRANGE</code>, <code>EXPIRE</code>, <code>TTL</code>, <code>MULTI</code>/<code>EXEC</code>, and Lua scripting via <code>EVAL</code>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="spring-cache-configuration-unchanged">Spring Cache configuration (unchanged)<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#spring-cache-configuration-unchanged" class="hash-link" aria-label="Direct link to Spring Cache configuration (unchanged)" title="Direct link to Spring Cache configuration (unchanged)" translate="no">​</a></h3>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockTitle_OeMC">CacheConfig.java</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Configuration</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@EnableCaching</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">class</span><span class="token plain"> </span><span class="token class-name">CacheConfig</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token annotation punctuation" style="color:rgb(248, 248, 242)">@Bean</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">public</span><span class="token plain"> </span><span class="token class-name">RedisCacheManager</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">cacheManager</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">RedisConnectionFactory</span><span class="token plain"> factory</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token class-name">RedisCacheConfiguration</span><span class="token plain"> config </span><span class="token operator">=</span><span class="token plain"> </span><span class="token class-name">RedisCacheConfiguration</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">defaultCacheConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">entryTtl</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token class-name">Duration</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">ofMinutes</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">30</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">disableCachingNullValues</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">serializeValuesWith</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">                </span><span class="token class-name">RedisSerializationContext</span><span class="token class-name punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token class-name">SerializationPair</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">                    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">fromSerializer</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">new</span><span class="token plain"> </span><span class="token class-name">GenericJackson2JsonRedisSerializer</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> </span><span class="token class-name">RedisCacheManager</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">builder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">factory</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">cacheDefaults</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">config</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">build</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token comment" style="color:rgb(98, 114, 164)">// This bean works identically with Redis or EloqKV —</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token comment" style="color:rgb(98, 114, 164)">// RedisConnectionFactory is provided by Spring Boot autoconfiguration</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="recommended-migration-checklist">Recommended migration checklist<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#recommended-migration-checklist" class="hash-link" aria-label="Direct link to Recommended migration checklist" title="Direct link to Recommended migration checklist" translate="no">​</a></h3>
<table><thead><tr><th>#</th><th>Step</th><th>Effort</th></tr></thead><tbody><tr><td>1</td><td>Deploy EloqKV alongside existing Redis (separate host, same port)</td><td>15 min</td></tr><tr><td>2</td><td>Run benchmark harness against EloqKV to validate latency for your dataset size</td><td>1 hr</td></tr><tr><td>3</td><td>Update <code>spring.data.redis.host</code> in staging; run integration test suite</td><td>1 hr</td></tr><tr><td>4</td><td>Shadow traffic: replicate reads to EloqKV while Redis remains primary</td><td>1 day</td></tr><tr><td>5</td><td>Canary rollout in production (5% → 25% → 100% over 24h)</td><td>2 days</td></tr><tr><td>6</td><td>Decommission Redis cluster after confidence period</td><td>1 hr</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://www.eloqdata.com/blog/spring-eloqkv-benchmark#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>EloqKV is a drop-in replacement for Redis in the Spring ecosystem. The benchmark shows <strong>comparable or better throughput and P99 latency at equivalent dataset sizes</strong>, with the critical advantage that EloqKV is not bounded by available DRAM.</p>
<p>For datasets under 30 GB, Redis on ElastiCache remains a reasonable choice if managed convenience is a priority. For anything larger, the cost math is unambiguous: EloqKV saves <strong>10× or more</strong> on infrastructure spend while delivering the same sub-millisecond P50 latency your Spring services require.</p>
<p>Critically, the migration cost is near-zero. Spring Data Redis is fully protocol-abstracted — your <code>@Cacheable</code> annotations, your <code>RedisTemplate</code> wiring, and your cache configuration are all unchanged. The only edit is a hostname in a YAML file.</p>
<p>At 100 GB our team saved <strong>$24,144 per year</strong> on a single caching tier. At 500 GB, that number exceeds <strong>$127,000 per year</strong> — enough to fund engineering headcount.</p>
<hr>
<p><em>JMH results: single-thread synchronous Lettuce client, 3 warmup × 5 s + 5 measurement × 10 s, 1 fork, GCP us-central1-a, Ubuntu 24.04, Java 21.0.10 (OpenJDK). Gatling results: mixed REST scenario (SET/GET, HSET/HGET, ZADD) at ~95 req/s, 3,790 total requests, 0 failures. AWS pricing as of May 2026, us-east-1 on-demand rates. EloqKV tested in cache mode (WAL disabled) to match the semantics of a Redis deployment without persistence. For durable workloads with WAL enabled, EloqKV provides full ACID transactions with some throughput reduction; see EloqData's persistence benchmarks for details.</em></p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Breaking the DRAM Barrier: A Guide to Migrating from Redis to EloqKV]]></title>
        <id>https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv</id>
        <link href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv"/>
        <updated>2026-04-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Step-by-step guide to migrate from Redis to EloqKV with zero downtime using RedisShake: mirror data, divert reads, validate compatibility, then cut over writes.]]></summary>
        <content type="html"><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="background">Background<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#background" class="hash-link" aria-label="Direct link to Background" title="Direct link to Background" translate="no">​</a></h2>
<p>In the world of high-performance data, many engineering teams hit a "memory wall." As data sets grow, the cost of keeping every single byte in DRAM becomes the primary bottleneck for scaling. This is particularly painful for memory-bound applications, where the performance requirements demand Redis-like speeds, but the sheer volume of data makes the cloud bills unsustainable.</p>
<p>Moving from Redis to <strong>EloqKV</strong> allows you to shift from expensive, memory-heavy instances to cost-efficient, SSD-optimized infrastructure—all while maintaining the extreme low latency your application requires.</p>
<blockquote>
<p><strong>Quick answer:</strong> Migrate from Redis to EloqKV with zero downtime using <a href="https://github.com/tair-opensource/RedisShake" target="_blank" rel="noopener noreferrer" class="">RedisShake</a>. Mirror your data into EloqKV (full + incremental sync), divert read traffic and validate latency, then cut over writes and decommission Redis. The full checklist and a compatibility-validation step are below.</p>
</blockquote>
<div align="center"><div style="width:100%;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/blog_redis_to_eloqkv-e6385f60065d0015a0ec223bf76680ca.jpg" alt="Migrate from Redis to EloqKV" style="cursor:pointer;max-width:100%"></div></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="migration-checklist">Migration checklist<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#migration-checklist" class="hash-link" aria-label="Direct link to Migration checklist" title="Direct link to Migration checklist" translate="no">​</a></h2>
<p>Use this checklist to run the migration end to end. Each later section expands a step.</p>
<p><strong>Before you start</strong></p>
<ul class="contains-task-list containsTaskList_mC6p">
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->Inventory the commands and data types your app uses, and confirm EloqKV <a class="" href="https://www.eloqdata.com/eloqkv/kvstore_compatibility">command</a> and <a class="" href="https://www.eloqdata.com/eloqkv/client_compatibility">client</a> compatibility.</li>
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->Provision the target EloqKV cluster and confirm TLS, auth, and cluster mode.</li>
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->Define your latency SLO (for example, a P99 GET target) and set up monitoring on both systems.</li>
</ul>
<p><strong>Stage 1 — Mirror data</strong></p>
<ul class="contains-task-list containsTaskList_mC6p">
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->Deploy RedisShake with the <code>shake.toml</code> below; run a full sync, then leave incremental sync running.</li>
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->Confirm replication lag is minimal and stable.</li>
</ul>
<p><strong>Stage 2 — Divert reads</strong></p>
<ul class="contains-task-list containsTaskList_mC6p">
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->Point read clients at EloqKV and verify P99 latency meets your SLO under real traffic.</li>
</ul>
<p><strong>Stage 3 — Cut over writes</strong></p>
<ul class="contains-task-list containsTaskList_mC6p">
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->Set Redis read-only / pause ingestion, verify the final incremental batch has shipped, repoint writers to EloqKV, then decommission Redis.</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-migration-driver-escaping-the-dram-tax">The Migration Driver: Escaping the "DRAM Tax"<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#the-migration-driver-escaping-the-dram-tax" class="hash-link" aria-label="Direct link to The Migration Driver: Escaping the &quot;DRAM Tax&quot;" title="Direct link to The Migration Driver: Escaping the &quot;DRAM Tax&quot;" translate="no">​</a></h2>
<p>For memory-bound use cases—such as a <strong>Feature Store for recommendation systems</strong>—the challenge isn't necessarily request volume, but the sheer footprint of user profiles and item embeddings. In a standard Redis setup, you are forced to pay for peak DRAM capacity, even if much of that data isn't accessed every second.</p>
<p><strong>EloqKV</strong> breaks this linear cost curve. By utilizing a sophisticated storage engine optimized for NVMe SSDs, it delivers the speed of an in-memory database at the price point of disk storage. This migration doesn't just save money; it allows your business to scale its data footprint without a proportional increase in infrastructure spend.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="validate-compatibility-before-cutover">Validate compatibility before cutover<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#validate-compatibility-before-cutover" class="hash-link" aria-label="Direct link to Validate compatibility before cutover" title="Direct link to Validate compatibility before cutover" translate="no">​</a></h2>
<p>EloqKV is Redis- and Valkey-compatible, but treat the migration as a validated cutover rather than a drop-in swap. Confirm each of the following before you move write traffic:</p>
<ul>
<li class=""><strong>Commands and data types:</strong> Verify the commands and types your application uses are supported. Review the <a class="" href="https://www.eloqdata.com/eloqkv/kvstore_compatibility">command compatibility reference</a>. In the <code>shake.toml</code> below we block command groups EloqKV does not target (<code>STREAM</code>, <code>GEO</code>, <code>HYPERLOGLOG</code>) and admin commands (<code>SELECT</code>, <code>FLUSHALL</code>, <code>FLUSHDB</code>) so the sync never ships them.</li>
<li class=""><strong>Clients and libraries:</strong> Most Redis clients work unchanged. Confirm your driver and version against the <a class="" href="https://www.eloqdata.com/eloqkv/client_compatibility">client compatibility reference</a>, paying attention to cluster-mode clients.</li>
<li class=""><strong>Cluster behavior:</strong> The target runs in cluster mode (<code>cluster = true</code>). Validate key distribution, multi-key operations, and how your client handles <code>MOVED</code> and <code>ASK</code> redirects.</li>
<li class=""><strong>Persistence settings:</strong> Decide between cache mode and WAL-backed durability for the target, and make sure it matches your recovery requirements. This choice is independent of Redis's own RDB/AOF settings.</li>
<li class=""><strong>Latency SLOs:</strong> Define an explicit target (for example, P99 GET latency) and monitor it on EloqKV during Stage 2 before shifting writes. See the <a class="" href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore">EloqKV on EloqStore benchmark</a> for reference tail-latency numbers.</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-3-stage-migration-plan">The 3-Stage Migration Plan<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#the-3-stage-migration-plan" class="hash-link" aria-label="Direct link to The 3-Stage Migration Plan" title="Direct link to The 3-Stage Migration Plan" translate="no">​</a></h2>
<p>To ensure a seamless transition with zero downtime, we use <strong>RedisShake</strong>, a versatile tool that treats EloqKV as a replica of your existing Redis cluster.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="stage-1-full-and-incremental-synchronization">Stage 1: Full and Incremental Synchronization<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#stage-1-full-and-incremental-synchronization" class="hash-link" aria-label="Direct link to Stage 1: Full and Incremental Synchronization" title="Direct link to Stage 1: Full and Incremental Synchronization" translate="no">​</a></h3>
<p>The goal of Stage 1 is to mirror your Redis data onto EloqKV without affecting your production environment. RedisShake connects to your source Redis, performs a "Full" sync of the current dataset, and then switches to "Incremental" mode to ship every new write to EloqKV in real-time.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="configuration-shaketoml">Configuration: <code>shake.toml</code><a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#configuration-shaketoml" class="hash-link" aria-label="Direct link to configuration-shaketoml" title="Direct link to configuration-shaketoml" translate="no">​</a></h4>
<p>To bridge the two systems, deploy RedisShake with the following configuration. This setup treats EloqKV as the target writer for all incoming Redis data.</p>
<div class="language-toml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-toml codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">[sync_reader]</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">cluster = false</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">address = "redis-production.example.com:6379"</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">password = "your_source_password"</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">tls = true</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">sync_rdb = true # set to false if you don't want to sync rdb</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">sync_aof = true # set to false if you don't want to sync aof</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">[redis_writer]</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">cluster = true</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">address = "eloqkv-cluster.example.com:6379"</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">password = "your_target_password"</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">tls = true</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">off_reply = false</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">[filter]</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">block_command = ["SELECT", "FLUSHALL", "FLUSHDB"]</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">block_command_group = ["STREAM","GEO","HYPERLOGLOG"]</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">[advanced]</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">dir             = "/data"</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">ncpu            = 1</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">status_port     = 8084</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">pprof_port      = 9094</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">log_file        = "/data/shake.log"</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">log_level       = "info"</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">log_interval    = 5</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">log_rotation    = true</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">log_max_size    = 512</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">log_max_age     = 7</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">log_max_backups = 3</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">log_compress    = true</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">rdb_restore_command_behavior  = "rewrite"</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">pipeline_count_limit          = 512</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">target_redis_max_qps          = 30000</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">empty_db_before_sync          = false</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">target_redis_oom_requeue = true</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">target_redis_oom_requeue_max_times = 3</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">target_redis_oom_requeue_delay_ms = 500</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">io_reconnect = true</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">io_reconnect_max_times = 100</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">io_reconnect_delay_ms = 10000</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">target_redis_writer_shards=4</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">target_redis_proto_max_bulk_len = 512_000_000</span><br></span></code></pre></div></div>
<hr>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="stage-2-diverting-read-traffic">Stage 2: Diverting Read Traffic<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#stage-2-diverting-read-traffic" class="hash-link" aria-label="Direct link to Stage 2: Diverting Read Traffic" title="Direct link to Stage 2: Diverting Read Traffic" translate="no">​</a></h3>
<p>Once the synchronization is stable and replication lag is minimal (usually measured in microseconds), you can begin utilizing EloqKV.</p>
<p>Because recommendation systems are heavily dependent on fast lookups, we switch the <strong>Read Traffic</strong> first. By pointing your application’s read clients to EloqKV, you can verify that the SSD-based architecture meets your latency requirements under real-world conditions.</p>
<blockquote>
<p><strong>Pro Tip:</strong> Monitor your p99 latencies during this stage. Most users find that EloqKV’s SSD performance is indistinguishable from Redis DRAM performance for feature lookups, but at a fraction of the cost.</p>
</blockquote>
<hr>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="stage-3-the-final-cutover-write-traffic">Stage 3: The Final Cutover (Write Traffic)<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#stage-3-the-final-cutover-write-traffic" class="hash-link" aria-label="Direct link to Stage 3: The Final Cutover (Write Traffic)" title="Direct link to Stage 3: The Final Cutover (Write Traffic)" translate="no">​</a></h3>
<p>With reads successfully validated on EloqKV, it is time to move the "Source of Truth."</p>
<ol>
<li class=""><strong>Stop Ingestion to Redis:</strong> Briefly halt your data pipelines or set the Redis source to read-only.</li>
<li class=""><strong>Verify Buffer Flush:</strong> Ensure RedisShake has finished shipping the final set of incremental changes.</li>
<li class=""><strong>Point Writes to EloqKV:</strong> Update your ingestion workers (e.g., Flink, Spark, or custom API workers) to write directly to the EloqKV endpoint.</li>
<li class=""><strong>Decommission:</strong> Once the write path is stable, you can safely spin down the expensive, memory-bloated Redis instances.</li>
</ol>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion-economics-at-scale">Conclusion: Economics at Scale<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#conclusion-economics-at-scale" class="hash-link" aria-label="Direct link to Conclusion: Economics at Scale" title="Direct link to Conclusion: Economics at Scale" translate="no">​</a></h2>
<p>By migrating your memory-bound Feature Store to EloqKV, you effectively decouple your data growth from your DRAM budget. You gain the ability to store 10x the features on the same budget, providing your recommendation models with more context and your business with a lower TCO (Total Cost of Ownership).</p>
<p>The transition is low-risk, the performance remains "extreme," and the SSD-based cost model finally makes large-scale data sets sustainable.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="try-it-yourself">Try it yourself<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#try-it-yourself" class="hash-link" aria-label="Direct link to Try it yourself" title="Direct link to Try it yourself" translate="no">​</a></h2>
<ul>
<li class=""><strong>Product overview:</strong> <a class="" href="https://www.eloqdata.com/product/eloqkv">EloqKV product page</a></li>
<li class=""><strong>Estimate savings:</strong> <a class="" href="https://www.eloqdata.com/costsaving">Cost calculator</a> and the <a class="" href="https://www.eloqdata.com/post/redis-vs-eloqkv-cost-breakdown-at-scale">Redis vs EloqKV cost breakdown</a></li>
<li class=""><strong>Latency proof:</strong> <a class="" href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore">EloqKV on EloqStore benchmark</a></li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="frequently-asked-questions">Frequently Asked Questions<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#frequently-asked-questions" class="hash-link" aria-label="Direct link to Frequently Asked Questions" title="Direct link to Frequently Asked Questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="is-there-downtime-during-the-migration">Is there downtime during the migration?<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#is-there-downtime-during-the-migration" class="hash-link" aria-label="Direct link to Is there downtime during the migration?" title="Direct link to Is there downtime during the migration?" translate="no">​</a></h3>
<p>No. RedisShake mirrors your data into EloqKV with a full sync followed by continuous incremental sync, so reads and writes keep serving from Redis until you choose to divert them. The only brief pause is the final write cutover in Stage 3.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="which-redis-commands-and-data-types-are-not-supported">Which Redis commands and data types are not supported?<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#which-redis-commands-and-data-types-are-not-supported" class="hash-link" aria-label="Direct link to Which Redis commands and data types are not supported?" title="Direct link to Which Redis commands and data types are not supported?" translate="no">​</a></h3>
<p>EloqKV targets core key-value and common structures, not <code>STREAM</code>, <code>GEO</code>, or <code>HYPERLOGLOG</code>, which the sample <code>shake.toml</code> blocks from sync. Check your command and type usage against the <a class="" href="https://www.eloqdata.com/eloqkv/kvstore_compatibility">command compatibility reference</a> before migrating.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="do-my-existing-redis-clients-still-work">Do my existing Redis clients still work?<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#do-my-existing-redis-clients-still-work" class="hash-link" aria-label="Direct link to Do my existing Redis clients still work?" title="Direct link to Do my existing Redis clients still work?" translate="no">​</a></h3>
<p>Most Redis and Valkey clients work without changes because EloqKV is wire-compatible. Confirm your specific driver and cluster-mode behavior against the <a class="" href="https://www.eloqdata.com/eloqkv/client_compatibility">client compatibility reference</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-i-validate-latency-before-moving-writes">How do I validate latency before moving writes?<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#how-do-i-validate-latency-before-moving-writes" class="hash-link" aria-label="Direct link to How do I validate latency before moving writes?" title="Direct link to How do I validate latency before moving writes?" translate="no">​</a></h3>
<p>Define a latency SLO such as a P99 GET target, divert read traffic to EloqKV in Stage 2, and monitor P99 and P99.99 under real load before cutover. The <a class="" href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore">benchmark article</a> shows reference tail-latency numbers on NVMe.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-i-roll-back-if-something-goes-wrong">How do I roll back if something goes wrong?<a href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv#how-do-i-roll-back-if-something-goes-wrong" class="hash-link" aria-label="Direct link to How do I roll back if something goes wrong?" title="Direct link to How do I roll back if something goes wrong?" translate="no">​</a></h3>
<p>Until the Stage 3 write cutover, Redis remains the source of truth, so rolling back is simply leaving reads and writes on Redis. Keep Redis running until EloqKV has served production write traffic and met your SLOs, then decommission it.</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Scaling EloqKV to Power 800 Billion ChatGPT Agents: Beyond the Limits of PostgreSQL]]></title>
        <id>https://www.eloqdata.com/blog/2026/01/26/800-billion-agents</id>
        <link href="https://www.eloqdata.com/blog/2026/01/26/800-billion-agents"/>
        <updated>2026-01-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[OpenAI just showed how to push PostgreSQL to its limit for 800 million users. But what happens when 800 billion AI agents take over?]]></summary>
        <content type="html"><![CDATA[<p>First, a huge thank you to <strong>Bohan Zhang</strong> and the OpenAI engineering team for sharing their <a href="https://openai.com/index/scaling-postgresql/" target="_blank" rel="noopener noreferrer" class="">data infrastructure journey</a>. It is a fantastic read, packed with hands-on best practices—from connection pooling with PgBouncer to ruthless query optimization—that any engineer scaling a relational monolith should study.</p>
<p>However, the definition of "scale" is shifting under our feet.</p>
<p>On January 22nd at the <strong>Unlocked Conf</strong> (shout out to <strong>Khawaja</strong> and <strong>Daniela</strong> at <strong>Momento</strong> and always smile <strong>Madelyn</strong> for hosting such an incredible event!), the industry saw what "real" scale looks like today. Experts from <strong>Uber</strong> (Yang Yang and Shawn Wang), <strong>AWS</strong> (Kevin and Sarthak), and <strong>Apple</strong> (Yiwen Zhang) shared mind-boggling numbers. Most notably, <strong>Uber is now handling 1 billion RPS</strong> using their cache layer. In that context, "millions of QPS" is just the starting line.</p>
<div align="center"><div style="width:100%;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/800billlionagents-23c6cb56d9bc747a9c00951697731f70.jpg" alt="EloqKV P9999 Latency Comparison" style="cursor:pointer;max-width:100%"></div></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="from-800-million-users-to-800-billion-agents">From 800 Million Users to 800 Billion Agents<a href="https://www.eloqdata.com/blog/2026/01/26/800-billion-agents#from-800-million-users-to-800-billion-agents" class="hash-link" aria-label="Direct link to From 800 Million Users to 800 Billion Agents" title="Direct link to From 800 Million Users to 800 Billion Agents" translate="no">​</a></h3>
<p>OpenAI’s current architecture of 50 read replicas is a masterpiece of efficiency for 800 million humans. But we are rapidly approaching the <strong>Agentic Era</strong>. We aren't just building for humans who type a few prompts a day; we are building for <strong>800 billion autonomous agents</strong> that never sleep, never stop querying, and never stop generating data.</p>
<p>When we talk about a <strong>Trillion QPS</strong> in a major AI company, we have to ask: <strong>Can PostgreSQL read replicas actually survive?</strong></p>
<p>Even with cascading WAL shipping, the infrastructure cost of 50,000+ replicas is a budgetary black hole. More importantly, the <strong>replication lag</strong> and the <strong>CPU overhead</strong> of a traditional RDBMS engine make it the wrong tool for the "Trillion QPS" future.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-cache-miss-vulnerability">The "Cache Miss" Vulnerability<a href="https://www.eloqdata.com/blog/2026/01/26/800-billion-agents#the-cache-miss-vulnerability" class="hash-link" aria-label="Direct link to The &quot;Cache Miss&quot; Vulnerability" title="Direct link to The &quot;Cache Miss&quot; Vulnerability" translate="no">​</a></h3>
<p>OpenAI’s post highlights a critical defense: a <strong>cache locking and leasing mechanism</strong> to prevent "thundering herds" from crashing PostgreSQL during a cache-miss storm. It’s a brilliant safety net, but it proves that the database is a liability in a high-velocity environment.</p>
<p><strong>Our question is simple: Why not just cache all the data?</strong></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="enter-eloqkv-the-full-cache-reality">Enter EloqKV: The "Full Cache" Reality<a href="https://www.eloqdata.com/blog/2026/01/26/800-billion-agents#enter-eloqkv-the-full-cache-reality" class="hash-link" aria-label="Direct link to Enter EloqKV: The &quot;Full Cache&quot; Reality" title="Direct link to Enter EloqKV: The &quot;Full Cache&quot; Reality" translate="no">​</a></h3>
<p>OpenAI mentions that their cache only holds "hot data" because keeping everything in memory is too expensive. In the traditional Redis world, they are right. Keeping 800 million users' (or 800 billion agents) session data in RAM is a waste of capital.</p>
<p>EloqKV changes the math. By building on EloqStore, we’ve created a Redis-compatible KV which holds <strong>all your data on NVMe SSDs</strong>, creating a "Full Cache" architecture where <strong>cache misses simply don't exist.</strong></p>
<ul>
<li class=""><strong>No More Cache Miss:</strong> By making the "cache" with full data as high-performance layer for agent state, you eliminate the risk of a "burst of misses" crashing a back-end database.</li>
<li class=""><strong>NVMe Performance at Scale:</strong> On a 16vCore GCP instance, EloqKV delivers <strong>sub-millisecond P99 latency</strong> and <strong>under 2 millisecond P9999 latency</strong> at 100K QPS. Need more? A single 44vCore machine can push <strong>near 1 million QPS</strong>.</li>
<li class=""><strong>10x Cost Savings:</strong> PostgreSQL is great for relations, but for the raw key-value lookups required by 800 billion agents, it’s 10x more expensive than an SSD-optimized KV store like EloqKV.</li>
</ul>
<p>How does <strong>EloqKV on EloqStore</strong> achieve it? Read our Tech: <a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#introducing-our-open-source-eloqstore-engine" target="_blank" rel="noopener noreferrer" class="">EloqKV on EloqStore Introdution</a></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="but-what-about-joins">But What About Joins?<a href="https://www.eloqdata.com/blog/2026/01/26/800-billion-agents#but-what-about-joins" class="hash-link" aria-label="Direct link to But What About Joins?" title="Direct link to But What About Joins?" translate="no">​</a></h3>
<p>The classic rebuttal to KV stores is the lack of relational joins. However, as OpenAI themselves noted, they are already moving away from 12-table joins and toward application-layer logic.</p>
<p>We believe in the DynamoDB Philosophy: Design your data schema based on your access patterns. If you need predictable, single-digit millisecond performance at any scale, you denormalize.</p>
<p>The problem has always been that DynamoDB is prohibitively expensive at high QPS. If you’ve ever looked at a bill for 1 million provisioned WCUs/RCUs, you know the pain. <strong>EloqKV</strong> brings that same philosophy to your own infrastructure, making a <strong>Trillion QPS future</strong> not just technically possible, but economically affordable.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="looking-forward">Looking Forward<a href="https://www.eloqdata.com/blog/2026/01/26/800-billion-agents#looking-forward" class="hash-link" aria-label="Direct link to Looking Forward" title="Direct link to Looking Forward" translate="no">​</a></h3>
<p>We agree with OpenAI that PostgreSQL is an incredible tool. But for the next generation of agent-generated traffic—where QPS will reach billions—the "Primary + 50k Replicas + Hot Cache" model will be too slow and too expensive.</p>
<p>The future belongs to Full Cache architectures that treat SSDs like memory and eliminate the database bottleneck entirely.</p>
<hr>
<p><strong>Ready to scale for the Agentic Era?</strong></p>
<ul>
<li class=""><strong>Read the Tech:</strong> <a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore" target="_blank" rel="noopener noreferrer" class="">EloqKV on EloqStore</a></li>
<li class=""><strong>Explore the Source Code:</strong> <a href="https://github.com/eloqdata/eloqstore" target="_blank" rel="noopener noreferrer" class="">EloqStore Repo</a> &amp; <a href="https://github.com/eloqdata/eloqkv" target="_blank" rel="noopener noreferrer" class="">EloqKV Repo</a></li>
<li class=""><strong>Try it Now:</strong> <a href="https://www.eloqdata.com/eloqkv/install-from-binary" target="_blank" rel="noopener noreferrer" class="">Install EloqKV in One Minute</a></li>
</ul>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Breaking the Memory Barrier: Introducing EloqKV on EloqStore]]></title>
        <id>https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore</id>
        <link href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore"/>
        <updated>2026-01-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[EloqKV on EloqStore benchmark: P99.99 latency of a few milliseconds on NVMe for datasets up to 2TB, where a DRAM-only Redis node cannot fit the data—at a fraction of the cost.]]></summary>
        <content type="html"><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="introduction">Introduction<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#introduction" class="hash-link" aria-label="Direct link to Introduction" title="Direct link to Introduction" translate="no">​</a></h2>
<p>At <strong>CES 2026</strong>, NVIDIA CEO Jensen Huang delivered a stark warning: the industry is facing a critical shortage of DRAM. While the explosive growth of AI models is the primary driver, there is another massive consumer of memory that often flies under the radar: <strong>Caching Services</strong>.</p>
<p>Traditionally, caching services like <a href="https://redis.io/" target="_blank" rel="noopener noreferrer" class="">Redis</a> and <a href="https://valkey.io/" target="_blank" rel="noopener noreferrer" class="">Valkey</a> are purely memory-based. Even though people have tried to leverage fast SSDs for caching (e.g. <a href="https://kvrocks.apache.org/" target="_blank" rel="noopener noreferrer" class="">Apache KVRocks</a>), for latency-sensitive workloads, DRAM-based solutions remained the only viable solution because SSD-based alternatives often have significant tail latency issues. In mission-critical environments, a latency spike can easily disrupt real-time workflows and render a service unresponsive. Until recently, how to tame tail latency for IO intensive workloads has remained an unsolved challenge.</p>
<blockquote>
<p><strong>Quick answer:</strong> EloqKV on EloqStore serves Redis-style workloads from NVMe SSD while holding <strong>P99.99 latency to a few milliseconds</strong> — on datasets up to <strong>2TB</strong>, where a DRAM-only Redis node cannot fit the data at all. You get memory-like tail latency at disk-like cost, with the <strong>20X</strong> node reduction detailed below. See the <a class="" href="https://www.eloqdata.com/product/eloqkv">EloqKV product page</a> and estimate your own savings with the <a class="" href="https://www.eloqdata.com/costsaving">cost calculator</a>.</p>
</blockquote>
<p>Today, we're proud to introduce <a href="https://github.com/eloqdata/eloqstore" target="_blank" rel="noopener noreferrer" class="">EloqKV powered by EloqStore</a>. By leveraging the innovation of a modern data storage engine, EloqKV is the first Redis-compatible KV store designed to deliver DRAM-level P9999 latency with modern NVMe SSDs. By eliminating the performance tail that plagues traditional systems, EloqKV, when powered by EloqStore, offers up to <strong>20X cost savings</strong> without sacrificing the millisecond-level latency your business demands.</p>
<p>As discussed in our previous blogs, <strong>EloqKV</strong> is not just a cache. It can work as a full <a class="" href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog">persistent main data storage</a>. In this blog, we re-examine the cache usage case and we will leave the full persistent evaluation in another post. In all our tests, we turn off the write ahead log (WAL) to relax the strong durability guarantee.</p>
<div align="center"><div style="width:100%;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/eloqkv_redis_read-44d37d6ad3d4a6f5302fab935cdcb866.png" alt="EloqKV P9999 Latency Comparison" style="cursor:pointer;max-width:100%"></div></div></div>
<p>Traditional DRAM-based systems like Redis are strictly limited by physical memory capacity. As shown in the benchmark above, while Redis is restricted to smaller datasets, EloqKV breaks this barrier by leveraging NVMe without the "tail-latency penalty" typically associated with disks. In fact, at 20GB and 100GB, EloqKV actually provides <strong>higher throughput</strong> than Redis while maintaining a near identical <strong>P99.99 latency</strong> profile. When data size exceeds main memory size, Redis fails while EloqKV continues to scale. Even with a <strong>2TB dataset</strong>, EloqKV maintains a stable P99.99 latency of a few milliseconds, a performance previously thought possible only in pure DRAM environments.</p>
<p>For comparison, we also tried <a href="https://kvrocks.apache.org/" target="_blank" rel="noopener noreferrer" class="">KVRocks</a>, a solution that supports SSD serving. As shown below, the P99.99 latency grows out of control as IO intensive operations interfere with the read latencies (notice the log scale).</p>
<div align="center"><div style="width:100%;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/eloqkv_kvrocks_2tb-a37e9f88262358309c824bbd1d9dfc80.png" alt="EloqKV P9999 Latency Comparison" style="cursor:pointer;max-width:100%"></div></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="benchmark-conditions">Benchmark conditions<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#benchmark-conditions" class="hash-link" aria-label="Direct link to Benchmark conditions" title="Direct link to Benchmark conditions" translate="no">​</a></h3>
<p>These results reflect one specific setup. We share it in full so you can judge how well it maps to your own workload.</p>
<ul>
<li class=""><strong>Hardware:</strong> Single node, GCP <code>Z3-16</code> instance — 16 vCore, 128GB RAM, 2&nbsp;×&nbsp;2.9TB NVMe SSD.</li>
<li class=""><strong>Dataset sizes:</strong> 20GB, 100GB, 200GB, and 2TB (up to ~800 million KV pairs).</li>
<li class=""><strong>Payload:</strong> Variable value sizes of 1–4KB.</li>
<li class=""><strong>Read/write mixes:</strong> 95:5 (read-dominant), 50:50 (balanced), and 5:95 (write-dominant). The scaling table below uses the read-dominant (GET) workload; the balanced and write-heavy mixes appear in the charts above.</li>
<li class=""><strong>Persistence mode:</strong> Write-ahead log (WAL) <strong>disabled</strong> — i.e. cache mode — to isolate serving latency. Durable, WAL-backed performance is evaluated separately in <a class="" href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog">ACID in EloqKV: Durability</a>.</li>
<li class=""><strong>Benchmark tool:</strong> <code>memtier_benchmark</code>.</li>
<li class=""><strong>Metric definitions:</strong> <strong>P99</strong> is the 99th-percentile latency (slower than 99% of requests). <strong>P99.99</strong> is the 99.99th-percentile latency — the worst 1 in 10,000 requests — the figure that exposes the tail-latency spikes SSD caches usually suffer.</li>
</ul>
<p>The scaling test holds the read-dominant workload steady while the dataset grows. EloqKV keeps a flat P99.99 profile across all four sizes; Redis cannot hold the 200GB and 2TB datasets on this single node because they exceed its RAM.</p>
<table><thead><tr><th>Dataset size</th><th>EloqKV throughput (ops/s)</th><th>EloqKV P99.99 GET (ms)</th><th>Redis throughput (ops/s)</th><th>Redis P99.99 GET (ms)</th></tr></thead><tbody><tr><td>20GB</td><td>~507,000</td><td>1.55</td><td>~162,000</td><td>1.43</td></tr><tr><td>100GB</td><td>~299,000</td><td>1.43</td><td>~155,000</td><td>1.51</td></tr><tr><td>200GB</td><td>~281,000</td><td>2.03</td><td>— (exceeds RAM)</td><td>—</td></tr><tr><td>2TB</td><td>~262,000</td><td>2.61</td><td>— (exceeds RAM)</td><td>—</td></tr></tbody></table>
<p>By solving the P99.99 latency problem on SSDs, EloqKV allows businesses to scale their data footprint by orders of magnitude without sacrificing the sub-millisecond responsiveness their users demand. To manage a 2TB dataset with Redis, an organization would typically need a cluster of <strong>20 nodes</strong> to provide enough RAM. EloqKV delivers the same "long-tail" latency reliably on a single NVMe-optimized node — a <strong>20X</strong> reduction in nodes for this 2TB scenario.</p>
<p>The actual cost saving depends on your dataset size, hot working set, replica count, and durability needs, so the multiple will differ for your workload. Model your own numbers with the <a class="" href="https://www.eloqdata.com/costsaving">EloqKV cost calculator</a>, or read the full <a class="" href="https://www.eloqdata.com/post/redis-vs-eloqkv-cost-breakdown-at-scale">Redis vs EloqKV cost breakdown</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-long-tail-problem-why-ssd-caching-is-hard">The Long Tail Problem: Why SSD Caching is Hard<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#the-long-tail-problem-why-ssd-caching-is-hard" class="hash-link" aria-label="Direct link to The Long Tail Problem: Why SSD Caching is Hard" title="Direct link to The Long Tail Problem: Why SSD Caching is Hard" translate="no">​</a></h2>
<p>At the upcoming <strong>Unlocked Conference 2026</strong>, engineers from <strong>Uber</strong> will present <em>"Real-world cache lessons from 1B RPS in prod."</em> This highlights the scale modern infrastructure must handle. While a standard PostgreSQL or MySQL setup works fine for low throughput workloads and moderate-sized hot datasets, it crumbles under the "Internet Scale" traffic processed by Google, Amazon, Uber, and Snap.</p>
<p>The challenge is compounding in the <strong>AI Age</strong>. We are moving toward a future of Agent-to-Agent communication, which will generate <strong>100x more data and RPS</strong> than human interaction. We see this with our own customers: AI chat scenarios create massive data explosion because prompts are long, complex, and carry context payloads 100x larger than typical human chat messages.</p>
<p>Since DRAM is expensive, the obvious alternative is to use SSDs. However, for high-performance caching, standard SSD implementations have historically failed. The dealbreaker isn't average latency, instead, it's the <strong>long tail latency</strong>, especially when dealing with millions or billions of requests per second (RPS). When you throw this level of concurrency at a standard SSD-based database, traditional architectures fail. Relying on multi-threading and synchronous I/O simply cannot guarantee consistent low tail latency on commodity hardware. The threads block, the context switches pile up, and the tail latency spikes.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-software-hardware-gap">The Software-Hardware Gap<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#the-software-hardware-gap" class="hash-link" aria-label="Direct link to The Software-Hardware Gap" title="Direct link to The Software-Hardware Gap" translate="no">​</a></h2>
<p>Our research found that the issue with the unstable performance of current storage design is that the software stack, traditionally designed for hard-disk based storage systems, has not caught up with the times. Traditionally, disk is slow, and every effort should be made to increase the performance of disks. However, modern NVMe SSDs are incredibly fast, capable of delivering <strong>millions of IOPS</strong> and <strong>gigabytes per second throughput</strong>, even for random data accesses. To take advantage of such high performance disks, a few things need to be changed.</p>
<p>The first and most fundamental change is the overall database architecture, which we described in detail in our previous <a class="" href="https://www.eloqdata.com/blog/2025/07/14/technology">blog posts</a>. In this blog, we discuss several implementation issues that allow us to fully exploit modern storage devices. In addition, two specific implementation techniques are also essential:</p>
<ol>
<li class=""><strong>Coroutines:</strong> To handle massive concurrency without the overhead of OS threads.</li>
<li class=""><strong>io_uring:</strong> The Linux kernel interface that enables high-performance asynchronous I/O.</li>
</ol>
<p>By combining these, we bypass the limitations of synchronous I/O. You can read more about our engineering philosophy on this topic in our deep dive: <a href="https://www.eloqdata.com/blog/2025/09/26/coroutines-async-programming" target="_blank" rel="noopener noreferrer" class="">Coroutines &amp; Async Programming</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="introducing-our-open-source-eloqstore-engine">Introducing Our Open Source EloqStore Engine<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#introducing-our-open-source-eloqstore-engine" class="hash-link" aria-label="Direct link to Introducing Our Open Source EloqStore Engine" title="Direct link to Introducing Our Open Source EloqStore Engine" translate="no">​</a></h2>
<p>We developed <strong>EloqStore</strong>, a modern storage engine that is optimized for and takes advantage of the Data Substrate architecture to achieve unparalleled performance and stability. We are thrilled to announce that it is now <strong>Open Source</strong>.</p>
<p><a href="https://github.com/eloqdata/eloqstore" target="_blank" rel="noopener noreferrer" class=""><strong>Check out EloqStore on GitHub</strong></a></p>
<p>EloqData's Data Substrate technology is designed for plugable storage engines. Previously, EloqKV utilized <strong>RocksDB</strong> and <strong>RocksCloud</strong> as its main underlying storage engine. While robust, RocksDB relies on Log-Structured Merge-trees (LSM), which suffer from well-known issues that destabilize long tail latency:</p>
<ul>
<li class=""><strong>Write Amplification:</strong> Repeatedly rewriting data during compaction.</li>
<li class=""><strong>Read Amplification:</strong> Checking multiple files to find a single key.</li>
<li class=""><strong>Compaction Stalls:</strong> CPU spikes during background compaction tasks.</li>
</ul>
<p>EloqStore addresses these issues head-on by abandoning the traditional Log-Structured Merge-tree (LSM) architecture in favor of a design optimized specifically for the high-parallelism nature of modern NVMe SSDs. While LSM-trees are designed to sequentialize writes for older spinning disks, EloqStore is built from the ground up to exploit the random-access strengths and massive IOPS of modern flash storage.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="deterministic-performance-via-b-tree-variant-indexing">Deterministic Performance via B-tree Variant Indexing<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#deterministic-performance-via-b-tree-variant-indexing" class="hash-link" aria-label="Direct link to Deterministic Performance via B-tree Variant Indexing" title="Direct link to Deterministic Performance via B-tree Variant Indexing" translate="no">​</a></h4>
<p>To achieve deterministic performance, EloqStore utilizes a specialized B-tree variant indexing strategy. It maintains a compact, memory-resident mapping of all non-leaf nodes, ensuring that the internal path to any piece of data is always held in DRAM.</p>
<p>This architecture guarantees exactly one disk access per read. By bypassing the multiple "level checks" found in RocksDB, EloqStore eliminates the read amplification that typically causes latency spikes during heavy lookups. Whether your dataset is 100GB or 10TB, a read request is always a direct, single-IOPS operation, providing the predictable latency required for mission-critical caching.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="high-throughput-batch-write-optimization">High-Throughput Batch Write Optimization<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#high-throughput-batch-write-optimization" class="hash-link" aria-label="Direct link to High-Throughput Batch Write Optimization" title="Direct link to High-Throughput Batch Write Optimization" translate="no">​</a></h4>
<p>The write path is equally optimized through Batch Write Optimization. Leveraging io_uring, EloqStore bypasses the overhead of traditional synchronous system calls, allowing the engine to group multiple incoming writes into large, aligned blocks. This approach:</p>
<ul>
<li class="">
<p>Reduces the frequency of expensive disk commits.</p>
</li>
<li class="">
<p>Significantly lowers write amplification.</p>
</li>
<li class="">
<p>Preserves the lifespan of the SSD while maintaining massive throughput.</p>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="coroutines-and-async-io">Coroutines and Async I/O<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#coroutines-and-async-io" class="hash-link" aria-label="Direct link to Coroutines and Async I/O" title="Direct link to Coroutines and Async I/O" translate="no">​</a></h4>
<p>Under the hood, EloqStore is built with coroutines. This allows the system to handle thousands of concurrent requests without the heavy memory footprint or context-switching penalties of OS threads. When an I/O operation is pending, the coroutine simply yields, allowing the CPU to process other requests until the NVMe signals completion via io_uring. Coroutine-based language-level concurrency and kernel-level asynchronous I/O allow EloqKV to maintain sub-millisecond responsiveness under heavy load.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="eliminating-the-compaction-stall">Eliminating the "Compaction Stall"<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#eliminating-the-compaction-stall" class="hash-link" aria-label="Direct link to Eliminating the &quot;Compaction Stall&quot;" title="Direct link to Eliminating the &quot;Compaction Stall&quot;" translate="no">​</a></h4>
<p>Most importantly, this architecture eliminates the jitter caused by background maintenance. Traditional LSM-trees must periodically merge and rewrite files to maintain performance—a process that consumes massive CPU and I/O resources, leading to the dreaded "compaction stall." EloqStore's append-only design manages data reclamation more gracefully, ensuring that background tasks never interfere with foreground traffic. The result is a P9999 latency profile that stays flat, delivering a "memory-like" experience at disk-based economics.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="rethinking-the-key-value-store-for-the-ai-era">Rethinking the Key-Value Store for the AI Era<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#rethinking-the-key-value-store-for-the-ai-era" class="hash-link" aria-label="Direct link to Rethinking the Key-Value Store for the AI Era" title="Direct link to Rethinking the Key-Value Store for the AI Era" translate="no">​</a></h3>
<p>Beyond its performance as a cache, EloqKV is a <a class="" href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog">full-featured key-value store</a>. This architectural shift enables a new class of advanced features:</p>
<ul>
<li class="">
<p><strong>Cloud-Native "Scale to Zero":</strong> Because data is persisted on NVMe and/or object storage rather than volatile DRAM, EloqKV can spin down to zero when idle.</p>
</li>
<li class="">
<p><strong>AI-Native "Quick Branching":</strong> For complex AI workflows and agent simulations, EloqKV supports rapid branching. You can fork your state to explore different prompt chains or agentic outcomes without duplicating massive DRAM footprints.</p>
</li>
<li class="">
<p><strong>Automatic Archiving:</strong> Archiving is no longer a separate operation and you do not need to choose a "cut-off" period. <strong>EloqKV</strong> does not even require you to have sufficient capacity to store all your data on local SSD. In fact, cold data can be seamlessly and automatically tiered to low-cost object storage such as S3. The only penalty is a few seconds of delay when accessing cold data.</p>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="meet-us-at-unlocked-2026">Meet us at Unlocked 2026<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#meet-us-at-unlocked-2026" class="hash-link" aria-label="Direct link to Meet us at Unlocked 2026" title="Direct link to Meet us at Unlocked 2026" translate="no">​</a></h3>
<p>We are excited to announce that <strong>EloqData is a sponsor of the <a href="https://unlocked.gomomento.com/" target="_blank" rel="noopener noreferrer" class="">Unlocked Conference 2026</a></strong>, hosted by Momento and AWS.</p>
<p>Unlocked is the premier event for developers discussing the future of backend infrastructure. We are eager to discuss how the shift from DRAM to NVMe can unlock new possibilities for AI and hyperscale applications.</p>
<p>Come visit our booth, chat with our engineers, and see EloqStore in action. We look forward to seeing you there.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="try-eloqkv">Try EloqKV<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#try-eloqkv" class="hash-link" aria-label="Direct link to Try EloqKV" title="Direct link to Try EloqKV" translate="no">​</a></h2>
<ul>
<li class=""><strong>Product overview:</strong> <a class="" href="https://www.eloqdata.com/product/eloqkv">EloqKV product page</a></li>
<li class=""><strong>Estimate savings:</strong> <a class="" href="https://www.eloqdata.com/costsaving">Cost calculator</a></li>
<li class=""><strong>Already on Redis?</strong> Follow the <a class="" href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv">Redis-to-EloqKV migration guide</a></li>
<li class=""><strong>Source:</strong> <a href="https://github.com/eloqdata/eloqkv" target="_blank" rel="noopener noreferrer" class="">EloqKV</a> and <a href="https://github.com/eloqdata/eloqstore" target="_blank" rel="noopener noreferrer" class="">EloqStore</a> on GitHub</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="frequently-asked-questions">Frequently Asked Questions<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#frequently-asked-questions" class="hash-link" aria-label="Direct link to Frequently Asked Questions" title="Direct link to Frequently Asked Questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-does-eloqkv-keep-p9999-latency-low-on-ssd">How does EloqKV keep P99.99 latency low on SSD?<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#how-does-eloqkv-keep-p9999-latency-low-on-ssd" class="hash-link" aria-label="Direct link to How does EloqKV keep P99.99 latency low on SSD?" title="Direct link to How does EloqKV keep P99.99 latency low on SSD?" translate="no">​</a></h3>
<p>EloqStore keeps every index's non-leaf nodes in DRAM, so each read is a single, direct NVMe access (one IOP) with no LSM-style level checks. It pairs this with coroutines and <code>io_uring</code> asynchronous I/O, and its append-only design avoids the compaction stalls that spike tail latency on other SSD stores. The result is a P99.99 of a few milliseconds even at 2TB.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-exactly-were-the-benchmark-conditions">What exactly were the benchmark conditions?<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#what-exactly-were-the-benchmark-conditions" class="hash-link" aria-label="Direct link to What exactly were the benchmark conditions?" title="Direct link to What exactly were the benchmark conditions?" translate="no">​</a></h3>
<p>A single GCP <code>Z3-16</code> node (16 vCore, 128GB RAM, 2&nbsp;×&nbsp;2.9TB NVMe), 1–4KB values, datasets from 20GB to 2TB, read/write mixes of 95:5, 50:50, and 5:95, measured with <code>memtier_benchmark</code> and the write-ahead log disabled (cache mode). See the <a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#benchmark-conditions" class="">Benchmark conditions</a> section for the full table.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="do-these-results-require-turning-off-durability">Do these results require turning off durability?<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#do-these-results-require-turning-off-durability" class="hash-link" aria-label="Direct link to Do these results require turning off durability?" title="Direct link to Do these results require turning off durability?" translate="no">​</a></h3>
<p>These specific numbers were measured in cache mode (WAL off) to isolate serving latency. EloqKV also supports WAL-backed durable persistence; that mode is benchmarked separately in <a class="" href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog">ACID in EloqKV: Durability</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-much-can-eloqkv-save-versus-redis">How much can EloqKV save versus Redis?<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#how-much-can-eloqkv-save-versus-redis" class="hash-link" aria-label="Direct link to How much can EloqKV save versus Redis?" title="Direct link to How much can EloqKV save versus Redis?" translate="no">​</a></h3>
<p>In this 2TB test, EloqKV replaces a roughly 20-node Redis cluster with a single NVMe node. Real savings depend on your data size, hot working set, replicas, and durability requirements — estimate yours with the <a class="" href="https://www.eloqdata.com/costsaving">cost calculator</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="is-eloqkv-a-drop-in-redis-replacement">Is EloqKV a drop-in Redis replacement?<a href="https://www.eloqdata.com/blog/2026/01/08/eloqkv-on-eloqstore#is-eloqkv-a-drop-in-redis-replacement" class="hash-link" aria-label="Direct link to Is EloqKV a drop-in Redis replacement?" title="Direct link to Is EloqKV a drop-in Redis replacement?" translate="no">​</a></h3>
<p>EloqKV is Redis- and Valkey-compatible, which keeps migration effort low, but teams should validate command coverage, cluster behavior, persistence settings, and latency SLOs before cutover. The <a class="" href="https://www.eloqdata.com/blog/2026/04/22/redis-migrate-to-eloqkv">migration guide</a> walks through this.</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[MinIO Community Edition Enters Maintenance-Only — Can You Still Trust Your Self-Hosted S3?]]></title>
        <id>https://www.eloqdata.com/blog/2025/12/10/minio-maintenance</id>
        <link href="https://www.eloqdata.com/blog/2025/12/10/minio-maintenance"/>
        <updated>2025-12-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A 48-Hour Emergency Plan for Engineers.]]></summary>
        <content type="html"><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="introduction">Introduction<a href="https://www.eloqdata.com/blog/2025/12/10/minio-maintenance#introduction" class="hash-link" aria-label="Direct link to Introduction" title="Direct link to Introduction" translate="no">​</a></h2>
<p>MinIO has officially moved its <strong>Community Edition</strong> to a <strong>“maintenance-only”</strong> release model:</p>
<ul>
<li class="">No more prebuilt binaries from the community.</li>
<li class="">No regular acceptance of new features or PRs.</li>
<li class="">Only critical security patches will be handled when necessary.</li>
</ul>
<p>In plain terms:
If your production object storage is built on MinIO Community Edition, you are now carrying <strong>hidden operational risk and rising maintenance costs</strong>.</p>
<!-- -->
<div align="center"><div style="width:75%;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/miniomaintenance-2bc8b1cc33b05fc4572f89c99d227f41.jpg" alt="x" style="cursor:pointer;max-width:100%"></div></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="for-sres-three-things-to-do-immediately">For SREs: Three Things To Do Immediately<a href="https://www.eloqdata.com/blog/2025/12/10/minio-maintenance#for-sres-three-things-to-do-immediately" class="hash-link" aria-label="Direct link to For SREs: Three Things To Do Immediately" title="Direct link to For SREs: Three Things To Do Immediately" translate="no">​</a></h2>
<ol>
<li class="">
<p>Classify your MinIO usage:</p>
<ul>
<li class="">Mission-critical production</li>
<li class="">Non-critical environment</li>
<li class="">Test / dev only</li>
</ul>
</li>
<li class="">
<p>For critical workloads, start a <strong>48-hour PoC immediately</strong>.</p>
</li>
<li class="">
<p>Select candidates that are:</p>
<ul>
<li class=""><strong>S3-compatible</strong></li>
<li class="">Actively maintained</li>
<li class="">Commercially supportable
(e.g., Ceph RGW, SeaweedFS, RustFS), and run compatibility regression tests.</li>
</ul>
</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="part-i-why-this-is-not-minor-news--real-engineering-risks">Part I: Why This Is Not “Minor News” — Real Engineering Risks<a href="https://www.eloqdata.com/blog/2025/12/10/minio-maintenance#part-i-why-this-is-not-minor-news--real-engineering-risks" class="hash-link" aria-label="Direct link to Part I: Why This Is Not “Minor News” — Real Engineering Risks" title="Direct link to Part I: Why This Is Not “Minor News” — Real Engineering Risks" translate="no">​</a></h2>
<p>Many teams treat object storage as <strong>the foundation of their infrastructure</strong>: cold data, backups, logs, media, and user uploads all live there.</p>
<p>Unlike databases or message queues, the risk of an <strong>unmaintained object storage layer</strong> is not sudden downtime, but:</p>
<ul>
<li class="">Extended patch windows</li>
<li class="">Growing compliance risk</li>
<li class="">Rising operational burden</li>
</ul>
<p>Once your S3 endpoint is public-facing and used with <strong>cross-region replication, IAM, and access control</strong>, even subtle incompatibilities in headers or signatures can become incident triggers.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="part-ii-viable-alternatives-in-20242025">Part II: Viable Alternatives in 2024–2025<a href="https://www.eloqdata.com/blog/2025/12/10/minio-maintenance#part-ii-viable-alternatives-in-20242025" class="hash-link" aria-label="Direct link to Part II: Viable Alternatives in 2024–2025" title="Direct link to Part II: Viable Alternatives in 2024–2025" translate="no">​</a></h2>
<p>Selection criteria:</p>
<ul>
<li class=""><strong>S3-compatible</strong></li>
<li class="">Active releases or commercial support</li>
<li class="">Ability to provide patches and vendor/community support</li>
</ul>
<p>Below is a quick engineering-focused comparison:</p>
<div style="overflow-x:auto"><table><thead><tr><th>Object Storage</th><th>Introduction</th><th>Engineering Appraisal</th><th>Repo</th></tr></thead><tbody><tr><td><strong>Ceph (RGW)</strong></td><td>Distributed object, block, and file storage platform</td><td>Enterprise-grade, native S3 compatibility. Complex to deploy but the most stable with strong vendor backing. Best for high-capacity, high-reliability teams.</td><td><a href="https://github.com/ceph/ceph" target="_blank" rel="noopener noreferrer" class="">https://github.com/ceph/ceph</a></td></tr><tr><td><strong>SeaweedFS</strong></td><td>High-performance distributed storage for billions of files</td><td>Lightweight, strong horizontal scalability, active S3 layer. Good for fast deployment and teams tolerant of minor compatibility gaps.</td><td><a href="https://github.com/seaweedfs/seaweedfs" target="_blank" rel="noopener noreferrer" class="">https://github.com/seaweedfs/seaweedfs</a></td></tr><tr><td><strong>RustFS</strong></td><td>High-performance, Rust-based S3-compatible object store</td><td>Claims 2.3× MinIO performance on 4KB objects. Provides Helm and Docker resources for Kubernetes. Young but promising.</td><td><a href="https://github.com/rustfs/rustfs" target="_blank" rel="noopener noreferrer" class="">https://github.com/rustfs/rustfs</a></td></tr><tr><td><strong>OpenIO</strong></td><td>Open-source object storage for large-scale unstructured data</td><td>S3-compatible, hardware-agnostic, strong UI, designed for big data and massive scale.</td><td><a href="https://github.com/open-io" target="_blank" rel="noopener noreferrer" class="">https://github.com/open-io</a></td></tr><tr><td><strong>Apache Ozone</strong></td><td>Scalable distributed object store for analytics and data lakes</td><td>Hadoop-native, optimized for analytics and container platforms, supports billions of objects.</td><td><a href="https://github.com/apache/ozone" target="_blank" rel="noopener noreferrer" class="">https://github.com/apache/ozone</a></td></tr><tr><td><strong>Garage (Deuxfleurs)</strong></td><td>Lightweight, distributed, S3-compatible object store</td><td>Designed for smaller self-hosted setups, geo-replication friendly. Suitable for edge and private clusters. Some advanced S3 features may be missing.</td><td><a href="https://github.com/deuxfleurs-org/garage" target="_blank" rel="noopener noreferrer" class="">https://github.com/deuxfleurs-org/garage</a></td></tr></tbody></table></div>
<p><strong>Quick Engineering Rule of `Thumb</strong></p>
<ul>
<li class="">Need full S3 features (versioning, lifecycle, ACLs, multipart)? → <strong>Ceph</strong> or commercial vendors first.</li>
<li class="">Only need basic read/write? → <strong>SeaweedFS</strong> or <strong>RustFS</strong> are fast to PoC.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="part-iii-a-48-hour-poc-using-seaweedfs-copy-paste-friendly">Part III: A 48-Hour PoC Using SeaweedFS (Copy-Paste Friendly)<a href="https://www.eloqdata.com/blog/2025/12/10/minio-maintenance#part-iii-a-48-hour-poc-using-seaweedfs-copy-paste-friendly" class="hash-link" aria-label="Direct link to Part III: A 48-Hour PoC Using SeaweedFS (Copy-Paste Friendly)" title="Direct link to Part III: A 48-Hour PoC Using SeaweedFS (Copy-Paste Friendly)" translate="no">​</a></h2>
<p><strong>Goal:</strong>
Within 48 hours, produce <strong>quantifiable results</strong> to support decisions:</p>
<ul>
<li class="">Compatibility gaps (API / headers / multipart / ACL)</li>
<li class="">Throughput &amp; latency (P50 / P95 / P99)</li>
<li class="">Operational complexity (steps, recovery time, scriptability)</li>
<li class="">Failure recovery behavior (node failure → consistency)</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="recommended-time-allocation">Recommended Time Allocation<a href="https://www.eloqdata.com/blog/2025/12/10/minio-maintenance#recommended-time-allocation" class="hash-link" aria-label="Direct link to Recommended Time Allocation" title="Direct link to Recommended Time Allocation" translate="no">​</a></h3>
<table><thead><tr><th>Phase</th><th>Time</th></tr></thead><tbody><tr><td>Preparation &amp; deployment</td><td>0–6 hours</td></tr><tr><td>Data migration &amp; concurrency benchmarks</td><td>6–18 hours</td></tr><tr><td>Compatibility testing</td><td>18–30 hours</td></tr><tr><td>Ops drills &amp; fault injection</td><td>30–42 hours</td></tr><tr><td>Summary &amp; decision matrix</td><td>42–48 hours</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="environment-requirements">Environment Requirements<a href="https://www.eloqdata.com/blog/2025/12/10/minio-maintenance#environment-requirements" class="hash-link" aria-label="Direct link to Environment Requirements" title="Direct link to Environment Requirements" translate="no">​</a></h3>
<ul>
<li class="">CPU: ≥4 vCPU per node (2 minimum)</li>
<li class="">Memory: 8–16 GB per node (Ceph: 16GB+ recommended)</li>
<li class="">Disk: 500GB–1TB per node (500GB enough for PoC)</li>
<li class="">Network: 1Gbps or better</li>
<li class="">OS: Ubuntu 20.04/22.04 or CentOS 8/9</li>
<li class="">Nodes: minimum 6</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="common-setup-script-run-on-all-nodes">Common Setup Script (Run on All Nodes)<a href="https://www.eloqdata.com/blog/2025/12/10/minio-maintenance#common-setup-script-run-on-all-nodes" class="hash-link" aria-label="Direct link to Common Setup Script (Run on All Nodes)" title="Direct link to Common Setup Script (Run on All Nodes)" translate="no">​</a></h3>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">#!/usr/bin/env bash</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">sudo apt update</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">sudo apt-get install s3cmd</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">set -euo pipefail</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">WORKDIR="${PWD}/s3_distributed_poc"</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">mkdir -p "$WORKDIR"{/seaweed,/scripts,/data}</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">for cmd in s3cmd ; do</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  if ! command -v $cmd &gt;/dev/null 2&gt;&amp;1; then</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    echo "Warning: $cmd not installed."</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  fi</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">done</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">cat &gt; "$WORKDIR/README.md" &lt;&lt;'EOF'</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">Directory layout:</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">- seaweed/: SeaweedFS deployment and benchmarks</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">- scripts/: General test scripts</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">EOF</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">echo "Init done"</span><br></span></code></pre></div></div>
<p>Run:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">chmod +x poc_setup_common.sh</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">./poc_setup_common.sh</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="seaweedfs-deployment-3-master--3-volume-example">SeaweedFS Deployment (3-Master + 3-Volume Example)<a href="https://www.eloqdata.com/blog/2025/12/10/minio-maintenance#seaweedfs-deployment-3-master--3-volume-example" class="hash-link" aria-label="Direct link to SeaweedFS Deployment (3-Master + 3-Volume Example)" title="Direct link to SeaweedFS Deployment (3-Master + 3-Volume Example)" translate="no">​</a></h3>
<p>Assumed nodes:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token literal-property property">node1</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">10.0</span><span class="token number">.0</span><span class="token number">.11</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token literal-property property">node2</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">10.0</span><span class="token number">.0</span><span class="token number">.12</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token literal-property property">node3</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">10.0</span><span class="token number">.0</span><span class="token number">.13</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token literal-property property">node4</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">10.0</span><span class="token number">.0</span><span class="token number">.14</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token literal-property property">node5</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">10.0</span><span class="token number">.0</span><span class="token number">.15</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token literal-property property">node6</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">10.0</span><span class="token number">.0</span><span class="token number">.16</span><br></span></code></pre></div></div>
<p>Create directories (run on all nodes):</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">mkdir -p /data/seaweedfs/{master,volume,filer,data}</span><br></span></code></pre></div></div>
<p>Start <strong>masters</strong> on node1/2/3:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">nohup ./weed master -port=9333 -mdir=/data/seaweed/master -defaultReplication=001 -ip=10.0.0.11 -peers=10.0.0.11:9333,10.0.0.12:9333,10.0.0.13:9333 &gt;&gt; master.log 2&gt;&amp;1 &amp;</span><br></span></code></pre></div></div>
<p>(Repeat for three nodes)</p>
<p>Start <strong>volume servers</strong> on node4/5/6:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">nohup ./weed volume -port=9334 -dir=/data/seaweed/volume -max=30 \</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -mserver=10.0.0.11:9333,10.0.0.12:9333,10.0.0.13:9333 \</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  -dataCenter=dc1 -rack=rack1 -publicUrl=10.0.0.14:9334 -ip=10.0.0.14 &gt;&gt; v1.log 2&gt;&amp;1 &amp;</span><br></span></code></pre></div></div>
<p>Verify:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">curl http://10.0.0.11:9333</span><br></span></code></pre></div></div>
<p>Run filer + S3 gateway:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">./weed filer -s3 -ip=10.0.0.1x -master=10.0.0.11:9333,10.0.0.12:9333,10.0.0.13:9333</span><br></span></code></pre></div></div>
<hr>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="benchmark-script">Benchmark Script<a href="https://www.eloqdata.com/blog/2025/12/10/minio-maintenance#benchmark-script" class="hash-link" aria-label="Direct link to Benchmark Script" title="Direct link to Benchmark Script" translate="no">​</a></h3>
<p>The benchmark script can be downloaded at <a href="https://www.eloqdata.com/assets/files/s3_concurrent_test-5bf8a9185d0108fad0b3a7ac176690f7.sh" target="_blank" class="">EloqData Objecrt Storage Benchmark</a></p>
<p>Benchmark with the following commands:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain"># generate local files</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">./s3_concurrent_test.sh prepare</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"># upload local files to object storage</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">./s3_concurrent_test.sh upload</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"># download files in object storage to local</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">./s3_concurrent_test.sh download</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"># test upload and download concurrently</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">./s3_concurrent_test.sh both</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"># cleanup object storage with prefix</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">./s3_concurrent_test.sh cleanup</span><br></span></code></pre></div></div>
<hr>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="key-metrics">Key Metrics<a href="https://www.eloqdata.com/blog/2025/12/10/minio-maintenance#key-metrics" class="hash-link" aria-label="Direct link to Key Metrics" title="Direct link to Key Metrics" translate="no">​</a></h3>
<table><thead><tr><th>Metric</th><th>Meaning</th><th>How to Measure</th></tr></thead><tbody><tr><td>Throughput</td><td>Data per second (MB/s or req/s)</td><td>Total bytes / total time</td></tr><tr><td>Latency</td><td>Single request response time</td><td>p50/p90/p99 via curl or scripts</td></tr><tr><td>Error Rate</td><td>Failed request ratio</td><td>Non-zero s3cmd or HTTP 4xx/5xx</td></tr><tr><td>Availability</td><td>Uptime</td><td>Health checks / ping</td></tr><tr><td>Concurrency</td><td>Performance under load</td><td>Increase concurrency until degradation</td></tr><tr><td>Data Integrity</td><td>Data correctness</td><td>md5/sha256 comparison</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="part-iv-from-eloqdatas-perpective">Part IV: From EloqData's Perpective<a href="https://www.eloqdata.com/blog/2025/12/10/minio-maintenance#part-iv-from-eloqdatas-perpective" class="hash-link" aria-label="Direct link to Part IV: From EloqData's Perpective" title="Direct link to Part IV: From EloqData's Perpective" translate="no">​</a></h2>
<p>EloqData provides two production-grade databases:</p>
<ul>
<li class=""><a href="https://github.com/eloqdata/eloqkv" target="_blank" rel="noopener noreferrer" class="">EloqKV</a>: A <strong>Redis-compatible transactional KV database</strong></li>
<li class=""><a href="https://github.com/eloqdata/eloqdoc" target="_blank" rel="noopener noreferrer" class="">EloqDoc</a>: A <strong>MongoDB-compatible document database</strong></li>
</ul>
<p>Both are designed to <strong>use S3-compatible object storage as primary storage</strong>, eliminating traditional disk overhead and unlocking elastic scalability. EloqData also offers a <strong>25GB Free Tier cloud service</strong> at <a href="https://cloud.eloqdata.com/" target="_blank" rel="noopener noreferrer" class="">EloqCloud</a>.</p>
<p>MinIO moving into maintenance mode does not mean the market is abandoning object storage — quite the opposite. Object storage is becoming the <strong>foundation of next-generation databases</strong>:</p>
<ul>
<li class="">From Snowflake/Databricks-style lakehouse systems</li>
<li class="">To Kafka / Pulsar streaming platforms</li>
</ul>
<p>The real challenge today is:
<strong>How to unlock object storage for ultra-low-latency, high-concurrency OLTP workloads.</strong></p>
<p>EloqData provides an answer:</p>
<ul>
<li class=""><strong>EloqKV</strong>: a fully Redis-compatible transactional KV database, achieving sub-3ms P9999 latency under million-QPS workloads using disk-only reads.</li>
<li class=""><strong>EloqDoc</strong>: a MongoDB-compatible document database using a decoupled compute/storage architecture for elasticity and dramatic cost reduction.</li>
</ul>
<p>Unlike traditional cloud-disk architectures, EloqKV and EloqDoc use <strong>object storage as primary storage and local SSD as an intelligent cache</strong>, reducing total storage cost to <strong>one-tenth of traditional solutions</strong>.</p>
<p>With native multi-replica object storage, EloqData enables a <strong>single-compute + multi-storage replica</strong> HA model, dramatically reducing CPU and memory overhead.</p>
<p>Object storage also enables <strong>second-level backup and branching</strong>, turning backup, environment cloning, and testing workflows from hours into seconds.</p>
<p>MinIO’s transition is not the end of object storage — it is the beginning of a new cloud-native infrastructure era.
With a proprietary <strong>quad-decoupled architecture</strong>, EloqData breaks the performance limits of object storage in OLTP scenarios and is helping define the next generation of cloud-native databases.</p>
<p><strong>EloqData — bringing object storage into the OLTP core battlefield.</strong></p>
<p>Register at <a href="https://cloud.eloqdata.com/" target="_blank" rel="noopener noreferrer" class="">https://cloud.eloqdata.com</a> and get <strong>25GB free storage and 10,000 QPS free DBaaS</strong>.</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Why Everyone is using PostgreSQL and DuckDB — And Why We May Need More]]></title>
        <id>https://www.eloqdata.com/blog/2025/11/11/distributed-db-relevant</id>
        <link href="https://www.eloqdata.com/blog/2025/11/11/distributed-db-relevant"/>
        <updated>2025-11-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Discussing why now distributed databases are losing popularity, and why they are going to be relevant in the AI age.]]></summary>
        <content type="html"><![CDATA[<p><a href="https://www.postgresql.org/" target="_blank" rel="noopener noreferrer" class="">PostgreSQL</a> and <a href="https://duckdb.org/" target="_blank" rel="noopener noreferrer" class="">DuckDB</a> have become the go-to databases for developers everywhere. Postgres is the default choice for transactional workloads, while DuckDB has quietly taken over the analytics world. Both are simple, fast, and easy to use. You can spin them up in seconds, run them anywhere, and they "just work." For most use cases, that's more than enough. But it's worth noting that both are single-node systems at heart. They can scale up, but when you hit the limits of one machine, you have to look elsewhere to migrate your infrastructure.</p>
<p>Many people now argue that single-node databases are enough for almost everything. Hardware has become so powerful that even massive workloads can often fit on one beefy machine. OpenAI recently <a href="https://www.youtube.com/watch?v=Ni1SGhNu-Q4" target="_blank" rel="noopener noreferrer" class="">discussed</a> how  their main database runs on a single-writer PostgreSQL setup. Just one node handling all the writes, with many read replicas to scale out read traffic. That's a bold design, and it suggests that maybe we no longer need complicated distributed databases because modern CPUs, SSDs, and memory are making scale-out architectures look like overkill.</p>
<p>In this artcle, we discuss how we reached this state in the database landscape, and disucss the future for scalable databases. We draw many inspirations from history, and we believe that there is a very bright future for database community going forward as we entering the new era of growth and prosperity.</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/agent_need_more-c30d74f092ebd2aa61c48436388f4bc3.jpg" alt="One Size Fit All?" style="cursor:pointer;max-width:100%"></div></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-rise-and-fall-of-the-scale-out-mindset">The Rise and Fall of the Scale-Out Mindset<a href="https://www.eloqdata.com/blog/2025/11/11/distributed-db-relevant#the-rise-and-fall-of-the-scale-out-mindset" class="hash-link" aria-label="Direct link to The Rise and Fall of the Scale-Out Mindset" title="Direct link to The Rise and Fall of the Scale-Out Mindset" translate="no">​</a></h2>
<p>Twenty years ago, distributed databases were the hot thing. As the internet exploded and apps started serving millions of users, the old single-node giants like Oracle and SQL Server began to crack under the load. That's when companies like Google and Amazon came up with <a href="https://static.googleusercontent.com/media/research.google.com/en/archive/bigtable-osdi06.pdf" target="_blank" rel="noopener noreferrer" class="">Bigtable</a> and <a href="https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf" target="_blank" rel="noopener noreferrer" class="">Dynamo</a> — systems designed to scale horizontally across hundreds or thousands of machines. Their success kicked off a wave of distributed data systems: <a href="https://cassandra.apache.org/" target="_blank" rel="noopener noreferrer" class="">Cassandra</a>, <a href="https://www.mongodb.com/" target="_blank" rel="noopener noreferrer" class="">MongoDB</a>, <a href="https://redis.io/" target="_blank" rel="noopener noreferrer" class="">Redis</a>, <a href="https://hbase.apache.org/" target="_blank" rel="noopener noreferrer" class="">HBase</a> and many others. These databases powered the modern internet and became the backbone of web-scale engineering.</p>
<p>The first wave of distributed databases came with trade-offs. To scale across machines, they gave up the ACID guarantees people took for granted in traditional databases. These <a href="https://en.wikipedia.org/wiki/NoSQL" target="_blank" rel="noopener noreferrer" class="">"NoSQL"</a> systems and early <a href="https://en.wikipedia.org/wiki/Data_lake" target="_blank" rel="noopener noreferrer" class="">data lakes</a> were fast, but they pushed the pain of handling inconsistency back to the application developers. About a decade later, around 2015, the industry figured out how to bring transactions back — both for online and analytical workloads. That's when the <a href="https://en.wikipedia.org/wiki/NewSQL" target="_blank" rel="noopener noreferrer" class="">"NewSQL"</a> and <a href="https://www.databricks.com/blog/2020/01/30/what-is-a-data-lakehouse.html" target="_blank" rel="noopener noreferrer" class="">"Lakehouse"</a> movements took off, with systems like <a href="https://static.googleusercontent.com/media/research.google.com/en//archive/spanner-osdi2012.pdf" target="_blank" rel="noopener noreferrer" class="">Google Spanner</a>, <a href="https://www.cockroachlabs.com/" target="_blank" rel="noopener noreferrer" class="">CockroachDB</a>, <a href="https://www.pingcap.com/" target="_blank" rel="noopener noreferrer" class="">TiDB</a>, <a href="https://www.snowflake.com/" target="_blank" rel="noopener noreferrer" class="">Snowflake</a>, and <a href="https://www.databricks.com/" target="_blank" rel="noopener noreferrer" class="">Databricks </a> started to appear around that time. For a while, it looked like distributed databases were going to rule the world. Snowflake went public at a staggering valuation, and companies like Databricks, Clickhouse, Cockroach Labs, and Yugabyte attracted huge funding rounds each valued in the billions.</p>
<p>However, just as distributed databases were hitting their stride, a new idea started to spread — maybe massive scale-out isn't necessary anymore. The first hint came from <a href="https://sqlite.org/" target="_blank" rel="noopener noreferrer" class="">SQLite</a>, the tiny embedded database that quietly became the most deployed software in the world. Then PostgreSQL surged ahead of MySQL and became the default choice for serious applications, from startups to enterprises. And now, DuckDB — a single-node analytical engine that runs right on your laptop — is reshaping how people do analytics. Suddenly, the towering systems built around Bigtable, Spanner, TiDB, Clickhouse and Snowflake don't look as untouchable as they once did. So what happened?</p>
<p>To understand this shift in mindset, it helps to look back at what changed. Before the internet era, databases like <a href="https://en.wikipedia.org/wiki/Sybase" target="_blank" rel="noopener noreferrer" class="">Sybase</a> and <a href="https://www.oracle.com/database/" target="_blank" rel="noopener noreferrer" class="">Oracle</a> were built for a single organization, usually serving tens of thousands of users. When the first wave of internet applications arrived in the mid-90s, those same systems were pressed into service to run websites like <a href="https://www.ebay.com/" target="_blank" rel="noopener noreferrer" class="">eBay</a> and <a href="https://en.wikipedia.org/wiki/AltaVista" target="_blank" rel="noopener noreferrer" class="">AltaVista</a>. It worked for a while, until it didn't. By the early 2000s, companies like Google discovered that traditional databases simply couldn't keep up with millions of online users, the new "netizens". That realization gave birth to Google's internal infrastructure, and with it, the entire era of Big Data and distributed databases.</p>
<p>Back then, a typical server had two sockets, one core per socket, running around 1 GHz, and maybe a gigabyte of RAM. Google's famous early <a href="https://static.googleusercontent.com/media/research.google.com/en//archive/gfs-sosp2003.pdf" target="_blank" rel="noopener noreferrer" class="">papers</a> described clusters of thousands of such machines — an impressive scale at the time. Fast forward to today: a single <a href="https://aws.amazon.com/blogs/aws/new-amazon-ec2-high-memory-u7inh-instance-on-hpe-server-for-large-in-memory-databases/" target="_blank" rel="noopener noreferrer" class="">high-end server</a> can pack terabytes of memory, hundreds of CPU cores running at several gigahertz with much improved instruction-per-cycle, and dozens of SSDs each delivering gigabytes per second of throughput. In other words, the capacity that once required an entire data center now fits in a single box. Meanwhile, the total number of users hasn't grown exponentially. Even with mobile devices and global internet access, we're still bounded by the size of the human population. For most large-scale applications, serving a few million users, a single modern database can easily handle the load.</p>
<p>Distributed OLTP companies such as CockroachDB, TiDB, and Yugabyte have all felt the impact of this shift. Founded between 2015 and 2017, they entered the market with high expectations for explosive growth. Those expectations never quite materialized. By around 2020, their products had become stable and feature-complete, and their valuations reached their peak. Yet, the demand was far smaller than investors had imagined. These systems now face competition from two sides: from powerful single-node databases that can handle most workloads, and from partially distributed but more efficient systems such as <a href="https://aws.amazon.com/rds/aurora/" target="_blank" rel="noopener noreferrer" class="">Amazon Aurora</a>, <a href="https://cloud.google.com/products/alloydb" target="_blank" rel="noopener noreferrer" class="">Google AlloyDB</a>, and starup offerings such as <a href="https://neon.com/" target="_blank" rel="noopener noreferrer" class="">NeonDB</a>.</p>
<p>The OLAP world has fared better. Analytical workloads depend not only on the number of users but also on how much data is collected, how long the history is kept, and how many users or systems consume the results. As companies collect more signals over longer periods, and as machine learning models begin to consume analytical outputs, the demand for performance and scalability remains high. Snowflake and Databricks had the advantage of starting a few years earlier than the OLTP vendors, which helped them secure a strong position in the market. Much of the growth in the analytics market today comes from a new kind of consumer: AI training and inference systems that rely on massive amounts of analytical data. Still, newer systems such as <a href="https://motherduck.com/" target="_blank" rel="noopener noreferrer" class="">MotherDuck</a>, which builds on DuckDB, have gained attention from both developers and investors.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-new-users-ai-powered-applications-and-autonomous-agents">The New Users: AI Powered Applications and Autonomous Agents<a href="https://www.eloqdata.com/blog/2025/11/11/distributed-db-relevant#the-new-users-ai-powered-applications-and-autonomous-agents" class="hash-link" aria-label="Direct link to The New Users: AI Powered Applications and Autonomous Agents" title="Direct link to The New Users: AI Powered Applications and Autonomous Agents" translate="no">​</a></h2>
<p>So what happens next? Hardware will keep getting faster, and there is no sign of that slowing down. Does that mean single-node databases like SQLite, PostgreSQL and DuckDB will take over completely, leaving little room for distributed systems? Probably not.</p>
<p>Due to the recent explosive AI innovation and the emergence of AI powered applications, the way we use software has changed. Every chat, prompt, or API call produces data. For instance, I use <a href="https://chatgpt.com/" target="_blank" rel="noopener noreferrer" class="">ChatGPT</a> almost every day and generate several conversations daily. Each one is a few hundred kilobytes, stored as chat history that I can scroll through anytime. After less than a year, I already have more than a thousand conversations. Compare that with my Amazon account, where I might have made less than a thousand purchases in more than a decade, each only a few kilobytes of data. I have easily produced a hundred times more data with OpenAI than with Amazon, in a fraction of the time.</p>
<p>Programming tools such as <a href="https://cursor.com/" target="_blank" rel="noopener noreferrer" class="">Cursor</a> go even further. They can generate tens of millions of tokens every day, and all that text needs to be stored for debugging, history tracking, or later analysis. This volume of data already puts serious pressure on backend systems.</p>
<p>More importantly, a new kind of user is emerging: <a href="https://manus.im/" target="_blank" rel="noopener noreferrer" class="">autonomous</a> <a href="https://www.genspark.ai/" target="_blank" rel="noopener noreferrer" class="">agents</a> <a href="https://www.19pine.ai/" target="_blank" rel="noopener noreferrer" class="">powered</a> <a href="https://www.lindy.ai/" target="_blank" rel="noopener noreferrer" class="">by</a> <a href="https://openai.com/index/introducing-agentkit/" target="_blank" rel="noopener noreferrer" class="">AI</a>. This shift is unlike anything we have seen before. It is creating enormous demand for both data volume and query intensity. In the age of AI, we have to rethink our data infrastructure, just as the internet once forced us to do.</p>
<p>Soon, autonomous agents will become the dominant users of IT systems. Take something as simple as buying a T-shirt. A human might browse a few pages and make one request every half minute. An AI agent, given the same goal, could issue thousands of requests in seconds to find the perfect item. This kind of machine-driven activity will easily overwhelm infrastructure that was built for human speed and scale. Just like the netizens used to overwhelm infrastructures built for employees in a single company at the begining of the internet age.</p>
<p>And this is only the beginning. Other kinds of AI applications and physical robots will arrive soon. We are standing at the edge of a new wave of innovation. If you think back to 1999, it would have been almost impossible to imagine people posting daily videos on TikTok or calling a ride with a phone through Uber. The same thing will happen again. Many new AI-powered scenarios have not even been imagined yet. Any company that wants to stay ahead, whether a startup or a long-established enterprise, needs to prepare for the pressure that these new workloads will put on their data infrastructure.</p>
<p>The pain of non-scalable systems is not new. Engineers have <a href="https://www.youtube.com/watch?v=4jDQi9P9UIw" target="_blank" rel="noopener noreferrer" class="">shared</a> many <a href="https://www.youtube.com/watch?v=P3mV3atVL2Q" target="_blank" rel="noopener noreferrer" class="">war stories</a> about hitting a scaling wall and being forced to migrate their databases under fire. We believe it is time to think ahead. It is better to prepare for what AI applications will need, rather than wait until the pressure becomes unbearable. The pace of change may be much faster than people expect.</p>
<p>During the early internet era, Google gained a huge advantage from its internal data infrastructure. It gave their teams the freedom to run experiments, ship features, and index the web without worrying about system limits. I saw this gap first-hand while working on the <a href="https://www.microsoft.com/en-us/research/video/lintao-zhang-showcases-tiger-bings-next-generation-index-serving-platform/" target="_blank" rel="noopener noreferrer" class="">early version of Bing</a>. Competing with Google was extremely difficult because they could move faster, process more data, and try new ideas at a pace that others simply could not match. Their real-time indexing pipeline was built on top of <a href="https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Peng.pdf" target="_blank" rel="noopener noreferrer" class="">Percolator</a>, the large-scale distributed transactional database, which itself sat on top of Bigtable, the large-scale distributed NewSQL database. Reproducing that stack was almost impossible for anyone else at the time.</p>
<p>This kind of competitive advantage will matter even more as we enter the hyper-speed growth of the AI era. Companies that build strong data infrastructure early will have the freedom to innovate without being held back by system limits, while everyone else will be stuck fighting their own scalability problems. As mentioned before, OpenAI currently relies on PostgreSQL for its main database, and for now it works. But even at this stage, engineers have already <a href="https://www.pixelstech.net/article/1747708863-openai%3a-scaling-postgresql-to-the-next-level" target="_blank" rel="noopener noreferrer" class="">talked</a> about banning new tables creations in the main database and avoiding any write-heavy workload. They also may need hierachical replications as the replication workload of the many read-replicas are overwhelming the write replica. OpenAI is only two years into its hyper-growth phase. These kinds of infrastructure limits can quickly become a major liability as the company scales.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="looking-forward">Looking Forward<a href="https://www.eloqdata.com/blog/2025/11/11/distributed-db-relevant#looking-forward" class="hash-link" aria-label="Direct link to Looking Forward" title="Direct link to Looking Forward" translate="no">​</a></h2>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/shovel_excavator-5144b60fbccd3a811a807c5d8636f4f2.png" alt="One Size Fit All?" style="cursor:pointer;max-width:100%"></div></div></div>
<p>Going forward, there are certainly spaces for simple, light databases such as SQLite and DuckDB. PostgreSQL is certainly still going to be a major force in the database ecosystem and will continue to power many important applications, just as Orcale DB continues to be <a href="https://db-engines.com/en/ranking_trend" target="_blank" rel="noopener noreferrer" class="">super relevant</a> in the internet age. However, we must not ignore the challenges for scalability and performance as we entering the AI era. One cannot dismiss the need for <a href="https://www.cat.com/en_US/products/new/equipment/excavators.html" target="_blank" rel="noopener noreferrer" class="">Caterpillar Excavactors</a> just because a shovel is sufficient for my backyard work 99.9% of the time. This is especially true when we are on the cusp of taking on the endeavor of transforming the entire world.</p>
<p>So where does this leave us? If AI agents become the main users of software, the demands on storage and compute will rise far beyond what a single machine can handle. We will need systems that combine the simplicity and performance of single-node databases with the elasticity and fault tolerance of distributed ones. We will also need databases that speak multiple "dialects" at once, since AI applications mix transactional work, analytical work, vector search, caching, and document storage in the same request path. The future will not be served by a pile of disconnected systems glued together with application logic. It will be served by a unified, scalable, multi-model engine that treats data as a first-class substrate.</p>
<p>This is the direction we are building toward at EloqData. Our <a class="" href="https://www.eloqdata.com/blog/2025/07/14/technology">Data Substrate architecture</a> is designed for the AI age. It brings the speed of in-memory systems, the reliability of transactional databases, and the flexibility of multi-model APIs together in one platform. Our database offerings build on top of this technology, i.e. the Redis API compatible KV store <a href="https://www.eloqdata.com/product/eloqkv" target="_blank" rel="noopener noreferrer" class="">EloqKV</a>, the MongoDB API compatible document DB <a href="https://www.eloqdata.com/product/eloqdoc" target="_blank" rel="noopener noreferrer" class="">EloqDoc</a> and the MySQL API compatible relational database <a href="https://www.eloqdata.com/product/eloqsql" target="_blank" rel="noopener noreferrer" class="">EloqSQL</a> all inherit the architectural advantages of Data Substrate to provide scalable, high-performance, low cost, and transactional databases with AI native features such as low cost snapshot and branching. They all can scales out when you need, scales in when you don't, and keeps storage costs low by using object storage as the primary durable layer. They allow developers build AI-driven applications without worrying about whether the next spike in workload will tip the system over.</p>
<p>We believe the next decade of software will not be defined by yet another wave of siloed databases. It will be defined by systems that make data infrastructure disappear into the background, allowing AI systems to read, write, learn, and act at machine speed. Our goal is to help build that foundation. Join the discussion on our <a href="https://discord.gg/nmYjBkfak6" target="_blank" rel="noopener noreferrer" class="">Discord Channel</a>, leverage our free tier cloud service to <a href="https://cloud.eloqdata.com/signup" target="_blank" rel="noopener noreferrer" class="">try out</a>, or visit our <a href="https://github.com/eloqdata/" target="_blank" rel="noopener noreferrer" class="">GitHub repo</a> to contribute. We would be happy to hear your thoughts on the future of databases.</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Don't Split My Data: I Will Use a Database (Not PostgreSQL) for My Data Needs]]></title>
        <id>https://www.eloqdata.com/blog/2025/11/07/use-real-database-for-data-needs</id>
        <link href="https://www.eloqdata.com/blog/2025/11/07/use-real-database-for-data-needs"/>
        <updated>2025-11-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Discussing the converged database substrate and why postgresql is not sufficient]]></summary>
        <content type="html"><![CDATA[<p>The internet (or at least the IT community) had a field day when a couple of blog posts claimed you could replace Redis and Kafka with PostgreSQL. <a href="https://dizzy.zone/2025/09/24/Redis-is-fast-Ill-cache-in-Postgres/" target="_blank" rel="noopener noreferrer" class="">"Redis is fast, I'll cache in Postgres"</a> and <a href="https://topicpartition.io/blog/postgres-pubsub-queue-benchmarks" target="_blank" rel="noopener noreferrer" class="">"Kafka is fast -- I'll use Postgres"</a> have gotten much attention on HackerNews <a href="https://news.ycombinator.com/item?id=45380699" target="_blank" rel="noopener noreferrer" class="">here</a> and <a href="https://news.ycombinator.com/item?id=45747018" target="_blank" rel="noopener noreferrer" class="">here</a>, and on Reddit <a href="https://www.reddit.com/r/programming/comments/1nph2jh/redis_is_fast_ill_cache_in_postgres/" target="_blank" rel="noopener noreferrer" class="">here</a> and <a href="https://www.reddit.com/r/programming/comments/1oj7q6q/kafka_is_fast_ill_use_postgres/" target="_blank" rel="noopener noreferrer" class="">here</a>. Obviously, some of the claims in the posts got roasted on HN and Reddit for suggesting you could replace Redis or Kafka with PostgreSQL. Many people (correctly) pointed out that the benchmarks were far from properly set up, and the workloads were non-typical. Some of the Kafka people also posted <a href="https://www.morling.dev/blog/you-dont-need-kafka-just-use-postgres-considered-harmful/" target="_blank" rel="noopener noreferrer" class="">long articles</a> to clarify what Kafka is designed for and why it is not hard to use. But, on the flip side, many of the posts also (correctly) preached a valid point: keeping fewer moving parts matters, and using the right tool for the job matters even more.</p>
<div align="center"><div style="width:75%;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/pg_for_all-894f6f4f4331a2a61d0f350102244abf.jpg" alt="x" style="cursor:pointer;max-width:100%"></div></div></div>
<p>Those "Postgres can replace Redis/Kafka" posts usually benchmark light workloads where serious tooling simply isn't necessary, and then stretch that into a sweeping narrative that "you don't need Redis or Kafka at all". But whether a workload is small or not depends entirely on the application. For a local grocery store website, twenty concurrent checkouts might qualify as high traffic; for Instagram or Twitter, a million queries per second barely registers. We're not here to debate whether people may ever need ten thousand queries per second. Generalizing one's workload to everyone's infrastructure needs is non-productive. It's like writing an article about how much grocery a new convertable can carry and conclude that U-Haul is no longer needed. No one would post that on a trucking forum expecting a heated debate. So why does the same kind of reasoning explode into controversy every time someone applies it to databases?</p>
<p>Sure, part of the drama is emotional. Developers get attached to their tools. Kafka, Redis, and Postgres all have cult-like followings, and any praise or criticism feels personal. But there's more to it than fandom. These debates touch on real technical trade-offs: complexity, durability, scaling, and the cost of managing multiple systems. In this article, we'll look at why it actually makes sense to use a <em>real</em> database for workloads that used to require specialized systems, and then tackle the elephant in the room: why this shift hasn't happened before and what's needed to make it possible.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-advantage-of-combining-workloads-on-a-real-database">The Advantage of Combining Workloads on a Real Database<a href="https://www.eloqdata.com/blog/2025/11/07/use-real-database-for-data-needs#the-advantage-of-combining-workloads-on-a-real-database" class="hash-link" aria-label="Direct link to The Advantage of Combining Workloads on a Real Database" title="Direct link to The Advantage of Combining Workloads on a Real Database" translate="no">​</a></h2>
<p>Back in the pre-internet days, "database" basically meant Oracle or Sybase: big relational systems that stored all of a company's data and powered every enterprise app. Then came the internet era, and data exploded in both volume and variety. The once-unified database stack shattered into a zoo of specialized systems: PostgreSQL and MySQL for transactional truth, Kafka for streams, Redis for caching, and Spark, Snowflake, or ClickHouse for analytics. We also have specialized tools for <a href="https://neo4j.com/" target="_blank" rel="noopener noreferrer" class="">graphs</a>, for <a href="https://www.pinecone.io/" target="_blank" rel="noopener noreferrer" class="">vectors</a>, for <a href="https://www.influxdata.com/" target="_blank" rel="noopener noreferrer" class="">time-series</a>, for <a href="https://www.tiledb.com/" target="_blank" rel="noopener noreferrer" class="">tensors</a>, and even just for <a href="https://tigerbeetle.com/" target="_blank" rel="noopener noreferrer" class="">account balances</a>. Each tool solved a particular problem, but together, they created a monster of complexity. IT teams now spend countless hours wiring, tuning, and babysitting these silos just to keep data moving.</p>
<p>Lately, there's been a growing movement to consolidate around a single database (often PostgreSQL) as the backbone of data infrastructure. The obvious motivation is simplicity and cost: running one system is easier (and cheaper) than orchestrating a zoo of databases stitched together with duct tape and ETL jobs. But beyond operational convenience, there are deeper reasons this trend deserves attention. A converged data architecture unlocks advantages that go far beyond saving money and any systems architect can't afford to ignore.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-avoid-cascading-failures">1. Avoid Cascading Failures<a href="https://www.eloqdata.com/blog/2025/11/07/use-real-database-for-data-needs#1-avoid-cascading-failures" class="hash-link" aria-label="Direct link to 1. Avoid Cascading Failures" title="Direct link to 1. Avoid Cascading Failures" translate="no">​</a></h3>
<p>Every engineer who's operated a large-scale system knows this pain: one small failure snowballs into a full-blown outage. This phenomenon is called <a href="https://sre.google/sre-book/addressing-cascading-failures/" target="_blank" rel="noopener noreferrer" class="">cascading failure</a>. When you glue together multiple systems, say Kafka feeding MySQL with Redis caching on top, each layer becomes a potential failure point. A hiccup in Kafka can stall writes, which can then cause cache invalidations to fail, which can then cause a thundering herd of retries.</p>
<blockquote>
<p><strong>Example: Twitter's Cache Incident (via <a href="https://danluu.com/cache-incidents/" target="_blank" rel="noopener noreferrer" class="">Dan Luu</a>)</strong>
In 2013, Twitter experienced an outage where a minor latency issue in the caching layer caused by interrupt-affinity misconfiguration led to a GC spiral on the tweet service. As cache latency grew, more requests bypassed the cache, overwhelming downstream services and eventually collapsing success rates to near 0% in one datacenter.</p>
<p>The database itself wasn't the root cause, but the incident shows how fragile multi-tiered data stacks can be: when layers depend on each other for throughput, even small delays can cascade into total failure.</p>
</blockquote>
<p>This is exactly the kind of chain reaction that converged database architectures can avoid by keeping durability, caching, and data access under one coordinated system instead of scattered layers of dependency. In a converged architecture, where all data flows through a single database, node failures degrade capacity proportionally rather than triggering a domino effect. The system scales down gracefully instead of collapsing catastrophically.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-avoid-development-complexity">2. Avoid Development Complexity<a href="https://www.eloqdata.com/blog/2025/11/07/use-real-database-for-data-needs#2-avoid-development-complexity" class="hash-link" aria-label="Direct link to 2. Avoid Development Complexity" title="Direct link to 2. Avoid Development Complexity" translate="no">​</a></h3>
<p>Maintaining two code paths for the same data is a tax on every engineering team. A typical "read-through" cache setup means developers have to write one logic path for MySQL (cache miss) and another for Redis (cache hit). Over time, these two paths drift apart: schema changes, data formats, and business rules slowly go out of sync. Debugging that kind of inconsistency is pure misery. A single database eliminates that duality. Your application talks to one API, with one transaction model, and one source of truth. Simpler code, fewer bugs, faster iteration.</p>
<p>Here is an example code for reading user information. If Redis cache is involved, the code looks like this:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">get_user</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">user_id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)"># Try cache first</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">try</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        user </span><span class="token operator">=</span><span class="token plain"> redis</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">get</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)">f"user:</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string-interpolation interpolation">user_id</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)">"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> user</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> json</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">loads</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">user</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">except</span><span class="token plain"> RedisTimeout</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        logger</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">warning</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"Redis timeout, falling back to DB"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">except</span><span class="token plain"> RedisConnectionError</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        logger</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">error</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"Redis down, falling back to DB"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)"># Cache miss or Redis failed, try database</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">try</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">with</span><span class="token plain"> postgres</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">cursor</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token plain"> cur</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            cur</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">execute</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"SELECT * FROM users WHERE id = %s"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">user_id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            user </span><span class="token operator">=</span><span class="token plain"> cur</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">fetchone</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token comment" style="color:rgb(98, 114, 164)"># Try to update cache (but don't fail if Redis is down)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">try</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">                redis</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token builtin" style="color:rgb(189, 147, 249)">set</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)">f"user:</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string-interpolation interpolation">user_id</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)">"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> json</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">dumps</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">user</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> ex</span><span class="token operator">=</span><span class="token number">3600</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">except</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">                </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">pass</span><span class="token plain">  </span><span class="token comment" style="color:rgb(98, 114, 164)"># "It's fine, we'll cache it next time"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> user</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">except</span><span class="token plain"> PostgresConnectionError</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token comment" style="color:rgb(98, 114, 164)"># Both Redis and Postgres down? Try the read replica</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">with</span><span class="token plain"> postgres_replica</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">cursor</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token plain"> cur</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token comment" style="color:rgb(98, 114, 164)"># ... more error handling ...</span><br></span></code></pre></div></div>
<p><strong>47 lines of code to read one user.</strong> And this doesn't even handle cache invalidation, which was another 200 lines of scattered <code>redis.delete()</code> calls that everyone was afraid to touch.</p>
<p>With everything in one database, the code becomes trivial:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">get_user</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">user_id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">with</span><span class="token plain"> db</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">cursor</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token plain"> cur</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        cur</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">execute</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"SELECT * FROM users WHERE id = %s"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">user_id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> cur</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">fetchone</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">update_user</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">user_id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> data</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">with</span><span class="token plain"> db</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">cursor</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token plain"> cur</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        cur</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">execute</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"UPDATE users SET ... WHERE id = %s"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> user_id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-avoid-consistency-and-durability-confusion">3. Avoid Consistency and Durability Confusion<a href="https://www.eloqdata.com/blog/2025/11/07/use-real-database-for-data-needs#3-avoid-consistency-and-durability-confusion" class="hash-link" aria-label="Direct link to 3. Avoid Consistency and Durability Confusion" title="Direct link to 3. Avoid Consistency and Durability Confusion" translate="no">​</a></h3>
<p>Distributed systems are already hard to reason about. Add multiple data tiers like Redis, Kafka, and a transactional database and you multiply the number of durability and consistency scenarios. Each component has its own guarantees: Kafka offers a choice of <em>"at-least-once"</em>, <em>"at-most-once"</em>, <em>"exact-once"</em> delivery, and whether you need flush every writes to achieve durability is sometimes <a href="https://jack-vanlightly.com/blog/2023/4/24/why-apache-kafka-doesnt-need-fsync-to-be-safe" target="_blank" rel="noopener noreferrer" class="">debated</a>. Redis is <em>eventually consistent</em>, meaning data can be stale, and provide <em>replication, write-ahead-log and snapshot</em> as <a href="https://redis.io/technology/durable-redis/" target="_blank" rel="noopener noreferrer" class="">persistency mechanisms</a>. Your SQL database is usually transactionally consistent with full ACID guarantee, but only within its own boundary. Stitch them together, and you're left trying to reason about correctness in a house of cards.</p>
<blockquote>
<p><strong>Example: Stale Writes and Inconsistent Cache at Scale (via <a href="https://danluu.com/cache-incidents/" target="_blank" rel="noopener noreferrer" class="">Dan Luu</a>)</strong>
Dan Luu's review of real-world cache incidents includes multiple cases where caches went out of sync with the source of truth. In one Twitter incident, stale data persisted in the cache after the underlying value had changed, leading to users seeing outdated timelines. In another, duplicate cache entries caused some users to appear in multiple shards simultaneously: an impossible state from the database's perspective. The fixes required strict cache invalidation ordering and retry logic to prevent stale data from overwriting newer updates.</p>
<p>None of these were database bugs; they were <em>integration bugs</em>, symptoms of managing consistency across disconnected systems.</p>
</blockquote>
<p>A single, converged database architecture eliminates this entire class of problems. There's no cache to drift out of sync, no queue to replay twice, no data race between "source of truth" and "derived truth." Everything from reads to writes to streaming happens within one transaction boundary and one consistency model. You don't just simplify the system; you make correctness <em>provable</em> instead of probabilistic.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-about-scalability-and-performance">What About Scalability and Performance?<a href="https://www.eloqdata.com/blog/2025/11/07/use-real-database-for-data-needs#what-about-scalability-and-performance" class="hash-link" aria-label="Direct link to What About Scalability and Performance?" title="Direct link to What About Scalability and Performance?" translate="no">​</a></h2>
<p>If converging everything into a single database simplifies architecture so dramatically, the obvious question is: why hasn't everyone already done it? The short answer: performance and scalability.</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/one_size_fit_all-14457a7f60fe63c3f63518e991df8a1d.png" alt="One Size Fit All?" style="cursor:pointer;max-width:100%"></div></div></div>
<p>Specialized systems like Redis and Kafka were born because traditional databases couldn't keep up. Caches were faster, queues were more scalable, and analytics engines could crunch far more data than your average OLTP database. The trade-off was fragmentation and complexity, but at least things stayed fast. For decades, this was a necessary compromise. In early 2000 the godfather of database Mike Stonebreaker famously declared <a href="https://cs.brown.edu/~ugur/fits_all.pdf" target="_blank" rel="noopener noreferrer" class="">"One size does not Fit All"</a> in database systems. Even as late as 2018, the CTO of Amazon Werner Vogels decalared that <a href="https://www.allthingsdistributed.com/2018/06/purpose-built-databases-in-aws.html" target="_blank" rel="noopener noreferrer" class="">"A one-size-fits-all database doesn't fit anyone"</a>.</p>
<p>But is that still so? Clearly there is a demand for one size fit all database, as demonstrated by the blogs mentioned in the early part of this article. Indeed, only recently have database architectures evolved enough to make convergence a realistic alternative. In this section, we discuss the necessary capabilities of a database that must be satisfied to enable such convergence.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-scalable-transactions">1. Scalable Transactions<a href="https://www.eloqdata.com/blog/2025/11/07/use-real-database-for-data-needs#1-scalable-transactions" class="hash-link" aria-label="Direct link to 1. Scalable Transactions" title="Direct link to 1. Scalable Transactions" translate="no">​</a></h3>
<p>For years, <em>transactions</em> were the deal-breaker for scaling databases. Everyone wanted the simplicity of ACID semantics, but once workloads outgrew a single machine, something had to give. The conventional wisdom was: you can have efficiency, transaction, or scale, but not all of them. That's how we ended up with a zoo of specialized systems. NoSQL databases like MongoDB and Cassandra threw away multi-record atomicity in exchange for horizontal scale. Developers got used to compensating in application code: implementing retries, deduplication, or manual rollback logic. It worked, but it was painful and brittle. For relational systems, manual sharding became the necessary evil: once you hit the single-node ceiling, you split your data and your sanity along with it.</p>
<p>As workloads grew, people learned that sharding relational databases by hand was a nightmare. Every cross-shard join, every transaction that touched more than one key, became a distributed systems problem. You could scale reads, but writes were a different story. And once data started to overlap across shards, consistency slipped through your fingers.</p>
<p>That's why the new generation of distributed transactional databases, often grouped under the "NewSQL" label, was such a breakthrough. Systems like <strong>Google Spanner</strong>, <strong>TiDB</strong>, and <strong>CockroachDB</strong> showed that you could have global scale and serializable transactions, thanks to better consensus protocols, hybrid logical clocks, and deterministic commit algorithms. Even previously non-ACID systems have evolved: MongoDB added multi-document transactions in 4.0, and Aerospike introduced distributed ACID transactions feature in 8.0.0. The takeaway is that distributed transactions are already standard features in many systems.</p>
<p>This changes the calculus completely. When your database can handle distributed transactions efficiently, you no longer need to glue together different engines just to scale. You get scalability and correctness in one system that can become a foundation for building truly unified data architectures.</p>
<p>Yet, most existing distributed transactional databases are still less efficient than their single node counter part. This is an issue we need to address (see below).</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-independent-resource-scaling">2. Independent Resource Scaling<a href="https://www.eloqdata.com/blog/2025/11/07/use-real-database-for-data-needs#2-independent-resource-scaling" class="hash-link" aria-label="Direct link to 2. Independent Resource Scaling" title="Direct link to 2. Independent Resource Scaling" translate="no">​</a></h3>
<p>Different workloads stress different parts of a system. Streaming ingestion (like Kafka) is IO-bound and needs massive sequential write throughput. Caching workloads (like Redis) are memory-bound, thriving on low-latency access to hot data. Analytical queries and vector search, on the other hand, are CPU-bound, demanding large compute bursts. Traditional <em>shared-nothing</em> databases where compute, storage, and memory scale together force you to over-provision one resource just to satisfy another. You end up paying for RAM you don't use or SSDs that sit mostly idle.</p>
<p>That's one of the reasons why people historically broke their pipelines into multiple specialized systems. You could put Kafka on write-optimized disks, Redis on high-RAM nodes, and MySQL on balanced hardware.</p>
<p>Now, cloud infrastructure is changing that equation. Modern cloud platforms allow <strong>elastic and independent scaling</strong> of compute, storage, and memory. Databases are starting to embrace this model directly. The <strong>separation of compute and storage</strong>, first popularized by cloud data warehouses like Snowflake and BigQuery, is now making its way into OLTP systems as well. This decoupling lets a single database scale ingest, query, and cache layers independently.</p>
<p>A truly converged database architecture must exploit this flexibility: scale write nodes when ingestion spikes, scale compute when CPU demands surge, and expand storage as data volumn grows.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-performance-and-the-zero-overhead-principle">3. Performance and the Zero-Overhead Principle<a href="https://www.eloqdata.com/blog/2025/11/07/use-real-database-for-data-needs#3-performance-and-the-zero-overhead-principle" class="hash-link" aria-label="Direct link to 3. Performance and the Zero-Overhead Principle" title="Direct link to 3. Performance and the Zero-Overhead Principle" translate="no">​</a></h3>
<p>C++ developers have a mantra: the <strong>Zero-Overhead Principle</strong></p>
<ol>
<li class=""><em>You don't pay for what you don't use.</em></li>
<li class=""><em>What you do use is just as efficient as what you could reasonably write by hand.</em></li>
</ol>
<p>That's exactly the mindset a converged database must adopt. Existing databases rarely meet this bar. Even if you just want to run an in-memory cache, most engines still insist on running full durability machinery: write-ahead logging, background checkpoints, transaction journals, and page eviction logic. The result? You're paying CPU and latency overhead for guarantees you may not need on a given workload.</p>
<p>This is why Redis can outperform MySQL on pure in-memory reads even when both are running on the same hardware and the dataset is identical. The overhead isn't only in the query parser; it's in the architectural layers designed for persistence, recovery, and buffer management that never get out of the way.</p>
<p>A truly unified database must treat these mechanisms as <em>pluggable</em>, not mandatory. Durable writes, replication, and recovery logging should be modular: enabled only when needed, bypassed when not. The same engine should be able to act as a blazing-fast cache, a strongly consistent OLTP store, or a long-term analytical system, without paying unnecessary tax in the fast path.</p>
<p>In other words, <strong>unification can not come at the cost of overhead</strong>. It has to come from rethinking the core execution and storage engine so that the same system can be both flexible <em>and</em> ruthlessly efficient, depending on how it's used.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="postgresql-may-not-be-the-answer">PostgreSQL May Not Be the Answer<a href="https://www.eloqdata.com/blog/2025/11/07/use-real-database-for-data-needs#postgresql-may-not-be-the-answer" class="hash-link" aria-label="Direct link to PostgreSQL May Not Be the Answer" title="Direct link to PostgreSQL May Not Be the Answer" translate="no">​</a></h2>
<p>If "just use Postgres for everything" sounds too easy, that's because it is. PostgreSQL deserves its reputation: it's battle-tested, reliable, and absurdly extensible. But it was never designed to be the all-in-one database the modern world needs. Its architecture still reflects the assumptions of the 1990s: local disks, tightly coupled storage and compute, and a single-threaded execution model wrapped in process-based concurrency. The query pipeline, the storage system, the concurrency control mechanism are all designed with a single-node, row-based MVCC ACID relational database in mind.</p>
<p>You can retrofit Postgres with external layers: <a href="https://www.citusdata.com/" target="_blank" rel="noopener noreferrer" class="">Citus</a> for sharding, <a href="https://neon.com/" target="_blank" rel="noopener noreferrer" class="">Neon</a> for decoupled storage, <a href="https://www.tigerdata.com/" target="_blank" rel="noopener noreferrer" class="">TimeScale (now TigerData)</a> and <a href="https://github.com/pgvector/pgvector" target="_blank" rel="noopener noreferrer" class="">PGVector</a> for handling time-series and vector data. But this is often not optimal, and often just recreates the complexity we were trying to eliminate (i.e. they do not compose). When serious modification is needed, such as making it fully multi-writer capable as in <a href="https://www.cockroachlabs.com/" target="_blank" rel="noopener noreferrer" class="">CockroachDB</a> and <a href="https://www.yugabyte.com/" target="_blank" rel="noopener noreferrer" class="">YugabyteDB</a> or streaming capable as in <a href="https://materialize.com/" target="_blank" rel="noopener noreferrer" class="">Materialize</a> and <a href="https://risingwave.com/" target="_blank" rel="noopener noreferrer" class="">RisingWave</a>, a major rewrite is often needed and often only the wire-protocol can be reused. Extensions like logical replication, foreign data wrappers, and background workers are impressive engineering feats, but they're patches on a monolith, not blueprints for a unified data platform.</p>
<p>When people benchmark Postgres against Redis or Kafka, they're really comparing apples to power tools. Postgres can simulate a cache or a queue, but it isn't optimized for them. WAL writes still happen, visibility maps still update, vacuum still runs. The performance gaps show up not because Postgres is "slow," but because it's doing far more work than those specialized systems ever attempt. Its design trades raw throughput for correctness and compatibility, a perfectly valid choice, just not one that scales linearly to every use case.</p>
<p>Recently, there is another school of thought argues that as hardware keeps improving, scalability and efficiency becomes less relevant. If a single server can pack terabytes of RAM and hundreds of cores, why bother with distributed systems at all? Why not just keep everything in one big Postgres instance and call it a day? We fundamentally disagree with this line of thinking. Hardware growth helps, but data growth is exponential. User expectations for availability, latency, and global presence grow even faster. Scaling vertically might postpone the problem, it doesn't solve it. This is especially true in the AI age, when data is the lifeline of all applications. We'll dedicate another article to discuss this in detail.</p>
<p>PostgreSQL has earned its place as the default database of our time, but it's not the <em>endgame</em>. There is opportunity in databases that internalize the lessons of distributed systems, cloud elasticity, and modular design.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-road-ahead-toward-a-true-data-substrate">The Road Ahead: Toward a True Data Substrate<a href="https://www.eloqdata.com/blog/2025/11/07/use-real-database-for-data-needs#the-road-ahead-toward-a-true-data-substrate" class="hash-link" aria-label="Direct link to The Road Ahead: Toward a True Data Substrate" title="Direct link to The Road Ahead: Toward a True Data Substrate" translate="no">​</a></h2>
<p>The industry's obsession with patching and stitching different data systems together is a legacy of the past twenty years of hardware and software limits. But those limits are fading. The next generation of data infrastructure won't be defined by whether it speaks SQL or supports transactions. It will be defined by whether it can unify the data lifecycle without compromise: streaming, caching, analytics, and transactions under a single, coherent architecture.</p>
<p>That's exactly what we're building with <a class="" href="https://www.eloqdata.com/blog/2025/07/14/technology"><strong>EloqData's Data Substrate</strong></a>. Instead of bolting more features onto yesterday's database engines, we started from a clean slate: modular storage and compute layers, fully ACID-compliant distributed transactions, object storage as a first-class persistence medium, and elastic scaling across workloads. The same engine that serves as a durable operational database can act as a <a class="" href="https://www.eloqdata.com/blog/2024/08/17/benchmark-single-node">high-throughput cache</a>, a streaming log, or an analytical backend without duplicating data or wiring together half a dozen systems.</p>
<p>This is the promise of a <strong>converged data platform</strong>: simplicity without trade-offs, scalability without fragmentation, and performance without overhead. The future of data infrastructure belongs to systems that treat data as a continuous substrate rather than a pile of disconnected silos. That's where we're headed. Of course, we still have a very long way to go, but we are working hard on this goal. Join the discussion on our <a href="https://discord.gg/nmYjBkfak6" target="_blank" rel="noopener noreferrer" class="">Discord Channel</a>, or visit our <a href="https://github.com/eloqdata/" target="_blank" rel="noopener noreferrer" class="">GitHub repo</a> to contribute. We would be happy to hear your thoughts on the future of databases.</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[How NVMe and S3 Reshape Decoupling of Compute and Storage for Online Databases]]></title>
        <id>https://www.eloqdata.com/blog/2025/10/24/how-nvme-and-s3-reshape-decoupling</id>
        <link href="https://www.eloqdata.com/blog/2025/10/24/how-nvme-and-s3-reshape-decoupling"/>
        <updated>2025-10-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Re-examine the decoupled architecture through the lens of cloud storage mediums.]]></summary>
        <content type="html"><![CDATA[<p>Cloud native databases are designed from the ground up to embrace core cloud principles: distributed architecture, automatic scalability, high availability, and elasticity. A prominent example is Amazon Aurora, which established the prevailing paradigm for online databases by championing the decoupling of compute and storage. This architecture allows the compute layer (responsible for query and transaction processing) and the storage layer (handling data persistence) to scale independently. As a result, database users benefit from granular resource allocation, cost efficiency through pay-per-use pricing, flexibility in hardware choices, and improved resilience by isolating persistent data from ephemeral compute instances.</p>
<p>In this blog post, we re-examine this decoupled architecture through the lens of cloud storage mediums. We argue that this prevailing model is at a turning point, poised to be reshaped by the emerging synergy between instance-level, volatile NVMe and highly durable object storage.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="separate-storage-nodes">Separate Storage Nodes<a href="https://www.eloqdata.com/blog/2025/10/24/how-nvme-and-s3-reshape-decoupling#separate-storage-nodes" class="hash-link" aria-label="Direct link to Separate Storage Nodes" title="Direct link to Separate Storage Nodes" translate="no">​</a></h2>
<p>The Aurora's design decouples a database into a stateless compute node and stateful, distributed storage nodes. The compute node executes queries and transactions, while storage nodes manage the transaction logs (WAL) and data pages. Internally, storage nodes apply log records to data pages, perform log truncation, and serve read requests for data absent from the compute node's buffer pool.</p>
<div align="center"><div style="width:15%;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/decoupled-f892f44d07150304fde81644324565e0.png" alt="x" style="cursor:pointer;max-width:100%"></div></div></div>
<p>The influence of the Aurora’s decoupled architecture is unmistakable across the cloud database landscape. Successors like GCP AlloyDB and Azure SQL Database Hyperscale have all embraced similar architectures. The trend continues in open-source projects like NeonDB, which implements the decoupling architecture through its pageservers (data pages) and safekeepers (WALs).</p>
<p>The trend also extends to distributed databases like CockroachDB, TiDB, and YugabyteDB. Although they were originally architected as shared-nothing systems, it has become a common and recommended practice to deploy them on two sets of servers: one cluster for stateless compute and the other for stateful storage. This operational separation further refines the principle of independent scaling and management.</p>
<p>In essence, one could argue that compute-storage decoupling serves as a foundational pillar for cloud-native online databases, which rely on one or more separate storage nodes.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-landscape-of-cloud-storage-mediums">The Landscape of Cloud Storage Mediums<a href="https://www.eloqdata.com/blog/2025/10/24/how-nvme-and-s3-reshape-decoupling#the-landscape-of-cloud-storage-mediums" class="hash-link" aria-label="Direct link to The Landscape of Cloud Storage Mediums" title="Direct link to The Landscape of Cloud Storage Mediums" translate="no">​</a></h2>
<p>It is a common misconception to view cloud storage as merely an extension of on-premises solutions. In fact, cloud platforms offer a diverse spectrum of storage mediums, each with performance and cost characteristics that differ drastically from traditional models. The three most common tiers in the cloud are instance-local storage (e.g., NVMe), network-attached block storage (e.g., <a href="https://aws.amazon.com/ebs/" target="_blank" rel="noopener noreferrer" class="">Amazon EBS</a>), and object storage (e.g., <a href="https://aws.amazon.com/s3/" target="_blank" rel="noopener noreferrer" class="">Amazon S3</a>).</p>
<p>Block-level storage is a cloud service that emulates network-attached hard drives. As a resource provisioned and billed independently from VMs, it guarantees full data durability beyond the lifecycle of any attached VM. The combination of familiarity and resilience has made it the de facto storage medium for online databases.</p>
<p>Instance-level storage is temporary, high-performance block storage that resides directly on a VM's physical host. Physical proximity enables extremely low-latency access. Specifically, instance-level NVMe have pushed performance to unprecedented levels: up to <a href="https://semiconductor.samsung.com/consumer-storage/internal-ssd/9100-pro/" target="_blank" rel="noopener noreferrer" class="">2 million IOPS</a>. However, this raw speed comes with a critical trade-off: the storage is ephemeral, meaning all data is lost if the instance is stopped, terminated, or experiences a hardware failure.</p>
<p>Block-level and instance-level storage thus represent a fundamental trade-off for storing data in the cloud. Block storage guarantees durability at a steep premium, whereas instance storage delivers superior performance and lower cost but sacrifices all data persistence.</p>
<p>To understand the stark performance-price difference between instance-level NVMe and block-level storage, let's examine a concrete example from Amazon. Amazon EBS <a href="https://aws.amazon.com/ebs/volume-types/" target="_blank" rel="noopener noreferrer" class="">io2</a> is a premium block storage solution offering up to 256K IOPS. Provisioning an io2 volume with 150K IOPS and 1 TB capacity costs approximately $6,000 per month. By contrast, an Amazon storage-optimized instance <a href="https://aws.amazon.com/ec2/instance-types/i8g/" target="_blank" rel="noopener noreferrer" class="">i8g.xlarge</a> provides comparable performance with 150K IOPS and 937 GB of local NVMe storage. This instance features 4 vCPUs and 32 GB of memory, and yet costs only $247 per month. When we subtract the cost of compute resources, i.e., $170 for an equivalent <a href="https://aws.amazon.com/ec2/instance-types/r8g/" target="_blank" rel="noopener noreferrer" class="">r8g.xlarge</a> instance with identical CPU and memory but no local storage, the net cost of the high-performance NVMe storage becomes almost negligible compared to the premium EBS solution.</p>
<p>Apart from block and instance storage, object storage is a scalable storage service for organizing data as objects in buckets. It excels at handling unlimited data with high durability (e.g., 99.999999999%) at a very low cost, often cheaper than traditional on-premises storage. However, its access latency is unpredictable and can be as high as hundreds of milliseconds. This performance profile has historically precluded its use as the default storage for online databases.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="nvme--object-storage">NVMe + Object Storage<a href="https://www.eloqdata.com/blog/2025/10/24/how-nvme-and-s3-reshape-decoupling#nvme--object-storage" class="hash-link" aria-label="Direct link to NVMe + Object Storage" title="Direct link to NVMe + Object Storage" translate="no">​</a></h2>
<p>Given the landscape of cloud storage, a growing consensus recognizes that block-level storage is ill-suited for primary storage of online databases, being both prohibitively expensive and slow. Instead, a combination of instance-level NVMe and object storage is emerging as a superior architectural pattern.</p>
<ul>
<li class=""><b>Write path</b>: Data modifications are first committed to local NVMe storage and then uploaded to object storage.</li>
<li class=""><b>Read path</b>: Reads that result in in-memory cache misses are served directly from the local NVMe, ensuring low-latency access and high I/O throughput.</li>
<li class=""><b>Stateless compute</b>: VMs can be freely stopped or migrated. A new instance simply reconnects to object storage and incrementally downloads data files to its local NVMe.</li>
</ul>
<p>In this design, NVMe delivers exceptional performance at a fraction of the cost of block-level storage, while object storage provides the ultimate guarantee of data durability.</p>
<p>A legitimate concern with the combination is the potential for unpredictable latency. This could occur if an online query needs to download from object storage a data file not available on local NVMe, which has reached its capacity limit.</p>
<p>However, we contend that such scenarios should be rare for two reasons. First, the scale of local NVMe capacity available in the cloud is substantial. Consider Amazon's storage-optimized <a href="https://aws.amazon.com/ec2/instance-types/i8g/" target="_blank" rel="noopener noreferrer" class="">i8g</a> instances: a 64-vCPU VM provides 15 TB of local NVMe storage. Second, if a dataset grows beyond the capacity of a single node, the storage engine should be scaled out horizontally across multiple VMs. A conscious decision to operate with smaller local storage than the total dataset size should only be made when the application's access pattern exhibits strong locality (e.g., focusing on recent data) and can tolerate occasional high-latency access to colder, historical data archived in object storage.</p>
<p>A second common concern regarding object storage is high write latency. However, this is effectively mitigated by <a href="https://eloqdata.com/blog/2024/08/11/data-substrate" target="_blank" rel="noopener noreferrer" class="">a well-designed database architecture that leverages a transaction log</a>. The transaction log absorbs all writes on the critical path, ensuring low-latency commitments. The actual data writes to object storage are then deferred to asynchronous, background checkpointing processes.</p>
<p>Since checkpointing is a throughput-oriented, background task, it is inherently tolerant of higher latency. Therefore, the performance characteristics of object storage writes have a minimal impact on online queries.</p>
<p>With NVMe and object storage serving as the storage mediums for database storage engines, block-level storage finds a new role in managing the transaction log. The transaction log demands high write throughput, moderate IOPS (which is easily satisfied by the batched, append-only write pattern), and relatively low capacity due to continuous log truncation. Crucially, it must also guarantee 100% durability the moment a query commits, while maintaining low write latency. These specific requirements align well with the performance and durability profile of block-level storage, allowing databases to leverage it for the transaction log at a remarkably low cost. For instance, an Amazon EBS gp3 volume providing 3,000 IOPS, 500 MB/s of throughput, and 100 GB of capacity can be provisioned for approximately $20 per month.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="new-shapes-of-decoupling">New Shapes of Decoupling<a href="https://www.eloqdata.com/blog/2025/10/24/how-nvme-and-s3-reshape-decoupling#new-shapes-of-decoupling" class="hash-link" aria-label="Direct link to New Shapes of Decoupling" title="Direct link to New Shapes of Decoupling" translate="no">​</a></h2>
<p>The combination of instance-level NVMe and object storage will redefine the architecture of compute-storage decoupling. In this new paradigm, object storage serves as the decoupled storage layer, which offers unlimited capacity and permanent durability. Meanwhile, instance-level NVMe acts as a local storage cache, in addition to main memory, and becomes an integral part of the compute layer. Within this framework, block-level storage finds its optimal role as a dedicated, cost-effective medium for hosting the transaction log, which is attached directly to compute nodes where low-latency writes are critical.</p>
<div align="center"><div style="width:40%;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/new_shape_1-cdfe444793b7f40e8241d85bbd68210d.png" alt="x" style="cursor:pointer;max-width:100%"></div></div></div>
<p>Of course, the established decoupled architecture can be augmented by equipping storage nodes with local NVMe, preserving the original benefits of independent scaling while enhancing storage performance. However, we argue that the value of such an enhancement is diminished in many scenarios.</p>
<div align="center"><div style="width:40%;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/decoupled_2-7f7024a4ffaefed88c364051fa3e6007.png" alt="x" style="cursor:pointer;max-width:100%"></div></div></div>
<p>A key reason lies in the fixed resource ratios inherent to cloud instances. For example, on Amazon's storage-optimized <a href="https://aws.amazon.com/ec2/instance-types/i8g/" target="_blank" rel="noopener noreferrer" class="">i8g</a> instances, each vCPU is paired with 8 GB of memory and 468 GB of local NVMe. While these CPU and memory resources are provisioned to handle cache-miss read requests, they often remain underutilized due to two factors: the intermittent nature of cache-miss reads depending on workload patterns, and the fact that accessing local NVMe may require much less CPU and memory than allocated.</p>
<p>Furthermore, this architecture introduces inherent inefficiencies. Each cache-miss read necessitates a network round trip, incurring not only additional latency but substantial CPU overhead on both ends for data serialization and network processing. The two-sided CPU cost of network-based access becomes an increasingly significant resource drain, making remote storage access less appealing compared to local NVMe operations.</p>
<p>Nevertheless, the original decoupling architecture maintains its relevance in some scenarios, e.g., when dealing with a large-scale dataset that they must be distributed across multiple storage nodes.</p>
<p>When dealing with large-scale datasets, we believe a more promising approach lies in horizontally scaling both compute nodes and their attached NVMe storage. This architecture avoids the resource overhead of the traditional decoupled model by transforming the entire system into a multi-writer distributed database, where compute, memory and storage resources are fully utilized for processing, not just for serving remote requests.</p>
<div align="center"><div style="width:90%;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/new_shape_2-f20c9d923661483e4581ec939f98f2f8.png" alt="x" style="cursor:pointer;max-width:100%"></div></div></div>
<p>Viewing object storage as the decoupled persistence layer enables new variants of database architecture. This is evident in high availability designs: object storage natively provides cross-AZ or even cross-region reliability. By complementing this with replication of the transaction log across multiple zones, the entire database inherently achieves geo-distributed reliability. This approach eliminates the need for complex, resource-intensive storage nodes spanning multiple zones, replacing them with lightweight transaction log replicas that consume significantly fewer resources.</p>
<div align="center"><div style="width:95%;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/new_shape_3-9ba4d364025a2a4a488c8c7da321ed24.png" alt="x" style="cursor:pointer;max-width:100%"></div></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://www.eloqdata.com/blog/2025/10/24/how-nvme-and-s3-reshape-decoupling#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>In this post, we re-examined the decoupled architecture of online databases through the lens of emerging cloud storage technologies, particularly the powerful combination of instance-level NVMe and object storage. We believe that this new storage paradigm is poised to become the new storage backbone for future cloud databases, ultimately reshaping how Database-as-a-Service (DBaaS) platforms are designed and deployed in the cloud.</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Lessons from the AWS us-east-1 Outage: Why Local NVMe as Primary DB Storage Is Risky]]></title>
        <id>https://www.eloqdata.com/blog/2025/10/20/local-nvme-risky-for-database</id>
        <link href="https://www.eloqdata.com/blog/2025/10/20/local-nvme-risky-for-database"/>
        <updated>2025-10-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Many databases recommend local NVMe for performance with cross-AZ replicas for durability. The AWS outage showed how this can still lead to data loss. Here’s a breakdown and a safer alternative.]]></summary>
        <content type="html"><![CDATA[<p>On October 20, 2025, AWS experienced a major disruption across multiple services in the <strong>us-east-1 region</strong>. According to <a href="https://health.aws.amazon.com/health/status" target="_blank" rel="noopener noreferrer" class="">AWS Health Status</a>, various compute, storage, and networking services were impacted simultaneously. For many teams running OLTP databases on <strong>instances backed by local NVMe</strong>, this was not just a downtime problem-it was a <strong>data durability nightmare</strong>.</p>
<div align="center"><div style="width:75%;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/awsoutage-4ab20f3c29068e156d4d3f903e3cdcf2.jpg" alt="x" style="cursor:pointer;max-width:100%"></div></div></div>
<p>Cloud databases must constantly balance <strong>durability, performance, and cost</strong>. In modern cloud environments, there are three main types of storage available:</p>
<table><thead><tr><th>Storage Type</th><th>Durability</th><th>Latency</th><th>Cost</th><th>Persistence Across VM Crash</th></tr></thead><tbody><tr><td><strong>Block Storage (EBS)</strong></td><td>High</td><td>Medium</td><td>High</td><td>Data persists</td></tr><tr><td><strong>Local NVMe</strong></td><td>None</td><td>Ultra-fast</td><td>Low per IOPS</td><td>Lost on restart/crash</td></tr><tr><td><strong>Object Storage (S3)</strong></td><td>Very High</td><td>Slow</td><td>Lowest</td><td>Persistent</td></tr></tbody></table>
<p>Let’s break down the trade-offs and why recent events place a spotlight on risky architectural choices.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="option-1-block-level-storage-ebs---durable-but-expensive-and-slow">Option 1: Block-Level Storage (EBS) - Durable but Expensive and Slow<a href="https://www.eloqdata.com/blog/2025/10/20/local-nvme-risky-for-database#option-1-block-level-storage-ebs---durable-but-expensive-and-slow" class="hash-link" aria-label="Direct link to Option 1: Block-Level Storage (EBS) - Durable but Expensive and Slow" title="Direct link to Option 1: Block-Level Storage (EBS) - Durable but Expensive and Slow" translate="no">​</a></h2>
<p>EBS is the default choice for reliability:</p>
<ul>
<li class="">It survives instance failures.</li>
<li class="">It supports cross-AZ replication via multi-replica setups.</li>
<li class="">It enables quick reattachment to replacement nodes.</li>
</ul>
<p><strong>But the downside?</strong></p>
<ul>
<li class="">GP2/GP3 disks deliver modest IOPS and high latency.</li>
<li class="">High-performance variants like IO2 are <em>extremely expensive</em> when provisioned for hundreds of thousands of IOPS.</li>
<li class="">Scaling performance often means scaling cost linearly.</li>
</ul>
<p>EBS gives you durability-but performance per dollar is disappointing.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="option-2-local-nvme---fast-but-ephemeral-and-now-proven-risky">Option 2: Local NVMe - Fast but Ephemeral (and Now Proven Risky)<a href="https://www.eloqdata.com/blog/2025/10/20/local-nvme-risky-for-database#option-2-local-nvme---fast-but-ephemeral-and-now-proven-risky" class="hash-link" aria-label="Direct link to Option 2: Local NVMe - Fast but Ephemeral (and Now Proven Risky)" title="Direct link to Option 2: Local NVMe - Fast but Ephemeral (and Now Proven Risky)" translate="no">​</a></h2>
<p>Instance families like <strong>i4i</strong> provide <strong>400K+ to 1M+ IOPS</strong> from local NVMe, making them a natural fit for databases chasing performance.</p>
<p>So many database vendors recommend:</p>
<ul>
<li class="">Use local NVMe for primary storage</li>
<li class="">Add cross-AZ replicas for durability</li>
</ul>
<p><strong>But here’s the problem:</strong> Local NVMe is <em>tied to the node lifecycle</em>.
If the node restarts, fails, gets terminated due to spot interruption, or is impacted by a region-level failure such as the recent us-east-1 outage-you lose <strong>ALL</strong> the data.</p>
<p>During routine failures, cross-AZ replicas often protect you. But during region-wide degradation or cascading incidents, with <strong>local NVMe</strong>, there is <em>nothing to recover</em>. The storage is simply gone. What you can do is to recovery from recent backups - often lagging days. Write loss is guaranteed between last backup and crash.</p>
<p>In contrast, EBS volumes can always be reattached to a new node.</p>
<p>The AWS us-east-1 outage just validated that <em>“local NVMe + async replication” is a high-risk strategy for mission-critical databases.</em></p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="option-3-object-storage-s3---durable--cheap-but-latency-is-a-challenge">Option 3: Object Storage (S3) - Durable &amp; Cheap, But Latency Is a Challenge<a href="https://www.eloqdata.com/blog/2025/10/20/local-nvme-risky-for-database#option-3-object-storage-s3---durable--cheap-but-latency-is-a-challenge" class="hash-link" aria-label="Direct link to Option 3: Object Storage (S3) - Durable &amp; Cheap, But Latency Is a Challenge" title="Direct link to Option 3: Object Storage (S3) - Durable &amp; Cheap, But Latency Is a Challenge" translate="no">​</a></h2>
<p>Object storage is:</p>
<ul>
<li class="">3x cheaper than block storage</li>
<li class="">Regionally and cross-region durable</li>
<li class="">Built to survive region-level failures</li>
<li class="">Practically infinite</li>
<li class="">A first-class citizen for modern cloud-native platforms</li>
</ul>
<p>But the challenge remains: <strong>S3 latency is too high for OLTP</strong> if accessed synchronously.</p>
<p>This is why traditional OLTP engines avoid it.</p>
<p>So the question becomes: <em>How do we get the cost &amp; durability benefits of S3 without paying the latency penalty?</em></p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-data-substrate-approach-object-storage-first-nvme-as-cache-ebs-for-logs">The Data Substrate Approach: Object Storage First, NVMe as Cache, EBS for Logs<a href="https://www.eloqdata.com/blog/2025/10/20/local-nvme-risky-for-database#the-data-substrate-approach-object-storage-first-nvme-as-cache-ebs-for-logs" class="hash-link" aria-label="Direct link to The Data Substrate Approach: Object Storage First, NVMe as Cache, EBS for Logs" title="Direct link to The Data Substrate Approach: Object Storage First, NVMe as Cache, EBS for Logs" translate="no">​</a></h2>
<p>EloqData treats <strong>object storage (e.g., S3)</strong> as the <strong>primary data store</strong>, and architect the system to avoid the usual latency pitfalls:</p>
<table><thead><tr><th>Layer</th><th>Role</th><th>Why</th></tr></thead><tbody><tr><td><strong>S3 (Object Storage)</strong></td><td>Primary data store</td><td>Ultra-durable, Cheap</td></tr><tr><td><strong>EBS (Block Storage)</strong></td><td>Durable log storage</td><td>Small volume, low latency writes</td></tr><tr><td><strong>Local NVMe</strong></td><td>High-performance cache</td><td>Accelerates reads &amp; async flushes</td></tr></tbody></table>
<p>Through <a class="" href="https://www.eloqdata.com/blog/2024/08/11/data-substrate">Data Substrate</a>, we decouple storage from compute and split durability between:</p>
<ul>
<li class="">Log: persists immediately to EBS</li>
<li class="">Data store: periodically checkpointed to S3 (async + batched)</li>
<li class="">NVMe: purely a cache layer, safe to lose at any time</li>
</ul>
<p>This allows us to:</p>
<ul>
<li class="">Withstand node crashes seamlessly</li>
<li class="">Recover fully even if local NVMe is wiped</li>
<li class="">Handle region-level disruption by replaying logs and checkpoints</li>
<li class="">Enjoy millions of IOPS from NVMe without durability risk</li>
<li class="">Cut storage cost by 3x+ compared to full EBS-based systems</li>
</ul>
<p>Check out more on our products powered by Data Substrate:</p>
<ul>
<li class=""><a href="https://github.com/eloqdata/eloqkv" target="_blank" rel="noopener noreferrer" class="">EloqKV</a></li>
<li class=""><a href="https://github.com/eloqdata/eloqdoc" target="_blank" rel="noopener noreferrer" class="">EloqDoc</a></li>
<li class=""><a href="https://github.com/eloqdata/eloqsql" target="_blank" rel="noopener noreferrer" class="">EloqSQL</a></li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-larger-industry-trend">The Larger Industry Trend<a href="https://www.eloqdata.com/blog/2025/10/20/local-nvme-risky-for-database#the-larger-industry-trend" class="hash-link" aria-label="Direct link to The Larger Industry Trend" title="Direct link to The Larger Industry Trend" translate="no">​</a></h2>
<p>We are not alone in this shift. The broader ecosystem is moving object-storage-first:</p>
<table><thead><tr><th>System</th><th>Use of Object Storage</th></tr></thead><tbody><tr><td>Snowflake</td><td>OLAP on S3</td></tr><tr><td>StreamNative Ursa</td><td>Streaming data on S3</td></tr><tr><td>Confluent Freight Clusters</td><td>Streaming data on S3</td></tr><tr><td>Turbopuffer</td><td>Vector &amp; full-text search on S3</td></tr></tbody></table>
<p>EloqData brings this model to OLTP with a transactional, low-latency engine powered by Data Substrate.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="after-the-outage-a-hard-question-every-architect-should-ask">After the Outage: A Hard Question Every Architect Should Ask<a href="https://www.eloqdata.com/blog/2025/10/20/local-nvme-risky-for-database#after-the-outage-a-hard-question-every-architect-should-ask" class="hash-link" aria-label="Direct link to After the Outage: A Hard Question Every Architect Should Ask" title="Direct link to After the Outage: A Hard Question Every Architect Should Ask" translate="no">​</a></h2>
<blockquote>
<p><em>If my database node died right now, would I lose all my data?</em></p>
</blockquote>
<p>If you're running a primary database on <strong>local NVMe</strong>, and relying solely on async replicas, the answer might be yes.</p>
<p>It’s time to rethink durability assumptions in the cloud era.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="summary">Summary<a href="https://www.eloqdata.com/blog/2025/10/20/local-nvme-risky-for-database#summary" class="hash-link" aria-label="Direct link to Summary" title="Direct link to Summary" translate="no">​</a></h2>
<table><thead><tr><th>Strategy</th><th>Performance</th><th>Durability</th><th>Region Outage Risk</th><th>Cost</th></tr></thead><tbody><tr><td>EBS only</td><td>Medium</td><td>✅</td><td>✅</td><td>$$$</td></tr><tr><td>Local NVMe only</td><td>Fast</td><td>❌</td><td>❌</td><td>$$</td></tr><tr><td>NVMe + async replicas</td><td>Fast</td><td>Partial</td><td>High</td><td>$$</td></tr><tr><td>Object Storage + Log + NVMe Cache (<strong>EloqData</strong>)</td><td>Fast</td><td>✅✅</td><td>✅✅</td><td>$</td></tr></tbody></table>
<p>AWS us-east-1 just reminded the industry:
<strong>Performance is replaceable. Lost data is not.</strong></p>
<p>With the right architecture, you don’t have to choose.</p>
<ul>
<li class=""><em>Build fast.</em></li>
<li class=""><em>Stay durable.</em></li>
<li class=""><em>Be outage-proof.</em></li>
</ul>
<p>That’s the future we’re building at <strong>EloqData</strong>.</p>
<p>Check out more on our open source databases:</p>
<ul>
<li class=""><a href="https://github.com/eloqdata/eloqkv" target="_blank" rel="noopener noreferrer" class="">EloqKV</a></li>
<li class=""><a href="https://github.com/eloqdata/eloqdoc" target="_blank" rel="noopener noreferrer" class="">EloqDoc</a></li>
<li class=""><a href="https://github.com/eloqdata/eloqsql" target="_blank" rel="noopener noreferrer" class="">EloqSQL</a></li>
</ul>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Coroutines and Async Programming: The Future of Online Databases]]></title>
        <id>https://www.eloqdata.com/blog/2025/09/26/coroutines-async-programming</id>
        <link href="https://www.eloqdata.com/blog/2025/09/26/coroutines-async-programming"/>
        <updated>2025-09-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Explore one of our core engineering practices for future online databases: coroutines and async programming.]]></summary>
        <content type="html"><![CDATA[<p>Online databases are the backbone of interactive applications. Despite coming in many different types, online databases are all engineered for low-latency, high-throughput <a href="https://en.wikipedia.org/wiki/Create,_read,_update_and_delete" target="_blank" rel="noopener noreferrer" class="">CRUD</a> operations. At EloqData, we use the universal <a href="https://eloqdata.com/blog/2025/07/14/technology" target="_blank" rel="noopener noreferrer" class="">Data Substrate</a> to build online databases for any model—from key-value and tables to JSON documents and vectors. In this post, we explore one of our core engineering practices for future online databases.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="hardware-trend">Hardware Trend<a href="https://www.eloqdata.com/blog/2025/09/26/coroutines-async-programming#hardware-trend" class="hash-link" aria-label="Direct link to Hardware Trend" title="Direct link to Hardware Trend" translate="no">​</a></h2>
<p>Database implementations are closely tied to hardware development. If we look back at the past decade, the trend is clear: the performance improvement of I/O devices has far outpaced that of CPUs. And the trend is likely to continue for the <a href="https://www.tomshardware.com/tech-industry/pci-sig-announces-pcie-8-0-spec-with-twice-the-bandwidth-1tb-s-of-peak-bandwidth-256-gt-s-per-lane-and-a-possible-new-connector" target="_blank" rel="noopener noreferrer" class="">foreseeable future</a>. As of 2025, it’s common for a single NVMe device to deliver <a href="https://semiconductor.samsung.com/consumer-storage/internal-ssd/9100-pro/" target="_blank" rel="noopener noreferrer" class="">2 million IOPS</a>. With 6 to 8 PCIE lanes, a single server can achieve a staggering total of 12 to 16 million IOPS.</p>
<div align="center"><div style="width:50%;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/cpu_change-689e383532b61c40a7ed3e6cdbd1e11b.png" alt="x" style="cursor:pointer;max-width:100%"></div></div></div>
<div align="center"><div style="display:flex;justify-content:space-between;gap:20px;margin:20px 0"><div style="flex:1;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/iops_change-400001a7984abe507bfb40124dce0a65.png" style="cursor:pointer;max-width:100%"></div></div><div style="flex:1;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/net_change-d1ea49206dfbc9749644a5ab0bd4b352.png" style="cursor:pointer;max-width:100%"></div></div></div></div>
<p>Such massive I/O gains, however, come at a cost. These staggering numbers can only be unlocked through massive parallelism. In the traditional multi-threading model, this means a proliferation of threads—each dispatching and waiting for an I/O request. In conventional engineering, dispatching an I/O request involves a synchronous syscall (e.g., read() or write()), and waiting for it incurs two context switches. Though these operations are fast in isolation, their aggregate CPU cost is significant in comparison to the needs of sustaining 16 million I/O operations per second.</p>
<p>What’s more, I/O requests don't appear out of thin air. In a database, every query initiates I/O requests and the average number of I/O requests per query remains relatively stable for a given workload. Pushing for greater I/O parallelism means handling more concurrent queries, which consumes additional CPU for query parsing and execution. The system quickly gets caught in a self-reinforcing downward spiral: escalating competition for CPU ultimately makes the database CPU-bound. Therefore, reducing the CPU cost, both for I/O and other core components, will be a primary focus for future database engineering.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="coroutines-and-async-programming">Coroutines and Async Programming<a href="https://www.eloqdata.com/blog/2025/09/26/coroutines-async-programming#coroutines-and-async-programming" class="hash-link" aria-label="Direct link to Coroutines and Async Programming" title="Direct link to Coroutines and Async Programming" translate="no">​</a></h2>
<p>Coroutines are functions that can be suspended and resumed. Unlike threads, which require expensive OS context switches, suspending or resuming a coroutine (whether stackless or stackful) is a user-space operation and can complete in less than 10 nanoseconds. This represents a 10 to 100-fold performance advantage, making coroutines an ideal programming model for processing massive parallel I/O requests.</p>
<p>Coroutines can unlock their full potential when combined with modern asynchronous I/O interfaces like io_uring. Async I/O allows a coroutine to dispatch an I/O request without blocking or invoking a syscall. After issuing the request, the coroutine suspends itself, allowing another coroutine to take over and continue execution, until it dispatches an I/O request and yields.</p>
<p>The advantages of coroutines extend beyond I/O to concurrency control in databases. Online databases must handle lots of concurrent reads and writes while ensuring proper isolation and serialization. This often requires blocking certain operations when data conflicts occur and resuming them later. The pattern of frequent suspension and resumption makes database queries an ideal use case for coroutines.</p>
<p>Combing coroutines and async I/O, we run each database query as a coroutine bound to a CPU core. Each core has a dedicated worker thread that runs an event loop. When new network messages arrive, the async I/O notifies the worker thread, which creates a coroutine for each query and processes them sequentially. Whenever query execution is blocked—whether due to concurrency control or waiting for storage/network access—the current coroutine yields, allowing the next coroutine in the line to run.</p>
<p>At the end of each loop iteration, the thread makes one syscall (io_uring_submit) to notify the kernel all pending I/O requests accumulated during that cycle. The worker thread also calls a second syscall (io_uring_peek_cqe) for completed I/O requests from prior iterations or new incoming network requests. Coroutines whose requests have returned are scheduled to resume in the next loop iteration.</p>
<div style="text-align:center;margin:20px 0"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/exec_model-04fdebdff05b3c5fe4138b49c3c9727d.png" alt="Data Substrate Architecture 3" style="cursor:pointer;max-width:100%"></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="engineering-considerations">Engineering Considerations<a href="https://www.eloqdata.com/blog/2025/09/26/coroutines-async-programming#engineering-considerations" class="hash-link" aria-label="Direct link to Engineering Considerations" title="Direct link to Engineering Considerations" translate="no">​</a></h2>
<p>Coroutines are an old concept, but they have only recently gained popularity and achieved standardization in major programming languages. Throughout history, coroutines have appeared in various forms, such as</p>
<ul>
<li class="">Handwritten state machines for specific applications.</li>
<li class="">Coroutines in programming frameworks, e.g., <a href="https://brpc.apache.org/docs/bthread/" target="_blank" rel="noopener noreferrer" class="">bthread</a> in <a href="https://github.com/apache/brpc" target="_blank" rel="noopener noreferrer" class="">brpc</a>.</li>
<li class="">Stateful coroutines libraries, e.g., <a href="https://www.boost.org/libs/context" target="_blank" rel="noopener noreferrer" class="">boost<!-- -->:context</a>.</li>
<li class="">Stackless coroutines libraries, e.g., <a href="https://en.cppreference.com/w/cpp/coroutine/generator.html" target="_blank" rel="noopener noreferrer" class="">std::generator</a>.</li>
</ul>
<p>These coroutine variants emerged at different times and vary widely in maturity. For instance, std::generator was only introduced in C++23 and lacked compiler support until the recent GCC 14 release. Databases are built over many years, spanning multiple language standard and compiler release cycles. At every stage of development, we must carefully evaluate the availability, maturity, and inherent trade-offs of each coroutine implementation. <a href="https://github.com/eloqdata" target="_blank" rel="noopener noreferrer" class="">Our implementation</a> as of today has used all the first three forms:</p>
<ul>
<li class="">Handwritten state machines for core transaction processing</li>
<li class="">bthread in brpc for networking</li>
<li class="">boost::context to transform third-party code of query processing into stackful coroutines</li>
</ul>
<p>We’ve recently started adopting std::generator in some parts of transaction processing, where coroutine call chains are isolated and yield/resume states are complex to manage.</p>
<p>A second consideration when using coroutines is that they are susceptible to blocking: if a coroutine is blocked during execution, the entire worker thread is blocked. All other coroutines bound to that thread are also unable to make progress.</p>
<p>Blocking can arise from several sources. The first is I/O, which has been largely mitigated in our system through the adoption of async I/O. The second source is thread synchronization: when multiple worker threads access a critical section or a concurrent data structure, contention may cause one thread to block another. In our implementation, we adopt a shared-nothing memory architecture. Most thread synchronization is handled through lock-free concurrent queues. We still occasionally use locks (such as std::mutex), but we restrict them to very short, low-contention critical sections.</p>
<p>The third source of blocking originates from third-party code or libraries designed around the multi-threading model. Some code is straightforward to adapt. For instance, query processors, which involve minimal thread synchronization, require only minor code changes. Others, however, are inherently difficult or impossible to modify. One example is RocksDB, a widely used key-value store that serves as our default storage engine. As a multithreaded library, RocksDB relies extensively on internal locks and condition variables. Any coroutine invoking its APIs may block unpredictably. Hence, accessing RocksDB and similar libraries must be offloaded to a separate thread pool.</p>
<p>The final consideration is to carefully design internal states when using coroutines in distributed databases/systems. A key advantage of coroutines is their ability to preserve execution state—such as local variables and control flow—across suspension and resumption. However, this feature introduces a challenge in distributed environments, where failover is a common occurrence. Failover may not terminate the process but can invalidate node-local resources. Consider a scenario where a query holds a write lock, stores a pointer to it in its coroutine stack, and then yields. If a failover occurs during suspension, all locks on the node are deemed invalid. When the coroutine resumes, it must not blindly release the lock—which may now refer to an invalid memory address. To handle such cases, the system must incorporate a mechanism to validate coroutine-held resources that may be affected by failover. Only if the resources are confirmed to be alive should the resumed coroutine proceed to use them.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://www.eloqdata.com/blog/2025/09/26/coroutines-async-programming#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>Coroutines and async programming represent a paradigm shift in building high-performance online databases for modern hardware. Their adoption, however, demands careful engineering and thoughtful design. Our journey has just begun, and we believe significant work remains to fully unlock their potential. One area is storage engines. Currently, very few are fully built on coroutines and async I/O. We anticipate this will change soon.</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Benefits of Data Substrate Architecture]]></title>
        <id>https://www.eloqdata.com/blog/2025/07/16/data-substrate-benefit</id>
        <link href="https://www.eloqdata.com/blog/2025/07/16/data-substrate-benefit"/>
        <updated>2025-07-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this post, we elaborate on why we made specific design choices in Data Substrate and how these choices affect the resulting database solutions we built, including auto-scaling and cloud-native features.]]></summary>
        <content type="html"><![CDATA[<p>In the <a class="" href="https://www.eloqdata.com/blog/2025/07/15/data-substrate-detail">previous article</a>, we discussed the details of some of the architecture design of Data Substrate. In this article, we continue the discussion and elaborate on why we made these design choices and how these choices affect the resulting database solutions we built.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="thread-per-core-execution-model-and-async-io">Thread Per Core Execution Model and Async I/O<a href="https://www.eloqdata.com/blog/2025/07/16/data-substrate-benefit#thread-per-core-execution-model-and-async-io" class="hash-link" aria-label="Direct link to Thread Per Core Execution Model and Async I/O" title="Direct link to Thread Per Core Execution Model and Async I/O" translate="no">​</a></h2>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/cpu-84e8c4ecf83dd5fe3a289fb6656a596e.png" alt="Modern CPU" style="cursor:pointer;max-width:100%"></div></div></div>
<p>The Data Substrate adopts a <a href="https://en.wikipedia.org/wiki/Thread_(computing)#Thread_per_core" target="_blank" rel="noopener noreferrer" class="">thread-per-core execution model</a>, a strategy well-aligned with modern <a href="https://en.wikipedia.org/wiki/Multi-core_processor" target="_blank" rel="noopener noreferrer" class="">multicore hardware</a>. In this model, each core is assigned a dedicated thread that manages a variety of tasks using <a href="https://en.wikipedia.org/wiki/Cooperative_multitasking" target="_blank" rel="noopener noreferrer" class="">cooperative multitasking</a>, eliminating the overhead of frequent context switches. This design ensures high CPU cache locality, reduces synchronization complexity, and enables fine-grained control over task scheduling. By minimizing contention and maximizing predictable performance, the thread-per-core model lays the foundation for building scalable and efficient distributed systems.</p>
<p>In Data Substrate, the thread serves as the primary unit of abstraction, with data partitioned and processed independently by each thread. Inter-thread communication is handled exclusively through <a href="https://en.wikipedia.org/wiki/Message_passing" target="_blank" rel="noopener noreferrer" class="">message passing</a>, and synchronization is limited to lightweight producer-consumer queues. This design intentionally reduces reliance on shared memory within a server node, allowing threads to operate in isolation and avoid contention. Specialized threads are designated for tasks like <a href="https://en.wikipedia.org/wiki/Remote_procedure_call" target="_blank" rel="noopener noreferrer" class="">RPC</a> handling and storage access, but beyond that, threads remain largely self-contained. This approach not only reflects best practices for building high-performance single-node applications but also provides a clean and natural path for scaling out to distributed deployments.</p>
<p>To ensure responsiveness and efficient resource utilization, all potentially blocking operations in Data Substrate are handled asynchronously. By leveraging modern OS capabilities like <a href="https://en.wikipedia.org/wiki/Io_uring" target="_blank" rel="noopener noreferrer" class="">io_uring</a>, the system performs network and storage I/O without stalling worker threads. This asynchronous model is critical for managing latency in <a href="https://en.wikipedia.org/wiki/I/O_bound" target="_blank" rel="noopener noreferrer" class="">I/O-bound workloads</a>, a common characteristic of databases. With non-blocking operations, regular worker threads can remain active and productive, eliminating idle CPU cycles and maintaining high throughput even under heavy load.</p>
<p>In Data Substrate, each thread is designed to fulfill multiple roles—it may execute operations while also managing one or more CCMap shards. When a thread needs to access data and it happens to manage the shard containing the target entry, it can skip message passing altogether and access the memory directly, achieving maximum efficiency for local operations. This optimization demonstrates one of the core principles of the Data Substrate design, which is to enable zero overhead in degraded modes of operation: if a distributed feature isn't required, it should not impose any performance penalty. In this case, such optimization ensures that no additional overhead is incurred when the system operates in a non-distributed, single-threaded configuration. A direct consequence of this design is the <a class="" href="https://www.eloqdata.com/blog/2024/08/17/benchmark-single-node">performance characteristics of EloqKV</a>. When performing in-memory data access (i.e. when WAL and data store is disabled), its per-core performance is close to <a href="https://redis.io/" target="_blank" rel="noopener noreferrer" class="">Redis</a>, which is a single threaded implementation. In a multi-threaded setup, its performance is close to <a href="https://dragonflydb.io/" target="_blank" rel="noopener noreferrer" class="">DragonflyDB</a>, a modern purpose-built multi-threaded in-memory KV store with Redis API.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="decoupling-of-compute-memory-log-and-data-store">Decoupling of Compute, Memory, Log and Data Store<a href="https://www.eloqdata.com/blog/2025/07/16/data-substrate-benefit#decoupling-of-compute-memory-log-and-data-store" class="hash-link" aria-label="Direct link to Decoupling of Compute, Memory, Log and Data Store" title="Direct link to Decoupling of Compute, Memory, Log and Data Store" translate="no">​</a></h2>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/data_substrate_architecture-e42d999521ae2474db8ea5aa20b9cc19.png" alt="Data Substrate Architecture" style="cursor:pointer;max-width:100%"></div></div></div>
<p>Data Substrate achieves a high degree of modularity by decoupling compute, logging, and storage into separate threads, each responsible for its own function. This separation allows for clean isolation of responsibilities and avoids cross-component interference. While the current implementation may allow a thread to handle both query computation and buffer management, the architecture is designed such that these roles can be fully separated. The message-passing model abstracts away physical locality, enabling threads to interact seamlessly whether they reside on the same server or across the network. As a result, each subsystem can scale independently, allowing the system to adapt dynamically to changing workload patterns.</p>
<p>This decoupling is a foundational principle of the Data Substrate design, enabling the independent scaling of four critical resource types: compute (CPU cycles for query execution), memory (used as dirty data buffer and cache for hot data), logging (governing write throughput and durability latency), and storage (backing data capacity and cache miss performance). In cloud environments where resource provisioning is granular and on-demand, this separation offers a significant architectural advantage. It allows the system to allocate only the resources needed for a given workload, avoiding over-provisioning and improving both cost efficiency and operational flexibility.</p>
<p>Moreover, this architectural decoupling enables dynamic scaling—resources can be added or removed at runtime without interrupting ongoing operations. This is particularly powerful in <a href="https://en.wikipedia.org/wiki/Cloud_native_computing" target="_blank" rel="noopener noreferrer" class="">cloud-native deployments</a>, where workload patterns can shift rapidly and unpredictably. Data Substrate is designed to respond to these changes by elastically adjusting compute, memory, logging, and storage resources on demand. To our knowledge, it is the only database architecture that supports independent dynamic scaling of all four resource types, offering a level of elasticity and adaptability that traditional monolithic systems cannot match. (Note: dynamic scaling of logging is still under development.)</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="share-everything-architecture">Share Everything Architecture<a href="https://www.eloqdata.com/blog/2025/07/16/data-substrate-benefit#share-everything-architecture" class="hash-link" aria-label="Direct link to Share Everything Architecture" title="Direct link to Share Everything Architecture" translate="no">​</a></h2>
<p>Traditionally, the <a href="https://en.wikipedia.org/wiki/Shared_nothing_architecture" target="_blank" rel="noopener noreferrer" class="">shared-nothing architecture</a> has been the dominant model for building scalable distributed databases. In this approach, each server node operates as an independent unit with its own dedicated compute, memory, and storage resources. While this simplifies certain aspects of distribution, it also introduces rigid boundaries—when a node fails, its local data and state become immediately inaccessible, potentially disrupting the system. Moreover, dynamically scaling, though possible, is tedious and time consuming, since migrating data between servers often takes hours or even days. In contrast, modern cloud-native designs like <a href="https://aws.amazon.com/rds/aurora/" target="_blank" rel="noopener noreferrer" class="">Amazon Aurora</a> adopt a shared-storage model, where data is stored independently from compute, allowing read replicas to be dynamically launched to handle fluctuating read workloads. This separation of data and storage introduces greater elasticity and fault tolerance but remains limited in certain aspects. For example, to avoid the overhead of <a href="https://en.wikipedia.org/wiki/Distributed_transaction" target="_blank" rel="noopener noreferrer" class="">distributed transactions</a>, shared-storage system only allows a single writer node, and the readers generally can only see stale state.</p>
<p>Data Substrate takes a different approach by embracing a share-everything architecture, where every thread can access and interact with the full system state, regardless of its physical location. This is a deliberate departure from the traditional share-nothing and share-storage model.</p>
<p>In the share-everything model adopted by Data Substrate, each thread functions as an autonomous unit, capable of operating independently while still having access to the global system state. This abstraction simplifies system design by removing rigid node boundaries and enables a highly elastic and fault-tolerant architecture. Developers can think of programming Data Substrate as programming a large single-node machine, where the primary challenge is handling the failure and recovery of individual threads. When a thread fails, it is responsible for reconstructing its own state by replaying logs and retrieving data from the durable storage layer, ensuring that fault recovery is localized, efficient, and minimally disruptive to the rest of the system.</p>
<p>Of course, making a distributed system behave like a single computer requires careful handling of a number of complex challenges, such as avoiding <a href="https://en.wikipedia.org/wiki/Split-brain_(computing)" target="_blank" rel="noopener noreferrer" class="">split-brain scenarios</a> and ensuring consistent state across failures. Fortunately, decades of research in distributed systems have produced a rich set of techniques to address these problems. Consensus protocols like <a href="https://en.wikipedia.org/wiki/Paxos_(computer_science)" target="_blank" rel="noopener noreferrer" class="">Paxos</a> and <a href="https://en.wikipedia.org/wiki/Raft_(algorithm)" target="_blank" rel="noopener noreferrer" class="">Raft</a> ensure agreement among nodes, while <a href="https://www.microsoft.com/en-us/research/publication/pacifica-replication-in-log-based-distributed-storage-systems/" target="_blank" rel="noopener noreferrer" class="">replication protocols</a> maintain data consistency and durability. <a href="https://dl.acm.org/doi/10.1145/74850.74870" target="_blank" rel="noopener noreferrer" class="">Lease</a> and <a href="https://ieeexplore.ieee.org/document/980014" target="_blank" rel="noopener noreferrer" class="">failure detection mechanisms</a> help coordinate access and detect faults promptly. Data Substrate builds on these foundational techniques, integrating them into its architecture to provide the illusion of a unified system while preserving the robustness and safety required in a truly distributed environment.</p>
<p>One common concern when treating a distributed system as a single large computer is <em>data locality</em>, which traditionally has a major impact on performance due to network latency and bandwidth limitations. However, in modern cloud environments, high-speed datacenter networks with sub-millisecond latency and multi-gigabyte bandwidth have significantly reduced this bottleneck. While data locality still plays an important role in optimizing performance, it is no longer a strict constraint. In a share-everything architecture like Data Substrate, data locality is a matter of <em>policy</em> rather than a concern for correctness. By adjusting policy to restrict thread placement and scheduling, Data Substrate can effectively mimic shared-nothing or shared-storage architectures as needed.</p>
<p>With Data Substrate, building a database on top of a shared-everything distributed "computer" becomes a natural extension of decades of work on single-node database systems. This architecture allows us to reuse well-understood mechanisms for <a href="https://en.wikipedia.org/wiki/Concurrency_control" target="_blank" rel="noopener noreferrer" class="">concurrency control</a> and recovery, while adapting them to operate in a distributed context. For instance, databases on Data Substrate can support a range of <a href="https://en.wikipedia.org/wiki/Isolation_(database_systems)" target="_blank" rel="noopener noreferrer" class="">isolation levels</a>—read committed, repeatable read, serializable or <a href="https://en.wikipedia.org/wiki/Multiversion_concurrency_control" target="_blank" rel="noopener noreferrer" class="">MVCC and snapshot isolation</a>—just as traditional systems do. While higher isolation levels may carry greater performance costs on a large scale, their correctness remains straightforward to reason about. Thanks to the clean separation of concerns in the Data Substrate design, low-level distributed system challenges like node crashes and restarts are isolated from database-level logic, making the system easier to reason about, maintain, and extend.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="auto-scaling-and-scale-to-zero">Auto Scaling and Scale To Zero<a href="https://www.eloqdata.com/blog/2025/07/16/data-substrate-benefit#auto-scaling-and-scale-to-zero" class="hash-link" aria-label="Direct link to Auto Scaling and Scale To Zero" title="Direct link to Auto Scaling and Scale To Zero" translate="no">​</a></h2>
<p>In the context of cloud-native databases, features like <em>auto scaling</em> and <em>scale to zero</em> have become essential for optimizing cost and performance, with support now seen in systems such as <a href="https://neon.tech/" target="_blank" rel="noopener noreferrer" class="">Neon</a> and <a href="https://aws.amazon.com/rds/aurora/" target="_blank" rel="noopener noreferrer" class="">AWS Aurora</a>. Auto scaling enables a database to dynamically adjust compute resources in response to workload fluctuations—scaling up during peak demand and scaling down when load decreases—without manual intervention. Scale to zero takes this a step further by releasing compute and memory resources entirely when the system is idle for a certain duration. Upon receiving new workload, the system can rapidly rehydrate and begin processing within seconds. This approach dramatically improves cost efficiency, as users incur charges primarily for persistent storage when the database is not actively in use, aligning resource usage directly with actual demand.</p>
<p>Historically, implementing auto scaling and scale-to-zero has been challenging due to the tight coupling of compute and storage in traditional database architectures. In such systems, compute nodes typically manage in-memory caches, transaction state, and local storage, making it difficult to release or reassign resources without interrupting availability or risking data loss. The emergence of decoupled cloud storage changes this equation—by separating persistent data from compute, databases can now shed or acquire compute and memory resources.</p>
<p>Databases built on top of Data Substrate can implement auto-scaling with minimal complexity, thanks to the system's inherently decoupled architecture. Since compute, memory, storage, and logging are managed independently, a lightweight monitor process can dynamically allocate or release compute and memory resources—typically as containers or VMs—based on current workload demands. In a cloud environment, the storage (such as S3 or DynamoDB) are generally regarded as fully scalable. Last but not least, as an independent service in Data Substrate, the logging service can easily spin up more logging devices to allow better write throughput and reduce write latency as needed. As far as we know, the databases built on top of Data Substrate, such as <a class="" href="https://www.eloqdata.com/product/eloqkv">EloqKV</a>, <a class="" href="https://www.eloqdata.com/product/eloqsql">EloqSQL</a> and <a class="" href="https://www.eloqdata.com/product/eloqdoc">EloqDoc</a> are the only family of distributed databases that can independently scale all four types of resources.</p>
<p>As for scale-to-zero, when the database becomes idle, the monitor instructs it to take a final checkpoint, flush any in-memory state to durable storage, truncate the logs, and cleanly shut down the active components. Upon the return of workload, a new container can be launched, quickly recover the last state from storage, and resume processing with little to no delay—making this process efficient, cost-effective, and operationally seamless.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="object-storage-as-primary-data">Object Storage as Primary Data<a href="https://www.eloqdata.com/blog/2025/07/16/data-substrate-benefit#object-storage-as-primary-data" class="hash-link" aria-label="Direct link to Object Storage as Primary Data" title="Direct link to Object Storage as Primary Data" translate="no">​</a></h2>
<p>A major trend in the cloud computing era is the widespread adoption of object storages such as <a href="https://aws.amazon.com/s3/" target="_blank" rel="noopener noreferrer" class="">Amazon S3</a> as the primary data store, driven by their compelling advantages in cost, durability, and scalability. While object storage was initially considered slow, this perception has changed with the evolution of software architecture that can effectively hide or amortize latency. <a href="https://en.wikipedia.org/wiki/Online_analytical_processing" target="_blank" rel="noopener noreferrer" class="">OLAP</a> systems were early adopters—databases such as <a href="https://www.snowflake.com/" target="_blank" rel="noopener noreferrer" class="">Snowflake</a>, and open formats like <a href="https://iceberg.apache.org/" target="_blank" rel="noopener noreferrer" class="">Apache Iceberg</a>, support high-performance analytical processing on top of S3. The movement has since expanded beyond analytics: <a href="https://streamnative.io/" target="_blank" rel="noopener noreferrer" class="">StreamNative</a>'s Ursa engine uses S3 as its core storage layer for streaming data, <a href="https://www.confluent.io/" target="_blank" rel="noopener noreferrer" class="">Confluent</a> launched <a href="https://www.confluent.io/blog/freight-clusters-are-generally-available/" target="_blank" rel="noopener noreferrer" class="">Freight Clusters</a> to adopt a similar model. Even vector and full-text search engines, like <a href="https://turbopuffer.com/" target="_blank" rel="noopener noreferrer" class="">Turbopuffer</a>, are embracing object storage to decouple compute from data and support elastic scaling.</p>
<p>However, using object storage as the primary data store introduces non-trivial latency, which, while tolerable for OLAP workloads and asynchronous, event-driven architectures, becomes a bottleneck for <a href="https://en.wikipedia.org/wiki/Online_transaction_processing" target="_blank" rel="noopener noreferrer" class="">OLTP</a> systems that require consistently low-latency responses. This is why most OLTP databases still avoid relying directly on object storage. In cloud environments, fast and cost-effective <a href="https://en.wikipedia.org/wiki/NVM_Express" target="_blank" rel="noopener noreferrer" class="">NVMe</a> storage is often available on virtual machines, offering excellent performance characteristics—but this storage is ephemeral and tied to the lifecycle of the VM, meaning all data is lost if the VM is terminated. As a result, most OLTP databases today rely on services like <a href="https://aws.amazon.com/ebs/" target="_blank" rel="noopener noreferrer" class="">Amazon EBS</a> (Elastic Block Store), which provide durable, VM-independent storage. EBS can be reattached to a new VM after a failure, ensuring data durability, but this reliability comes at a cost—particularly when high throughput or low-latency IOPS are required, making it an expensive choice for performance-critical applications.</p>
<p>Database systems built on top of Data Substrate are uniquely positioned to take full advantage of object storage without suffering its typical drawbacks. Thanks to Data Substrate's decoupled architecture, S3 can be used as the primary data store to absorb the bulk of data capacity, while durability is jointly ensured by both the Log and the Data Store, as detailed in our previous post. Logging—which demands low-latency, durable writes—can be handled by a small EBS volume, since logs are mostly append-only and frequently truncated. This minimizes cost while preserving reliability. Updates to the data store happen at checkpointing time, when dirty CCMap entries are cleaned. Therefore, updates are performed in batches and asynchronously, meaning they do not block critical paths and can be safely written to S3 in the background. Meanwhile, local ephemeral NVMe storage can serve as a high-performance cache for read operations, absorbing cache misses efficiently. By orchestrating these three tiers—object storage for capacity, EBS for durability, and NVMe for performance—Data Substrate achieves a balanced, cost-effective storage strategy that leverages the strengths of each medium while avoiding their respective limitations.</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[A Deeper Dive Into Data Substrate Architecture]]></title>
        <id>https://www.eloqdata.com/blog/2025/07/15/data-substrate-detail</id>
        <link href="https://www.eloqdata.com/blog/2025/07/15/data-substrate-detail"/>
        <updated>2025-07-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this post, we dive deeper into the technical foundations of Data Substrate—highlighting the key design decisions, abstractions, and architectural choices that set it apart from both classical and modern distributed databases.]]></summary>
        <content type="html"><![CDATA[<p>In this article, we dive deeper into the technical foundations of Data Substrate—highlighting the key design decisions, abstractions, and architectural choices that set it apart from both classical and modern distributed databases.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="introduction">Introduction<a href="https://www.eloqdata.com/blog/2025/07/15/data-substrate-detail#introduction" class="hash-link" aria-label="Direct link to Introduction" title="Direct link to Introduction" translate="no">​</a></h2>
<p>In traditional database systems, scale and consistency often come at the cost of complexity, rigidity, or degraded performance. At EloqData, we've taken a fundamentally different approach. Data Substrate reimagines the database as a unified, distributed computer—where memory, compute, log and storage are fully decoupled yet globally addressable. In a <a class="" href="https://www.eloqdata.com/blog/2024/08/11/data-substrate">previous article</a>, we explored the motivations and core inspiration behind this architecture. Today, we dive deeper into its technical foundations—highlighting the key design decisions, abstractions, and architectural choices that set Data Substrate apart from both classical and modern distributed databases. While this article is not exhaustive, we aim to offer a clear, grounded view of how Data Substrate works and why it matters. For those interested in exploring further, visit our <a href="https://github.com/eloqdata" target="_blank" rel="noopener noreferrer" class="">GitHub repositories</a> or join our <a href="https://discord.gg/nmYjBkfak6" target="_blank" rel="noopener noreferrer" class="">Discord community</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-retrospect-overview-of-existing-database-designs">A Retrospect Overview of Existing Database Designs<a href="https://www.eloqdata.com/blog/2025/07/15/data-substrate-detail#a-retrospect-overview-of-existing-database-designs" class="hash-link" aria-label="Direct link to A Retrospect Overview of Existing Database Designs" title="Direct link to A Retrospect Overview of Existing Database Designs" translate="no">​</a></h2>
<p>Database systems have evolved over many years, and the canonical design remains the single-node, multi-threaded (or multi-process, in the case of e.g. <a href="https://www.postgresql.org/" target="_blank" rel="noopener noreferrer" class="">PostgreSQL</a>) architecture, as described in <a href="https://www.amazon.com/Database-System-Concepts-Abraham-Silberschatz/dp/9390727502" target="_blank" rel="noopener noreferrer" class="">numerous</a> <a href="https://www.amazon.com/Database-Management-Systems-Raghu-Ramakrishnan/dp/0072465638" target="_blank" rel="noopener noreferrer" class="">textbooks</a>. This classical design typically includes a page buffer manager for handling in-memory state, a lock manager for <a href="https://en.wikipedia.org/wiki/Concurrency_control" target="_blank" rel="noopener noreferrer" class="">concurrency control</a>, a <a href="https://en.wikipedia.org/wiki/Write-ahead_logging" target="_blank" rel="noopener noreferrer" class="">Write-Ahead Log (WAL)</a> for durability, and a B-Tree based data store for managing on-disk pages. These components collectively make up the state management layer of the database. Above this layer, the execution engine—which includes a SQL parser， query optimizer， and query execution engine—interacts with users and translates their requests into operations on the state management layer of the database.</p>
<p>As mentioned in our previous blog post, the Data Substrate design is heavily inspired by the textbook single-node database architecture. However, before diving into its specifics, it's important to examine the limitations of the traditional approach and the solutions that have emerged to address them.</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/impossible_trinity-9b84386449abad3ef7d00dbe51c0d31b.png" alt="The Impossible Trinity" style="cursor:pointer;max-width:100%"></div></div></div>
<p>One of the major challenge is <a href="https://en.wikipedia.org/wiki/Scalability#Horizontal_(scale_out)_and_vertical_scaling_(scale_up)" target="_blank" rel="noopener noreferrer" class="">scalability</a>: classical designs rely on vertical scaling—using larger, more powerful machines—and do not support horizontal scaling across multiple servers, which severely limits their ability to grow with increasing workloads.</p>
<p>To handle data that cannot fit in a single server, various approaches have been proposed. The simple method is <a href="https://en.wikipedia.org/wiki/Database_sharding" target="_blank" rel="noopener noreferrer" class="">database sharding</a>, by partitioning the database into multiple smaller database shards and split the workloads to be handled by multiple single-node databases. An example is the <a href="https://vitess.io/" target="_blank" rel="noopener noreferrer" class="">Vitess</a>/<a href="https://planetscale.com/" target="_blank" rel="noopener noreferrer" class="">PlanetScale</a> approach that leverages multiple <a href="https://www.mysql.com/" target="_blank" rel="noopener noreferrer" class="">MySQL</a> instances. While database sharding allows systems to scale out, it introduces significant operational complexity, often requiring manual planning, rebalancing, and custom tooling. Moreover, it offers poor support for cross-shard transactions and queries, which can lead to inconsistencies, degraded performance, or the need for complex coordination protocols that are hard to maintain and debug.</p>
<p>The alternative to database sharding is data sharding—building a truly distributed database system and distribute data among server nodes. However, this approach is inherently complex, particularly when it comes to supporting <a href="https://en.wikipedia.org/wiki/ACID" target="_blank" rel="noopener noreferrer" class="">ACID transactions</a> across distributed nodes. Many <a href="https://en.wikipedia.org/wiki/NoSQL" target="_blank" rel="noopener noreferrer" class="">NoSQL</a> systems like <a href="https://www.mongodb.com/" target="_blank" rel="noopener noreferrer" class="">MongoDB</a> and <a href="https://aws.amazon.com/dynamodb/" target="_blank" rel="noopener noreferrer" class="">DynamoDB</a> in the beginning chose to forgo strong consistency to simplify this challenge, offering <a href="https://en.wikipedia.org/wiki/Eventual_consistency" target="_blank" rel="noopener noreferrer" class="">eventual consistency</a> instead. Though later versions of these systems almost always added ACID support, famously pointed out by a recent <a href="https://dl.acm.org/doi/10.1145/3685980.3685984" target="_blank" rel="noopener noreferrer" class="">paper</a>. <a href="https://en.wikipedia.org/wiki/NewSQL" target="_blank" rel="noopener noreferrer" class="">NewSQL</a> databases such as <a href="https://www.cockroachlabs.com/" target="_blank" rel="noopener noreferrer" class="">CockroachDB</a> and <a href="https://www.pingcap.com/" target="_blank" rel="noopener noreferrer" class="">TiDB</a>, on the other hand, often adopt designs inspired by Google's <a href="https://research.google/pubs/pub36726/" target="_blank" rel="noopener noreferrer" class="">Percolator</a> or <a href="https://research.google/pubs/pub39966/" target="_blank" rel="noopener noreferrer" class="">Spanner</a>, layering distributed transactions over an underlying key-value store using protocols like <a href="https://en.wikipedia.org/wiki/Two-phase_commit_protocol" target="_blank" rel="noopener noreferrer" class="">two-phase commit</a>. While this enables stronger consistency, it typically comes with substantial overhead, resulting in significantly lower per-node performance compared to well-optimized single-node databases.</p>
<p>In recent years, shared-storage databases like <a href="https://aws.amazon.com/rds/aurora/" target="_blank" rel="noopener noreferrer" class="">Amazon Aurora</a> and <a href="https://neon.tech/" target="_blank" rel="noopener noreferrer" class="">Neon</a> (now part of <a href="https://www.databricks.com/" target="_blank" rel="noopener noreferrer" class="">Databricks</a>) have gained traction in the market. These systems offer virtually unlimited storage and support multiple read replicas, but they remain constrained by a single write node, which imposes fundamental limits on write scalability. Additionally, the read replicas often serve stale data due to replication lag, making them less suitable for workloads requiring strong consistency. This architecture also struggles when the hot data working set exceeds the memory capacity of a single node, causing performance degradation.</p>
<p>Another limitation of the classical single-node database design is the tight coupling between on-disk data storage and in-memory data representation. Historically, page–based B-trees have dominated as the primary on-disk storage format, leading many traditional databases to manage memory in terms of pages as well. This close coupling makes it difficult to support alternative data models like <a href="https://en.wikipedia.org/wiki/Graph_database" target="_blank" rel="noopener noreferrer" class="">graphs</a> or <a href="https://en.wikipedia.org/wiki/Stream_processing" target="_blank" rel="noopener noreferrer" class="">streams</a>, which may require different memory and storage abstractions. While newer systems have adopted techniques like <a href="https://en.wikipedia.org/wiki/Log-structured_merge-tree" target="_blank" rel="noopener noreferrer" class="">LSM trees</a> to address some of these challenges, legacy systems such as <a href="https://www.mysql.com/" target="_blank" rel="noopener noreferrer" class="">MySQL</a> and <a href="https://www.postgresql.org/" target="_blank" rel="noopener noreferrer" class="">PostgreSQL</a> remain largely constrained by their page-based memory architectures. Moreover, many storage related issues, such as garbage collection of free pages, are coupled with concurrency control of the databases or the query execution engines, making it difficult to swap out storage, adopt new concurrency control algorithms, or add new data models. In short, many of the existing databases are not cleanly modularized.</p>
<p>In this article, we discuss how EloqData's Data Substrate architecture addresses these issues.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="scalability-and-the-ccmap">Scalability and the CCMap<a href="https://www.eloqdata.com/blog/2025/07/15/data-substrate-detail#scalability-and-the-ccmap" class="hash-link" aria-label="Direct link to Scalability and the CCMap" title="Direct link to Scalability and the CCMap" translate="no">​</a></h2>
<p>As mentioned <a class="" href="https://www.eloqdata.com/blog/2024/08/11/data-substrate">earlier</a>, the Data Substrate architecture draws inspiration from classical single-node database design, but introduces key innovations to support diverse data models and enable true scalability.</p>
<p>One of the foundational changes lies in the redesign of the buffer pool. Instead of a traditional page-oriented buffer pool, we introduce an in-memory–optimized structure called the TxMap—a large, sharded key-value map. Each entry in the TxMap consists of a key-value pair, abstracting away the specific data model. The key could represent a page ID in a relational system, a node ID in a graph, or a timestamp in a time-series database, while the corresponding value might hold a page's content, a graph node's adjacency list, or a time-series payload. This abstraction allows the upper-layer computing engine to interact with data uniformly, regardless of the underlying model.</p>
<p>The TxMap is divided into multiple shards, each called a CCMap, or Cache and Concurrency control Map. A CCMap holds a subset of the TxMap's entries and is responsible not only for storing data but also for managing concurrency control metadata such as row-level locks. It tracks modification timestamps and marks entries as dirty when they have been updated. Crucially, each CCMap is exclusively accessed by a single thread, eliminating the need for latches and enabling highly efficient operations. Interactions with a CCMap entry are performed through <a href="https://en.wikipedia.org/wiki/Message_passing" target="_blank" rel="noopener noreferrer" class="">message passing</a> to the thread that owns the shard, ensuring safe and scalable concurrency management.</p>
<p>This abstraction provides a powerful foundation for scalability by offering a uniform mechanism for accessing sharded data through CCMaps. When a thread needs to access a data item, it simply issues a request to the corresponding CCMap, without needing to know whether the target shard resides on the local machine or a remote one. This decoupling allows the system to scale across multiple machines transparently.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="decoupling-of-logging-and-datastore">Decoupling of Logging and DataStore<a href="https://www.eloqdata.com/blog/2025/07/15/data-substrate-detail#decoupling-of-logging-and-datastore" class="hash-link" aria-label="Direct link to Decoupling of Logging and DataStore" title="Direct link to Decoupling of Logging and DataStore" translate="no">​</a></h2>
<p>Unlike many distributed database designs that adopt a "<a href="https://en.wikipedia.org/wiki/Shared_nothing_architecture" target="_blank" rel="noopener noreferrer" class="">shared nothing</a>" architecture, Data Substrate embraces a "shared everything" approach, treating the entire distributed system as a single, unified computer. In this model, the TxMap functions as a large, distributed memory accessible by any thread, regardless of where the data physically resides. In addition to TxMap, Data Substrate introduces two other major components: the logging service and the data store. These components are designed to be decoupled and shared across the system, enabling flexible coordination and efficient resource utilization at scale.</p>
<p>The logging service in Data Substrate fulfills largely the same role as the Write-Ahead Log (WAL) in traditional single-node databases, but it is reimagined for distributed environments. Unlike the monolithic WAL of classic systems, Data Substrate supports multiple distributed logging devices, each functioning as a durable, recoverable log accessible by any thread during recovery. Log durability is ensured either through replication or by leveraging highly reliable services such as <a href="https://aws.amazon.com/ebs/" target="_blank" rel="noopener noreferrer" class="">AWS EBS</a>.</p>
<p>When a thread needs to persist log entries during transaction execution, it can write to any available logging device. To maintain global order among distributed log entries—a critical requirement for consistency—traditional distributed databases often rely on external timestamp services (like time oracles) or specialized hardware (such as Spanner's atomic clock based truetime). Data Substrate, by contrast, adopts a hybrid approach that combines <a href="https://cse.buffalo.edu/~demirbas/publications/hlc.pdf" target="_blank" rel="noopener noreferrer" class="">Hybrid Logical Clocks (HLC)</a> with a <a href="https://dl.acm.org/doi/10.1145/2882903.2882935" target="_blank" rel="noopener noreferrer" class="">TicToc</a>-inspired time-travel mechanism to assign globally ordered timestamps. Because recovery threads can access all log devices, the system can reliably reconstruct the state of the database during failures. This flexible, decentralized logging model not only enhances fault tolerance but also improves scalability by avoiding bottlenecks associated with centralized log (and timestamp) coordination.</p>
<p>As in traditional single-node databases, a transaction in Data Substrate is considered committed once its WAL entry has been successfully written. The effects of the committed transaction are then applied to the in-memory buffer (i.e., the TxMap), marking certain entries as dirty. To manage memory usage, these dirty entries must be periodically checkpointed to a persistent data storage engine. Once checkpointing is complete, the corresponding log entries can be safely truncated to reclaim space. In Data Substrate, the data storage engine is abstracted to work seamlessly with any durable key-value stores, enabling flexibility in choosing the most appropriate storage backend for different workloads or deployment environments.</p>
<p>In the Data Substrate architecture, durability is jointly ensured by the WAL and the storage engine, while availability is maintained through the in-memory TxMap and the data store. A key invariant governs this design: any entry in the TxMap must exist either in the WAL or in the data store, ensuring its durability; conversely, a valid data entry must be present in the TxMap (if dirty), in the data store, or in both (if clean). Concurrency control is handled by CCMap, and very importantly is not the concern of the data store. This decoupling opens the door to significant performance optimizations.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="threading-and-cooperative-multitasking">Threading and Cooperative Multitasking<a href="https://www.eloqdata.com/blog/2025/07/15/data-substrate-detail#threading-and-cooperative-multitasking" class="hash-link" aria-label="Direct link to Threading and Cooperative Multitasking" title="Direct link to Threading and Cooperative Multitasking" translate="no">​</a></h2>
<p>Modern CPUs offer massive parallelism, with machines routinely equipped with dozens or even hundreds of cores. To fully utilize such hardware, software must be architected like a distributed system—even within a single server node. Data Substrate embraces this philosophy from the ground up. As discussed earlier, threads and CCMap shards are the primary units of execution and data ownership, and message passing is the dominant communication model. By avoiding global synchronization and minimizing shared mutable states, this design drastically reduces lock contention and enables highly scalable execution, even within a single physical machine.</p>
<p>Hardware advancements—such as <a href="https://en.wikipedia.org/wiki/NVM_Express" target="_blank" rel="noopener noreferrer" class="">NVMe</a> storage and <a href="https://en.wikipedia.org/wiki/Remote_direct_memory_access" target="_blank" rel="noopener noreferrer" class="">RDMA</a> networking—have fundamentally shifted the performance ratios of modern computer servers. Newer hardware often offers unprecedented I/O throughput. This contrasts sharply with traditional database designs, where I/O was often the primary bottleneck. In Data Substrate, we fully embrace this shift by adopting <a href="https://en.wikipedia.org/wiki/Cooperative_multitasking" target="_blank" rel="noopener noreferrer" class="">cooperative multitasking</a>. This allows threads to issue non-blocking operations and efficiently switch between tasks, minimizing CPU waits and ensuring that each thread remains productive, continuously processing meaningful work.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="fault-tolerance-recovery-and-transactions">Fault Tolerance, Recovery, and Transactions<a href="https://www.eloqdata.com/blog/2025/07/15/data-substrate-detail#fault-tolerance-recovery-and-transactions" class="hash-link" aria-label="Direct link to Fault Tolerance, Recovery, and Transactions" title="Direct link to Fault Tolerance, Recovery, and Transactions" translate="no">​</a></h2>
<p>Fault tolerance is a critical aspect of any distributed system, and Data Substrate is designed to gracefully handle both hardware failures and software crashes without sacrificing consistency or availability.</p>
<p>We categorize failures and recovery into two types. The first type involves traditional distributed systems failures, such as data loss due to hardware faults like disk crashes. To mitigate these risks, Data Substrate relies on standard distributed systems techniques—primarily data replication. In cloud environments, many storage services are already replicated for fault tolerance. For instance, <a href="https://aws.amazon.com/ebs/" target="_blank" rel="noopener noreferrer" class="">AWS EBS</a> and <a href="https://aws.amazon.com/s3/" target="_blank" rel="noopener noreferrer" class="">S3</a> are designed to withstand server and storage failures, allowing us to treat them as effectively fail-safe. In environments where such services are not available—such as on-premises deployments—we can achieve similar level of resilience by replicating data using protocols like <a href="https://en.wikipedia.org/wiki/Raft_(algorithm)" target="_blank" rel="noopener noreferrer" class="">Raft</a>. When a compute node fails, orchestration tools like <a href="https://kubernetes.io/" target="_blank" rel="noopener noreferrer" class="">Kubernetes</a> can automatically restart the node elsewhere. In the absence of such tooling, failure detection mechanisms based on <a href="https://dl.acm.org/doi/10.1145/74850.74870" target="_blank" rel="noopener noreferrer" class="">lease</a> protocols are well-understood and straightforward to implement.</p>
<p>The second type of failure concerns database transaction execution. For example, a transaction might be interrupted midway due to a node crash, resulting in a situation where the transaction is either uncommitted or its local updates have not yet been fully installed. In such cases, the system must determine the transaction's status and either roll it back or complete its execution. This decision is guided by the WAL, which records the transaction's intent and progress. If the log indicates that the transaction was not committed, all partial updates can be safely discarded. If the commit was recorded but the effects were not fully applied, the system can replay the transaction's updates to bring the state to consistency.</p>
<p>While transaction recovery is a well-studied topic, distributed environments introduce additional complexity—such as handling orphaned locks or coordinating partial transaction states across nodes. We will not go into all the details in this article as a separate paper is under submission to discuss in detail the transaction commit protocols used in Data Substrate. We will sure to discuss this paper in another article when the double blind review period passes.</p>
<p>It's worth noting that by cleanly decoupling the concerns of distributed systems and database transactions, failure handling in Data Substrate becomes much easier to reason about. Historically, the database and distributed systems communities have often taken overlapping but subtly different views on core concepts—such as the definition of consistency and the purpose of logging—which can make discussions around distributed databases unnecessarily confusing. By adopting a "shared everything" approach and treating the system as one large computer, the Data Substrate architecture aims to bridge these perspectives and offer a unified conceptual framework for reasoning about consistency, recovery, and fault tolerance in modern distributed databases.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-about-the-cap-theorem">What about the CAP Theorem?<a href="https://www.eloqdata.com/blog/2025/07/15/data-substrate-detail#what-about-the-cap-theorem" class="hash-link" aria-label="Direct link to What about the CAP Theorem?" title="Direct link to What about the CAP Theorem?" translate="no">​</a></h2>
<p>No discussion of distributed storage systems is complete without addressing the <a href="https://en.wikipedia.org/wiki/CAP_theorem" target="_blank" rel="noopener noreferrer" class="">CAP theorem</a>. The theorem famously states that a distributed system cannot simultaneously guarantee consistency, availability, and partition tolerance. However, as ]<a href="https://www.cs.umd.edu/~abadi/papers/abadi-pacelc.pdf" target="_blank" rel="noopener noreferrer" class="">researchers</a> have pointed out, the theorem's practical implications are most relevant in the presence of network partitions. In normal operation—when the cluster is healthy and there are no partitions—a well-designed distributed system should provide both consistency and availability. Therefore, understanding and clearly defining system behavior during partitions is key to building predictable and resilient distributed databases.</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/cap_and_pacelc-5bc15a24238c80bda1187d84d82b4a0a.png" alt="CAP and PACELC Theorem" style="cursor:pointer;max-width:100%"></div></div></div>
<p>A better understanding of distributed system behavior comes from the <a href="https://en.wikipedia.org/wiki/PACELC_theorem" target="_blank" rel="noopener noreferrer" class="">PACELC model</a>, which extends the CAP theorem by considering system trade-offs both during and outside of network partitions. PACELC categorizes how a system prioritizes consistency (C) or availability (A) during a Partition (P), and how it balances latency (L) versus consistency (C) Else (E) when no partition exists. Data Substrate generally aligns with the PC/EC category, similar to fully distributed databases like <a href="https://www.cockroachlabs.com/" target="_blank" rel="noopener noreferrer" class="">CockroachDB</a> and <a href="https://www.pingcap.com/" target="_blank" rel="noopener noreferrer" class="">TiDB</a>. This means that during a partition, the system prioritizes consistency over availability—it will halt database operations if it cannot ensure correctness. Outside of partitions, the system still favors consistency over latency: transactions must be fully committed and durable (e.g., WAL entries replicated and fsync'ed to disk) before acknowledging success to the client. This model ensures strong correctness guarantees, even at the cost of increased latency in normal operations.</p>
<p>Here we'd like to clarify a technical nuance to preempt potential concerns from careful readers. The timestamping mechanism used in Data Substrate allows databases built on it to achieve <a href="https://en.wikipedia.org/wiki/Serializability" target="_blank" rel="noopener noreferrer" class="">serializability</a> but not necessarily <a href="https://en.wikipedia.org/wiki/Linearizability" target="_blank" rel="noopener noreferrer" class="">linearizability</a>. This is due to potential clock skew and the use of time-traveling mechanisms, which may assign commit timestamps that do not strictly reflect real-time order for transactions <em>that are not causally related</em> (of course, the DB is linearlizable for any individual data item). This design is intentional—it reduces transaction aborts and improves throughput while still providing strong, well-understood consistency guarantees. In the context of traditional database workloads, serializability is typically sufficient to ensure correctness, and the so-called strict serializability (i.e. serializability plus real-time ordering) is usually not necessary. That said, like most components in Data Substrate, the timestamping subsystem is modular and can be swapped out—for example, by introducing a centralized time oracle—to support linearizability if required by the application.</p>
<p>While the current implementation of Data Substrate follows a PC/EC configuration, this behavior is not hardwired—it can be adapted to suit different application needs. For example, in high-performance caching scenarios where strict durability and consistency are less critical, the system can be reconfigured to operate in a PA/EL mode, prioritizing availability during partitions and favoring lower latency over strong consistency during normal operation. This flexibility allows Data Substrate to support a wide range of use cases, from transactional databases with strong guarantees to low-latency, high-throughput NoSQL-style workloads, all within a unified architectural framework.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="summary-and-future-topics">Summary and Future Topics<a href="https://www.eloqdata.com/blog/2025/07/15/data-substrate-detail#summary-and-future-topics" class="hash-link" aria-label="Direct link to Summary and Future Topics" title="Direct link to Summary and Future Topics" translate="no">​</a></h2>
<p>Data Substrate represents a new architectural approach to building databases for the cloud era, and we believe it has the potential to significantly influence future database design. That said, we do <strong>not</strong> claim to have invented all the techniques discussed in this article. In fact, nearly all of them—or some variation—have appeared in prior systems or research. What sets Data Substrate apart is the careful and deliberate selection of these techniques, which we believe is the essence of sound architectural design. In a field as mature and intricate as database systems—with over half a century of evolution—it is unrealistic to expect a single, never-before-seen breakthrough to resolve all challenges. The real value lies in the thoughtful integration of proven ideas and the meticulous attention to detail that brings a coherent and effective system to life.</p>
<p>This article has explored several technical aspects of the Data Substrate architecture, but many important topics remain for future discussion. For example, We have not addressed how existing compute engines, such as those from <a href="https://www.mysql.com/" target="_blank" rel="noopener noreferrer" class="">MySQL</a> or <a href="https://www.mongodb.com/" target="_blank" rel="noopener noreferrer" class="">MongoDB</a>, can be adapted to run on top of the Data Substrate. We have also not yet explored how this architecture naturally enables cloud-native advantages like Scale to Zero—where compute resources can be fully released during idle periods, incurring only storage costs—and diskless storage -- where object stores like <a href="https://aws.amazon.com/s3/" target="_blank" rel="noopener noreferrer" class="">S3</a> serve as the primary storage layer. These features offer significant reductions in total cost of ownership (TCO) in cloud environments. We will cover some of these topics in later posts.</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Data Substrate Technology Explained]]></title>
        <id>https://www.eloqdata.com/blog/2025/07/14/technology</id>
        <link href="https://www.eloqdata.com/blog/2025/07/14/technology"/>
        <updated>2025-07-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[This article explores the motivations, technical foundations, and benefits of Data Substrate, providing a comprehensive understanding of how this architecture addresses the critical challenges facing modern data infrastructures.]]></summary>
        <content type="html"><![CDATA[<p>At EloqData, we've developed <strong>Data Substrate</strong>—a database architecture designed to meet the unprecedented demands of modern applications in the AI age. Unlike traditional database systems that struggle with the scale and complexity of AI workloads, Data Substrate reimagines the database as a unified, distributed computer where memory, compute, logging, and storage are fully decoupled yet globally addressable.</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/dstech1-e86c01635b471c48534b9d7c0ba5b8b7.jpg" alt="Building a Data Foundation for Agentic AI Applications" style="cursor:pointer;max-width:100%"></div></div></div>
<p>This series of articles explore the motivations, technical foundations, and benefits of Data Substrate, providing a comprehensive understanding of how this architecture addresses the critical challenges facing modern data infrastructure in the AI age.</p>
<p>Some of the topics covered are rather heavy in technical jargons, and require a good understanding of database internal mechanisms to appreciate. We apologize in advance.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-data-substrate-motivation-and-philosophy">1. <a class="" href="https://www.eloqdata.com/blog/2024/08/11/data-substrate">Data Substrate: Motivation and Philosophy</a><a href="https://www.eloqdata.com/blog/2025/07/14/technology#1-data-substrate-motivation-and-philosophy" class="hash-link" aria-label="Direct link to 1-data-substrate-motivation-and-philosophy" title="Direct link to 1-data-substrate-motivation-and-philosophy" translate="no">​</a></h3>
<p>This article introduces the core philosophy behind Data Substrate. We explore why traditional database architectures fall short in the AI era and present our vision for a new approach that treats the entire distributed system as a single, unified computer.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-a-deeper-dive-into-data-substrate-architecture">2. <a class="" href="https://www.eloqdata.com/blog/2025/07/15/data-substrate-detail">A Deeper Dive Into Data Substrate Architecture</a><a href="https://www.eloqdata.com/blog/2025/07/14/technology#2-a-deeper-dive-into-data-substrate-architecture" class="hash-link" aria-label="Direct link to 2-a-deeper-dive-into-data-substrate-architecture" title="Direct link to 2-a-deeper-dive-into-data-substrate-architecture" translate="no">​</a></h3>
<p>This technical deep-dive explores the architectural foundations of Data Substrate. We examine the key design decisions, abstractions, and technical choices that set Data Substrate apart from both classical and modern distributed databases.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-the-benefits-of-data-substrate-architecture">3. <a class="" href="https://www.eloqdata.com/blog/2025/07/16/data-substrate-benefit">The Benefits of Data Substrate Architecture</a><a href="https://www.eloqdata.com/blog/2025/07/14/technology#3-the-benefits-of-data-substrate-architecture" class="hash-link" aria-label="Direct link to 3-the-benefits-of-data-substrate-architecture" title="Direct link to 3-the-benefits-of-data-substrate-architecture" translate="no">​</a></h3>
<p>This article examines the practical benefits and real-world implications of Data Substrate. We discuss how our design choices translate into concrete advantages for modern applications, particularly in cloud environments.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-data-substrate-matters">Why Data Substrate Matters<a href="https://www.eloqdata.com/blog/2025/07/14/technology#why-data-substrate-matters" class="hash-link" aria-label="Direct link to Why Data Substrate Matters" title="Direct link to Why Data Substrate Matters" translate="no">​</a></h2>
<p>Traditional database architectures were designed for a different era—one where data volumes were smaller, workloads were more predictable, and the demands of AI applications were unimaginable. Data Substrate represents a fundamental rethinking of database design, built from the ground up for the challenges and opportunities of the AI age.</p>
<p>By treating the distributed system as a single, unified computer, Data Substrate eliminates many of the complexities that have traditionally made distributed databases difficult to build, operate, and reason about. This approach enables:</p>
<ul>
<li class=""><strong>Modular architecture</strong> enables community collaboration and avoid reinventing the (many) wheels</li>
<li class=""><strong>True scalability</strong> without sacrificing consistency</li>
<li class=""><strong>Independent resource scaling</strong> for compute, memory, logging, and storage</li>
<li class=""><strong>Better performance</strong> through optimized hardware utilization and innovative algorithm design</li>
<li class=""><strong>Cloud-native features</strong> like auto-scaling and scale-to-zero</li>
<li class=""><strong>Simplified development</strong> through familiar single-node programming models</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="get-started-with-data-substrate">Get Started with Data Substrate<a href="https://www.eloqdata.com/blog/2025/07/14/technology#get-started-with-data-substrate" class="hash-link" aria-label="Direct link to Get Started with Data Substrate" title="Direct link to Get Started with Data Substrate" translate="no">​</a></h2>
<p>Ready to explore Data Substrate in action? Our open-source implementations are available on GitHub:</p>
<ul>
<li class=""><strong><a href="https://github.com/eloqdata/eloqkv" target="_blank" rel="noopener noreferrer" class="">EloqKV</a></strong>: A high-performance key-value store built on Data Substrate</li>
<li class=""><strong><a href="https://github.com/eloqdata/eloqsql" target="_blank" rel="noopener noreferrer" class="">EloqSQL</a></strong>: A MySQL-compatible distributed SQL database</li>
<li class=""><strong><a href="https://github.com/eloqdata/eloqdoc" target="_blank" rel="noopener noreferrer" class="">EloqDoc</a></strong>: A document database for modern applications</li>
</ul>
<p>Join our <a href="https://discord.gg/nmYjBkfak6" target="_blank" rel="noopener noreferrer" class="">Discord community</a> to connect with other developers and stay updated on the latest developments in Data Substrate technology.</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Rise of Object Storage in Cloud OLTP Architecture]]></title>
        <id>https://www.eloqdata.com/blog/2025/06/11/data-stream-summit</id>
        <link href="https://www.eloqdata.com/blog/2025/06/11/data-stream-summit"/>
        <updated>2025-06-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[At Data Stream Summit 2025, EloqData showed how Ursa and EloqDoc use object storage to cut costs and boost scalability—redefining cloud OLTP and streaming for AI-era workloads.]]></summary>
        <content type="html"><![CDATA[<p>At the recent <a href="https://datastreaming-summit.org/event/data-streaming-virtual-2025/speakers" target="_blank" rel="noopener noreferrer" class="">Data Stream Summit 2025</a>, Hubert Zhang, CTO of EloqData, delivered a talk on building elastic, agentic AI data pipelines using <a href="https://pulsar.apache.org/" target="_blank" rel="noopener noreferrer" class="">Apache Pulsar</a> and <a href="https://www.eloqdata.com/product/eloqdoc" target="_blank" rel="noopener noreferrer" class="">EloqDoc</a>.</p>
<div align="center"><iframe width="560" height="315" src="https://www.youtube.com/embed/VwOh2e7cwbA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"></iframe></div>
<p>The rise of autonomous, agentic AI applications has dramatically increased demands on underlying data infrastructure—highlighting the need for scalable, flexible, and cost-efficient solutions. While scalability and flexibility are widely discussed, cost-efficiency is harder to achieve. Many organizations only recognize the high cost of databases when faced with unexpectedly large cloud bills.</p>
<p>EloqData's <a class="" href="https://www.eloqdata.com/blog/2025/07/14/technology">Data Substrate</a> architecture is built from the ground up to address these challenges. In this post, we explore how our innovative approach delivers substantial cost savings. To understand the context, let’s first examine the pain points of traditional cloud data infrastructures, particularly those relying on EBS or local NVMe storage.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="challenges-with-traditional-cloud-architectures">Challenges with Traditional Cloud Architectures<a href="https://www.eloqdata.com/blog/2025/06/11/data-stream-summit#challenges-with-traditional-cloud-architectures" class="hash-link" aria-label="Direct link to Challenges with Traditional Cloud Architectures" title="Direct link to Challenges with Traditional Cloud Architectures" translate="no">​</a></h2>
<p>Traditional database architectures often assume that disk storage is durable and networking is free—assumptions that don't hold true in the cloud. Cloud environments introduce unique economic and operational characteristics. Key issues include:</p>
<ul>
<li class=""><strong>Expensive and Slow Persistent Storage:</strong> EBS offers durability, but at high cost and with limited performance.</li>
<li class=""><strong>Ephemeral Local Storage:</strong> Local NVMe is fast but lacks durability, making it unsuitable for critical data.</li>
<li class=""><strong>High Cross-AZ and Cross-Region Costs:</strong> Conventional architectures incur substantial network expenses due to required replication and frequent cross-zone or cross-region data transfers.</li>
</ul>
<p>Because of these differences, traditional architectures often perform poorly after migrating to the cloud. The most widely adopted storage class in the cloud is now <strong>object storage</strong>, exemplified by AWS S3—a format that traditional databases were not designed to support.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="object-storage-the-new-storage-standard-in-the-cloud">Object Storage: The New Storage Standard in the Cloud<a href="https://www.eloqdata.com/blog/2025/06/11/data-stream-summit#object-storage-the-new-storage-standard-in-the-cloud" class="hash-link" aria-label="Direct link to Object Storage: The New Storage Standard in the Cloud" title="Direct link to Object Storage: The New Storage Standard in the Cloud" translate="no">​</a></h2>
<p>Object storage provides key benefits that enable cost-effective scaling and separation of compute and storage resources. Its advantages include:</p>
<ul>
<li class=""><strong>High Durability:</strong> Object storage offers cross-AZ and cross-region replication by default, with SLAs up to 99.999999999% durability.</li>
<li class=""><strong>Reduced Network Costs:</strong> Eliminates costly cross-AZ replication fees.</li>
<li class=""><strong>Low Bandwidth Cost for Access:</strong> Even in failover scenarios, retrieving terabytes of data costs around $0.10 per million requests—far lower than other storage solutions.</li>
<li class=""><strong>Lowest Cost Per GB:</strong> Object storage is approximately 3.5× cheaper than standard EBS.</li>
</ul>
<p>Next-generation databases are already leveraging object storage. OLAP systems were early adopters, with technologies like Snowflake and formats like Iceberg and Parquet making full use of S3. More recently, streaming platforms such as <a href="https://streamnative.io/" target="_blank" rel="noopener noreferrer" class="">StreamNative</a>'s Ursa engine have adopted object storage as the primary storage layer. Recognizing this trend, <a href="https://www.confluent.io/" target="_blank" rel="noopener noreferrer" class="">Confluent</a> acquired <a href="https://www.warpstream.com/" target="_blank" rel="noopener noreferrer" class="">WarpStream</a> and introduced Freight Clusters that also utilize object storage. Vector and full-text search engines like <a href="https://turbopuffer.com/" target="_blank" rel="noopener noreferrer" class="">Turbopuffer</a> have followed suit.</p>
<p>However, using object storage directly introduces additional latency. While acceptable for OLAP workloads and asynchronous event-driven architectures, this latency is less suitable for OLTP workloads that demand low-latency responses. As a result, object storage is rarely used in OLTP database systems—until now.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="eloqdoc-a-cost-efficient-high-performance-document-database">EloqDoc: A Cost-Efficient, High-Performance Document Database<a href="https://www.eloqdata.com/blog/2025/06/11/data-stream-summit#eloqdoc-a-cost-efficient-high-performance-document-database" class="hash-link" aria-label="Direct link to EloqDoc: A Cost-Efficient, High-Performance Document Database" title="Direct link to EloqDoc: A Cost-Efficient, High-Performance Document Database" translate="no">​</a></h2>
<p>Leveraging EloqData’s groundbreaking decoupled architecture, <a class="" href="https://www.eloqdata.com/product/eloqdoc">EloqDoc</a>—a MongoDB-compatible JSON document store—effectively solves this challenge. EloqDoc fully embraces object storage, extending its benefits to document databases and unlocking elasticity, performance, and cost-efficiency. Unlike <a href="https://www.mongodb.com/products/platform/atlas-database" target="_blank" rel="noopener noreferrer" class="">MongoDB Atlas</a>, which incurs high costs from multiple replicas and cross-AZ traffic, EloqDoc uses object storage as its persistent layer.</p>
<p>Key advantages of EloqDoc include:</p>
<ul>
<li class=""><strong>Stateless Compute with Single Replica:</strong> Minimizes CPU and memory overhead while ensuring high availability and zero data loss.</li>
<li class=""><strong>Object Storage as Primary Storage:</strong> Batches writes asynchronously to mitigate object storage latency, while recent data is safeguarded with EBS for durability.</li>
<li class=""><strong>Local NVMe Caching:</strong> Delivers high-throughput, low-latency reads that outperform EBS IOPS by caching object storage locally.</li>
<li class=""><strong>Scalable Redo Logs:</strong> Improves write performance cost-effectively using additional EBS volumes.</li>
<li class=""><strong>MongoDB Compatibility and Distributed Transactions:</strong> Seamless migration from MongoDB, with robust transaction support powered by <a class="" href="https://www.eloqdata.com/blog/2025/07/14/technology">Data Substrate</a>.</li>
</ul>
<p>EloqDoc addresses the latency challenge by using local NVMe storage as an intelligent cache. Modern cloud-optimized instances offer significant local NVMe capacity (e.g., 3.5TB per 16-core server), enabling high-performance caching of active data while reducing costs.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="summary-of-the-video">Summary of the Video<a href="https://www.eloqdata.com/blog/2025/06/11/data-stream-summit#summary-of-the-video" class="hash-link" aria-label="Direct link to Summary of the Video" title="Direct link to Summary of the Video" translate="no">​</a></h2>
<p>In Hubert’s presentation, we demonstrate how EloqDoc and StreamNative’s Ursa can be combined to build scalable, efficient data pipelines. Ursa is an innovative streaming storage engine that disrupts traditional models like Kafka, which rely on costly persistent storage and cross-AZ replication.</p>
<p>Key innovations of Ursa include:</p>
<ul>
<li class=""><strong>Diskless Architecture:</strong> Eliminates the need for expensive EBS storage.</li>
<li class=""><strong>Stateless Brokers:</strong> Enables instant scaling and seamless failover without data movement.</li>
<li class=""><strong>Leaderless Design:</strong> Avoids Kafka’s partition leader bottlenecks, reducing latency and cross-AZ costs.</li>
</ul>
<p>Together with EloqDoc, Ursa enables the construction of a modern data pipeline that uses object storage as the primary data layer—fully realizing the potential of cloud-native infrastructure.</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Exploring EloqKV Decoupled Architecture: A Tour with an Agentic AI]]></title>
        <id>https://www.eloqdata.com/blog/2025/03/28/eloq-tour</id>
        <link href="https://www.eloqdata.com/blog/2025/03/28/eloq-tour"/>
        <updated>2025-03-28T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Explore EloqKV’s decoupled architecture powering TourBuddy, scaling smoothly from fast cache to durable, elastic storage—keeping your AI apps quick, resilient, and cost-effective at every growth stage.]]></summary>
        <content type="html"><![CDATA[<p>In the previous blog, we discussed the future <a class="" href="https://www.eloqdata.com/blog/2025/03/19/agentic">database foundation</a> for Agentic AI Applications. In this blog we will simplify the agentic application and use EloqKV as data store to explore EloqKV's decoupled architecture.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tour-begin">Tour Begin<a href="https://www.eloqdata.com/blog/2025/03/28/eloq-tour#tour-begin" class="hash-link" aria-label="Direct link to Tour Begin" title="Direct link to Tour Begin" translate="no">​</a></h2>
<p>Imagine you’re building an <a href="https://nvidianews.nvidia.com/news/nvidia-alphabet-and-google-collaborate-on-the-future-of-agentic-and-physical-ai" target="_blank" rel="noopener noreferrer" class="">agentic AI application</a>—a chatty, autonomous assistant that roams the digital world, helping users with everything from travel tips to life advice. Your agent is smart, proactive, and powered by a large language model (<a href="https://chat.openai.com/" target="_blank" rel="noopener noreferrer" class="">LLM</a>), but there’s one catch: it needs to remember <em>everything</em>. Every witty remark, every user request, every "Aha!" moment—it all needs to be stored somewhere reliable. That’s where EloqKV comes in, and today, I’m taking you on a wild tour of how this decoupled architecture saved my AI agent (and my sanity) as it grew from a humble prototype to a bustling, scalable powerhouse.</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/tourbuddy-2d951814c7c803c26b629e0ccf911916.png" alt="Building a Data Foundation for Agentic AI Applications" style="cursor:pointer;max-width:100%"></div></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="starting-simple-with-a-memory-cache">Starting Simple with a Memory Cache<a href="https://www.eloqdata.com/blog/2025/03/28/eloq-tour#starting-simple-with-a-memory-cache" class="hash-link" aria-label="Direct link to Starting Simple with a Memory Cache" title="Direct link to Starting Simple with a Memory Cache" translate="no">​</a></h3>
<p>When I first launched my agent—let’s call it "TourBuddy"—it was a lightweight conversationalist, guiding users through imaginary trips. I needed a quick way to store chat history, so I turned to EloqKV as a simple memory cache. Deploying it was a breeze with a single-node setup using this Docker Compose file:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token key atrule">version</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'3.8'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token key atrule">networks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">eloqnet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">driver</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> bridge</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">ipam</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">config</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> </span><span class="token key atrule">subnet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> 172.20.0.0/16</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token key atrule">services</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">eloqkv</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> eloqdata/eloqkv_full</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">container_name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> eloqkv</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">networks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">eloqnet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">ipv4_address</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> 172.20.0.4</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'6379:6379'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">command</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> /EloqKVRocksDB/bin/eloqkv </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">config /EloqKVRocksDB/conf/eloqkv.ini </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">ip=172.20.0.4 </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">port=6379 </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">ip_port_list=172.20.0.4</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token number">6379</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">working_dir</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> /EloqKVRocksDB</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">tty</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token boolean important">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">stdin_open</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token boolean important">true</span><br></span></code></pre></div></div>
<p>Boom! TourBuddy’s chat history was cached in memory, lightning-fast and ready to recall at a moment’s notice. It felt like giving my agent a short-term memory upgrade—perfect for its early days.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="scaling-up-with-persistent-storage">Scaling Up with Persistent Storage<a href="https://www.eloqdata.com/blog/2025/03/28/eloq-tour#scaling-up-with-persistent-storage" class="hash-link" aria-label="Direct link to Scaling Up with Persistent Storage" title="Direct link to Scaling Up with Persistent Storage" translate="no">​</a></h3>
<p>As TourBuddy gained fans (who doesn’t love a chatty travel guide?), the chat history ballooned. Storing everything in memory started to feel like stuffing a suitcase with too many souvenirs—expensive and impractical, especially since older messages were rarely accessed. Luckily, EloqKV has a persistent storage trick up its sleeve. With one tiny tweak to the config, I enabled RocksDB as a backend:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token key atrule">version</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'3.8'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token key atrule">networks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">eloqnet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">driver</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> bridge</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">ipam</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">config</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> </span><span class="token key atrule">subnet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> 172.20.0.0/16</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token key atrule">services</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">eloqkv</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> eloqdata/eloqkv_full</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">container_name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> eloqkv</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">networks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">eloqnet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">ipv4_address</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> 172.20.0.4</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'6379:6379'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">command</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> /EloqKVRocksDB/bin/eloqkv </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">config /EloqKVRocksDB/conf/eloqkv.ini </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">enable_data_store=true </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">ip=172.20.0.4 </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">port=6379 </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">ip_port_list=172.20.0.4</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token number">6379</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">working_dir</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> /EloqKVRocksDB</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">tty</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token boolean important">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">stdin_open</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token boolean important">true</span><br></span></code></pre></div></div>
<p>Now, historical chats lived happily on disk, freeing up memory for hot data while keeping costs down. TourBuddy could still reminisce about that epic Tokyo itinerary from months ago without breaking the bank.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="ensuring-durability-when-disaster-strikes">Ensuring Durability When Disaster Strikes<a href="https://www.eloqdata.com/blog/2025/03/28/eloq-tour#ensuring-durability-when-disaster-strikes" class="hash-link" aria-label="Direct link to Ensuring Durability When Disaster Strikes" title="Direct link to Ensuring Durability When Disaster Strikes" translate="no">​</a></h3>
<p>Everything was smooth sailing until one day, TourBuddy forgot my latest adventure—my new location had vanished into thin air! After some frantic debugging, I realized the cache had lost data during a hiccup. Durability became my new obsession. Thankfully, EloqKV’s got a Write-Ahead Log (WAL) feature to make data loss a thing of the past. Another quick tweak:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token key atrule">version</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'3.8'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token key atrule">networks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">eloqnet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">driver</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> bridge</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">ipam</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">config</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> </span><span class="token key atrule">subnet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> 172.20.0.0/16</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token key atrule">services</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">eloqkv</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> eloqdata/eloqkv_full</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">container_name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> eloqkv</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">networks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">eloqnet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">ipv4_address</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> 172.20.0.4</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'6379:6379'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">command</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> /EloqKVRocksDB/bin/eloqkv </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">config /EloqKVRocksDB/conf/eloqkv.ini </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">enable_data_store=true </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">enable_wal=true </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">ip=172.20.0.4 </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">port=6379 </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">ip_port_list=172.20.0.4</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token number">6379</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">working_dir</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> /EloqKVRocksDB</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">tty</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token boolean important">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">stdin_open</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token boolean important">true</span><br></span></code></pre></div></div>
<p>With WAL enabled, every chat message was safely logged before being committed. TourBuddy never forgot a destination again, and I slept better knowing my data was durable.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="decoupling-for-scalability">Decoupling for Scalability<a href="https://www.eloqdata.com/blog/2025/03/28/eloq-tour#decoupling-for-scalability" class="hash-link" aria-label="Direct link to Decoupling for Scalability" title="Direct link to Decoupling for Scalability" translate="no">​</a></h3>
<p>TourBuddy’s fame grew, and so did the traffic. Users flooded in, asking for tips from Paris to Patagonia, but the app’s response time crept up. Customers grumbled, and I traced the culprit to a rising cache miss rate—my memory buffer was too small. Scaling the entire cluster felt like overkill (moving terabytes of disk data? No thanks!). Enter EloqKV’s decoupled architecture: I could scale the compute/memory cluster separately from storage. Here’s how I set up a 3-node compute/memory cluster with a Cassandra storage backend:</p>
<p>Below is a snippet of the <code>docker-compose.yaml</code> file. For the complete <code>decoupled_storage</code> configuration, please refer to the <a href="https://github.com/eloqdata/docker-compose-example" target="_blank" rel="noopener noreferrer" class="">GitHub repository</a>.</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token key atrule">services</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">cassandra</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> cassandra</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">4.1.8</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">container_name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> cassandra</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">networks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">eloqnet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">ipv4_address</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> 172.20.0.2</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'9042:9042'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">environment</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> CASSANDRA_CLUSTER_NAME=eloq_kv</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> CASSANDRA_DC=DC1</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">healthcheck</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">test</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token string" style="color:rgb(255, 121, 198)">'CMD'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'cqlsh'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'-e'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'describe keyspaces'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">interval</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> 10s</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">timeout</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> 5s</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">retries</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token number">20</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">eloqkv1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> eloqdata/eloqkv_full</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">container_name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> eloqkv1</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">networks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">eloqnet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">ipv4_address</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> 172.20.0.3</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'6379:6379'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">depends_on</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">cassandra</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">condition</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> service_healthy</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">eloqkvbootstrap</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">condition</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> service_completed_successfully</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">command</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> /EloqKVCassandra/bin/eloqkv </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">config /EloqKVCassandra/conf/eloqkv.ini </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">ip=$</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">NODE1_IP</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">port=$</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">NODE1_PORT</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">ip_port_list=$</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">IP_PORT_LIST</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">enable_wal=$</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">ENABLE_WAL</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">enable_data_store=$</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">ENABLE_DATA_STORE</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">checkpoint_interval=$</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">CHECKPOINT_INTERVAL</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">cass_hosts=172.20.0.2 </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">cass_password=cassandra </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">cass_user=cassandra</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">working_dir</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> /EloqKVCassandra</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">eloqkv2</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  eloqkv3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><br></span></code></pre></div></div>
<p>In seconds, I had an elastic memory pool to handle hot chat data, slashing read latency. TourBuddy was back to its zippy self, delighting users with instant responses.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="boosting-write-throughput-on-a-budget">Boosting Write Throughput on a Budget<a href="https://www.eloqdata.com/blog/2025/03/28/eloq-tour#boosting-write-throughput-on-a-budget" class="hash-link" aria-label="Direct link to Boosting Write Throughput on a Budget" title="Direct link to Boosting Write Throughput on a Budget" translate="no">​</a></h3>
<p>Success brought a new challenge: write latency spiked as users chatted up a storm. My 3-node compute cluster was maxed out, and my budget screamed "No more high spec nodes!" EloqKV’s decoupled WAL log service came to the rescue. I added standalone low spec log nodes, each with its own cheap disk, to boost write throughput:</p>
<p>Below is a snippet of the <code>docker-compose.yaml</code> file. For the complete <code>decoupled_log_storage</code> configuration, please refer to the <a href="https://github.com/eloqdata/docker-compose-example" target="_blank" rel="noopener noreferrer" class="">GitHub repository</a>.</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token key atrule">services</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">cassandra</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> cassandra</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">4.1.8</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">container_name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> cassandra</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">networks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">eloqnet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">ipv4_address</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> 172.20.0.2</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'9042:9042'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">environment</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> CASSANDRA_CLUSTER_NAME=eloq_kv</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> CASSANDRA_DC=DC1</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">healthcheck</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">test</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token string" style="color:rgb(255, 121, 198)">'CMD'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'cqlsh'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'-e'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'describe keyspaces'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">interval</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> 10s</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">timeout</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> 5s</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">retries</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token number">20</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">eloqkv1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> eloqdata/eloqkv_full</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">container_name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> eloqkv1</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">networks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">eloqnet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">ipv4_address</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> 172.20.0.3</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'6379:6379'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">depends_on</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">cassandra</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">condition</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> service_healthy</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">eloqkvbootstrap</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">condition</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> service_completed_successfully</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">command</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> /EloqKVCassandra/bin/eloqkv </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">config /EloqKVCassandra/conf/eloqkv.ini </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">ip=$</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">NODE1_IP</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">port=$</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">NODE1_PORT</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">ip_port_list=$</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">IP_PORT_LIST</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">enable_wal=$</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">ENABLE_WAL</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">enable_data_store=$</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">ENABLE_DATA_STORE</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">checkpoint_interval=$</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">CHECKPOINT_INTERVAL</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">cass_hosts=172.20.0.2 </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">cass_password=cassandra </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">cass_user=cassandra</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">working_dir</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> /EloqKVCassandra</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">eloqkv2</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">eloqkv3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">logserver1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> eloqdata/eloqkv_full</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">container_name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> logserver1</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">networks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token key atrule">eloqnet</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token key atrule">ipv4_address</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> 172.20.0.11</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'9001:9001'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">command</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> sh </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">c "export LD_LIBRARY_PATH=/LogService/lib</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">$</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">LD_LIBRARY_PATH</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain">;/LogService/bin/launch_sv </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">conf=172.20.0.11</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token number">9001</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain">172.20.0.12</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token number">9002</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain">172.20.0.13</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">9003 </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">start_log_group_id=0 </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">node_id=0 </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">storage_path=/wal_eloqkv1"</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">working_dir</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> /LogServer</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">tty</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token boolean important">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token key atrule">stdin_open</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token boolean important">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">logserver2</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">  logserver3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><br></span></code></pre></div></div>
<p>Write latency plummeted, and TourBuddy handled the chatter like a champ—all without breaking the bank.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-cherry-on-topmore-features-to-explore">The Cherry on Top—More Features to Explore<a href="https://www.eloqdata.com/blog/2025/03/28/eloq-tour#the-cherry-on-topmore-features-to-explore" class="hash-link" aria-label="Direct link to The Cherry on Top—More Features to Explore" title="Direct link to The Cherry on Top—More Features to Explore" translate="no">​</a></h3>
<p>Now, my EloqKV cluster was a lean, mean, fully elastic machine. I could kill a compute node, a log node, or even a storage node, and TourBuddy kept humming along, thanks to EloqKV’s high availability magic. No single point of failure could stop my agent from sharing travel tales.</p>
<p>EloqKV isn’t done surprising me. It’s packed with goodies like cross-shard LUA scripting, Multi/Exec commands, and explicit transactions with Begin/Commit/Rollback APIs. Dive in and explore!</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wrapping-up-the-tour">Wrapping Up the Tour<a href="https://www.eloqdata.com/blog/2025/03/28/eloq-tour#wrapping-up-the-tour" class="hash-link" aria-label="Direct link to Wrapping Up the Tour" title="Direct link to Wrapping Up the Tour" translate="no">​</a></h2>
<p>Building TourBuddy with EloqKV was like taking a trusty companion on a journey. From a simple cache to a decoupled, elastic database, EloqKV adapted to every twist and turn, keeping my agentic AI app fast, durable, and cost-effective. Whether you’re caching hot data, persisting history, or scaling for the masses, EloqKV’s decoupled architecture has your back. So, grab the YAML snippets, fire up Docker, and take your own agent on a tour—you’ll be amazed at where it can go!</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building a Data Foundation for Agentic AI Applications]]></title>
        <id>https://www.eloqdata.com/blog/2025/03/19/agentic</id>
        <link href="https://www.eloqdata.com/blog/2025/03/19/agentic"/>
        <updated>2025-03-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[We are rapidly entering the Agentic Application Age. However, the backbone of these applications—the data infrastructure—faces immense challenges in scalability, consistency, and performance.]]></summary>
        <content type="html"><![CDATA[<p>We have recently open sourced our three products: <a href="https://github.com/eloqdata/eloqkv" target="_blank" rel="noopener noreferrer" class="">EloqKV</a>, <a href="https://github.com/eloqdata/eloqsql" target="_blank" rel="noopener noreferrer" class="">EloqSQL</a>, and <a href="https://github.com/eloqdata/eloqdoc" target="_blank" rel="noopener noreferrer" class="">EloqDoc</a>. These offerings reflect our commitment to addressing the evolving demands of modern data infrastructure, particularly as we enter an era dominated by powerful, autonomous AI systems.</p>
<p>LLM-powered Artificial Intelligence (AI) applications are driving transformative changes across industries, from healthcare to finance and beyond. We are rapidly entering the <a href="https://nvidianews.nvidia.com/news/nvidia-alphabet-and-google-collaborate-on-the-future-of-agentic-and-physical-ai" target="_blank" rel="noopener noreferrer" class="">Agentic Application Age</a>, an era where autonomous, AI-driven agents not only assist but actively make decisions, manage tasks, and optimize outcomes independently.</p>
<p>However, the backbone of these applications—the data infrastructure—faces immense challenges in scalability, consistency, and performance. In this post, we explore the critical limitations of current solutions and introduce EloqData’s innovative approach specifically designed to address these challenges. We also share our vision for an AI-native database, purpose-built to empower the Agentic Application Age, paving the way for smarter, more autonomous, and responsive AI applications in the future.</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/agentic-5a97c8c7b996d6e145855c60dcec50f3.png" alt="Building a Data Foundation for Agentic AI Applications" style="cursor:pointer;max-width:100%"></div></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-we-need-new-data-infra-in-the-ai-age">Why We Need New Data Infra in the AI Age<a href="https://www.eloqdata.com/blog/2025/03/19/agentic#why-we-need-new-data-infra-in-the-ai-age" class="hash-link" aria-label="Direct link to Why We Need New Data Infra in the AI Age" title="Direct link to Why We Need New Data Infra in the AI Age" translate="no">​</a></h2>
<p>AI applications are inherently data-intensive. To build such an application, developers must design a system capable of storing, processing, and retrieving diverse data types to fuel machine learning models, ensure real-time decision-making, and support continuous improvement.
A typical AI application integrates several components:</p>
<ul>
<li class="">Data Ingestion and Preprocessing: Collect raw data from various sources and transform it into formats suitable for analysis.</li>
<li class="">Model Training and Fine-Tuning: Train AI models on structured and unstructured data to improve performance on specific tasks.</li>
<li class="">Inference and Deployment: Use trained models to make predictions or automate workflows.</li>
<li class="">Feedback Loops: Capture user interactions and outcomes to retrain models for improved accuracy and adaptability.</li>
</ul>
<p>AI applications deal with diverse data types, each with specific storage and retrieval requirements:</p>
<ul>
<li class="">Structured Data: Typically stored in relational databases for tasks requiring tabular representations (e.g., user profiles, transaction histories).</li>
<li class="">Unstructured Data: Includes text, images, and videos, stored in document databases or object storage systems.</li>
<li class="">Embeddings: Vector representations of unstructured data for similarity search, often stored in vector databases.</li>
<li class="">Graphs: Relationships between entities (e.g., social networks, knowledge graphs), stored in graph databases.</li>
<li class="">Metadata: Information about data objects, stored in SQL or document databases for indexing and retrieval.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="challenges">Challenges<a href="https://www.eloqdata.com/blog/2025/03/19/agentic#challenges" class="hash-link" aria-label="Direct link to Challenges" title="Direct link to Challenges" translate="no">​</a></h3>
<p>With platforms like <a href="https://www.langchain.com/" target="_blank" rel="noopener noreferrer" class="">LangChain</a> and <a href="https://www.llamaindex.ai/" target="_blank" rel="noopener noreferrer" class="">LlamaIndex</a>, developers can seamlessly integrate multiple databases into AI workflows, enabling efficient storage and retrieval of multi-modal data. However, this approach often causes several challenges.</p>
<p>The first challenge is <strong>management complexity</strong>. AI applications typically need multiple databases to manage different data types. Each database comes from a different vendor, requires specialized skill sets, and has unique backup and failure recovery procedures. Backup strategies and failure recovery operations vary significantly between database systems, complicating overall infrastructure management. The additional effort required to maintain consistent backup routines, implement disaster recovery protocols, and ensure high availability across diverse database environments significantly increases operational complexity.</p>
<p>The second challenge is related to <strong>flexibility and agility</strong>. Modern databases are often designed to handle a specific type of workload under a specific range of performance envelopes. For example, Redis is designed to address latency issues, by sacrificing persistency, while PostgreSQL guarantees durability by trading-off scalability. Unfortunately, the landscape of AI applications is still rapidly changing. There is simply no “standard architecture” to handle typical AI application needs. Choosing a simple data architecture might involve a total revamp of the entire data pipeline as the application evolves.</p>
<p>The third challenge, which might be much more fundamental, is the difficulty of ensuring <strong>data consistency across multiple databases</strong>. AI applications often require combining data from diverse sources, which introduces transactional challenges. Databases traditionally provides ACID transactional semantics to ease the complexity of application development. This valuable property is lost when a query needs to be carried out by multiple databases. Without the ACID property, application developers are left to deal with the data consistency issues in application code, which is often error prone.</p>
<p>Consider building a simple Retrieval-Augmented Generation (<a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation" target="_blank" rel="noopener noreferrer" class="">RAG</a>) application to handle, say, financial reports of the public companies. RAG requires a document to be chunked and vectors of each chunk to be added to a vector database, while the document itself stored in a document DB. If the document is added while the insertion of the vectors failed, we may not be able to retrieve the document. On the other hand, if the vectors are added while the document failed to be inserted, we may get “broken links”. After a while, the developers may feel that knowledge graph of the companies might be a helpful source of information, and <a href="https://microsoft.github.io/graphrag/" target="_blank" rel="noopener noreferrer" class="">GraphRAG</a> might increase the recall quality. Therefore, we may need to add a graph database to the pipeline. Soon, real-time news and social media might be interesting data sources, so we need to quickly ingest these feeds. Shall we use a streaming platform? As one can imagine, the data pipeline quickly grows out of hand. How to guarantee consistent user experience when data is constantly being added to multiple complicated and independent database systems? How to make joint queries when the databases are constantly out of sync?</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/agentic1-44ee67b6ba08d1d17330504d22bc38a7.png" alt="EloqKV vs Redis Transaction" style="cursor:pointer;max-width:100%"></div></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-eloqdata-can-help">How EloqData can Help<a href="https://www.eloqdata.com/blog/2025/03/19/agentic#how-eloqdata-can-help" class="hash-link" aria-label="Direct link to How EloqData can Help" title="Direct link to How EloqData can Help" translate="no">​</a></h2>
<p>Some argue that eventual consistency is sufficient for AI applications. While this is true in certain scenarios, we believe a one-stop solution with full ACID transactions and lower costs is not only desirable but essential.</p>
<p>Why Eventual Consistency Falls Short</p>
<ul>
<li class="">Operational Complexity: Managing retries, failures, and inconsistencies in an eventual consistency model adds significant development and maintenance burden.</li>
<li class="">Scalability Risks: As AI applications grow, maintaining consistency across multiple databases becomes increasingly challenging.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-promise-of-a-unified-solution">The Promise of a Unified Solution<a href="https://www.eloqdata.com/blog/2025/03/19/agentic#the-promise-of-a-unified-solution" class="hash-link" aria-label="Direct link to The Promise of a Unified Solution" title="Direct link to The Promise of a Unified Solution" translate="no">​</a></h3>
<p>EloqData is redefining data infrastructure for AI applications with a one-stop solution ConvergedDB, that offers high performance, scalability, and full ACID transactions. This breakthrough eliminates the need for complex middleware like <a href="https://kafka.apache.org/" target="_blank" rel="noopener noreferrer" class="">Kafka</a> and allows developers to focus on building applications rather than managing infrastructure. At the heart of this solution is our innovative <a class="" href="https://www.eloqdata.com/blog/2025/07/14/technology">Data Substrate</a> architecture.</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/agentic2-040fbfdc3c43a2a43466b4622fc8dd2d.png" alt="EloqKV vs Redis Transaction" style="cursor:pointer;max-width:100%"></div></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="looking-into-the-future">Looking into the Future<a href="https://www.eloqdata.com/blog/2025/03/19/agentic#looking-into-the-future" class="hash-link" aria-label="Direct link to Looking into the Future" title="Direct link to Looking into the Future" translate="no">​</a></h2>
<p>While current RAG implementations may not demand strong ACID guarantees, the future of AI applications points toward more complex, collaborative agent architectures. Imagine a network of AI agents collaborating on tasks ranging from financial transactions to regulatory compliance. These scenarios will undoubtedly require strong ACID properties to ensure consistency and reliability.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="future-proof-your-infrastructure-with-eloqdata">Future-Proof Your Infrastructure with EloqData<a href="https://www.eloqdata.com/blog/2025/03/19/agentic#future-proof-your-infrastructure-with-eloqdata" class="hash-link" aria-label="Direct link to Future-Proof Your Infrastructure with EloqData" title="Direct link to Future-Proof Your Infrastructure with EloqData" translate="no">​</a></h3>
<p>By choosing EloqData from day one, organizations can:</p>
<ul>
<li class="">Eliminate Scalability Concerns: EloqData’s architecture ensures seamless scalability, whether your business grows vertically or horizontally.</li>
<li class="">Ensure Consistency: As multi-agent architectures become the norm, EloqData’s cross-model transactional guarantees will prove invaluable.</li>
<li class="">Maintain High Performance: With no performance degradation, EloqData delivers a robust solution for the most demanding AI applications.</li>
</ul>
<p>The future of AI applications requires data infrastructure that is scalable, consistent, and performant. EloqData’s one-stop multi-model database ConvergedDB, powered by its Data Substrate architecture, delivers on all three fronts, enabling developers to focus on innovation rather than infrastructure.</p>
<p>ConvergedDB is launching soon! Your feedback is greatly appreciated—stay tuned!</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Why We Develop EloqDB Mainly in C++]]></title>
        <id>https://www.eloqdata.com/blog/2024/10/26/why-cpp</id>
        <link href="https://www.eloqdata.com/blog/2024/10/26/why-cpp"/>
        <updated>2024-10-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Discover why we chose C++ for EloqKV, focusing on its scalability, efficiency, and durability over languages like Rust and Go.]]></summary>
        <content type="html"><![CDATA[<p>We have recently introduced <a class="" href="https://www.eloqdata.com/news/2024/08/16/eloqkv">EloqKV</a>, our distributed database product built on a cutting-edge architecture known as <a class="" href="https://www.eloqdata.com/blog/2025/07/14/technology">Data Substrate</a>. Over the past several years, the <strong>EloqData</strong> team has worked tirelessly to develop this software, ensuring it meets the highest standards of performance and scalability. One key detail we’d like to share is that the majority of EloqKV’s codebase was written in C++.</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/c_old_solid-922080bb4e69acce0e9c9cdfbb32abf2.jpg" alt="EloqKV vs Redis Transaction" style="cursor:pointer;max-width:100%"></div></div></div>
<p>Had we launched our product a decade ago, using C++ would have been an obvious and unremarkable choice. However, it's 2024, and the landscape has changed. Today, languages like <a href="https://www.rust-lang.org/" target="_blank" rel="noopener noreferrer" class="">Rust</a>, <a href="https://ziglang.org/" target="_blank" rel="noopener noreferrer" class="">Zig</a>, and other type-safe options like <a href="https://go.dev/" target="_blank" rel="noopener noreferrer" class="">Golang</a> are considered modern and trendy for systems programming. So, when we chose C++, a language that some might view as outdated or less "cool", or even bug-prone and "unsafe", it’s natural for people to wonder why.</p>
<p>In this article, we’d like to share the thought process behind our decision to choose C++ over some of the newer, more fashionable languages, the historical lessons we drew inspiration from, and the upcoming progress we expect in the future.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="choosing-a-programming-language-is-important">Choosing a Programming Language Is Important<a href="https://www.eloqdata.com/blog/2024/10/26/why-cpp#choosing-a-programming-language-is-important" class="hash-link" aria-label="Direct link to Choosing a Programming Language Is Important" title="Direct link to Choosing a Programming Language Is Important" translate="no">​</a></h2>
<p>Selecting the right programming language is crucial for any software project, but it becomes even more significant for complex systems software such as databases. The choice of language influences various aspects, including performance, ease of development, and maintainability. In a domain where efficiency and reliability are paramount, the programming language serves as the foundation upon which the entire system is built.</p>
<p>For databases, the implications of this choice are profound. A database must be capable of handling vast amounts of data while providing fast query responses and ensuring data integrity. These requirements necessitate a language that not only excels in performance but also allows for scalable and efficient development practices. Additionally, databases often undergo continuous development and enhancement over decades, making maintainability a critical factor. A well-chosen language can simplify the process of updating and expanding the software's features over time, ensuring that it remains relevant and effective in an ever-evolving technological landscape.</p>
<p>Consider the <a href="https://hadoop.apache.org/" target="_blank" rel="noopener noreferrer" class="">Hadoop</a> big data stack, which is predominantly built on the <a href="https://en.wikipedia.org/wiki/Java_virtual_machine" target="_blank" rel="noopener noreferrer" class="">Java Virtual Machine (JVM)</a>. While Java and JVM ecosystems have been one of the most popular programming language families and were lauded for their portability and rich features, in retrospect, this choice may not have been without controversy. The performance and memory overhead of the JVM, particularly issues related to garbage collection, has caused numerous challenges for developers. Indeed, <a href="https://www.redpanda.com/" target="_blank" rel="noopener noreferrer" class="">RedPanda</a> and <a href="https://www.scylladb.com/" target="_blank" rel="noopener noreferrer" class="">ScyllaDB</a> are notable examples of rewriting mature, widely-used Java-based frameworks—Kafka and Cassandra, respectively—in C++ from scratch to avoid the JVM penalties.</p>
<p>Another important consideration is the popularity of the programming language and the availability of developers familiar with it. For instance, <a href="https://spark.apache.org/" target="_blank" rel="noopener noreferrer" class="">Spark</a> and <a href="https://kafka.apache.org/" target="_blank" rel="noopener noreferrer" class="">Kafka</a> are developed using <a href="https://www.scala-lang.org/" target="_blank" rel="noopener noreferrer" class="">Scala</a>, while <a href="https://www.couchbase.com/" target="_blank" rel="noopener noreferrer" class="">Couchbase</a> and <a href="https://www.rabbitmq.com/" target="_blank" rel="noopener noreferrer" class="">Rabbitmq</a> are in <a href="https://www.erlang.org/" target="_blank" rel="noopener noreferrer" class="">Erlang</a>. Although these languages offer robust features and capabilities, they are not as widely adopted as other programming languages. This relative lack of popularity can create challenges when it comes to larger-scale developer engagement and finding experienced programmers. Toolchain support is generally not on par with more popular programming languages. A less common language may result in increased difficulty in recruiting talent, slowing down development processes and limiting community support for troubleshooting and innovation.</p>
<p>By the late 2010s, Rust emerged as one of the leading programming languages for developing database software. Newer projects such as <a href="https://www.pingcap.com/" target="_blank" rel="noopener noreferrer" class="">TiDB</a>, <a href="https://risingwave.com/" target="_blank" rel="noopener noreferrer" class="">RisingWave</a>, <a href="https://datafusion.apache.org/" target="_blank" rel="noopener noreferrer" class="">DataFusion</a>, and <a href="https://neon.tech/" target="_blank" rel="noopener noreferrer" class="">NeonDB</a> are prominent examples that leverage Rust's capabilities to build efficient and high-quality databases. Notably, RisingWave even published a <a href="https://risingwave.com/blog/building-a-cloud-database-from-scratch-why-we-moved-from-c-to-rust/" target="_blank" rel="noopener noreferrer" class="">blog post</a> detailing their decision to discard ten months of work in C++ to rewrite their entire codebase in Rust. Given that <strong>EloqData</strong> began its journey around 2021, when Rust was already well-established as a robust programming language with excellent features for building safe and performant databases, one might wonder why we opted for C++ instead.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="building-a-database-from-scratch-in-c-in-2024">Building a Database from Scratch in C++ in 2024<a href="https://www.eloqdata.com/blog/2024/10/26/why-cpp#building-a-database-from-scratch-in-c-in-2024" class="hash-link" aria-label="Direct link to Building a Database from Scratch in C++ in 2024" title="Direct link to Building a Database from Scratch in C++ in 2024" translate="no">​</a></h2>
<p>When we began our project, we were keenly aware that Rust was a highly competitive language for building the foundations of our database. Our eventual decision to choose C++ was based on three main factors.</p>
<p>The first strength of C/C++ lies in its database ecosystem support. Most existing and popular databases are developed in C/C++, providing a wealth of resources and innovations we could leverage. Our <strong>Data Substrate</strong> technology aims to create a unified, modular architecture that can capitalize on these existing resources while avoiding the need to reinvent the wheel. Although Rust offers good interoperability with C/C++, its memory management model and certain safety restrictions can complicate integration with many established projects.</p>
<p>Another advantage of C++ is its extensive support for foundational libraries. Since most operating systems and lower-level drivers are written in C or C++, bindings for these languages are often the native and best-supported APIs. Performance-focused libraries for IO and networking, such as <a href="https://www.dpdk.org/" target="_blank" rel="noopener noreferrer" class="">DPDK</a>, <a href="https://en.wikipedia.org/wiki/Remote_direct_memory_access" target="_blank" rel="noopener noreferrer" class="">RDMA</a> and <a href="https://github.com/axboe/liburing" target="_blank" rel="noopener noreferrer" class="">liburing</a>, as well as memory management tools such as <a href="https://github.com/microsoft/mimalloc" target="_blank" rel="noopener noreferrer" class="">mimalloc</a>, are developed with C/C++ and provide native support. In contrast, other languages typically require additional layers to effectively utilize these libraries. We anticipate that this trend will continue, with newer hardware and OS abstractions favoring C/C++ support first and foremost.</p>
<p>The third advantage of C++ is its longevity and mature toolchain. Infrastructure software often requires continuous updates and improvements over several decades. For instance, <a href="https://en.wikipedia.org/wiki/Oracle_Database" target="_blank" rel="noopener noreferrer" class="">Oracle Database</a> is over 45 years old, while <a href="https://en.wikipedia.org/wiki/MySQL" target="_blank" rel="noopener noreferrer" class="">MySQL</a> and <a href="https://en.wikipedia.org/wiki/PostgreSQL" target="_blank" rel="noopener noreferrer" class="">PostgreSQL</a> have been around for around 30 years. Even relatively newer systems like <a href="https://cassandra.apache.org/" target="_blank" rel="noopener noreferrer" class="">Cassandra</a>, <a href="https://www.mongodb.com/" target="_blank" rel="noopener noreferrer" class="">MongoDB</a>, and <a href="https://redis.io/" target="_blank" rel="noopener noreferrer" class="">Redis</a> are over 15 years old. To develop a reliable infrastructure solution, we need to be prepared to maintain the codebase for potentially half a century. A lot can change in the tech world over such a long period—consider that 20 years ago, <a href="https://www.perl.org/" target="_blank" rel="noopener noreferrer" class="">Perl</a> was a highly popular language, and <a href="https://observablehq.com/@mbostock/most-popular-programming-languages-2004-2021" target="_blank" rel="noopener noreferrer" class="">Delphi was more widely used than Python</a>.</p>
<p>When building long-lasting software, it’s crucial to consider the long-term survivability of the programming language, such as continued improvements on the compilers, up-to-date library developments, and modern IDE, debugger and profiler support. In this respect, C++ is a much safer bet. Its extensive history, active development community, and proven resilience over time give us confidence that it will continue to be relevant and well-supported for decades to come.</p>
<p>Of course, C++ comes with its share of legacies that can present challenges compared to many modern languages. To maximize productivity on C++ projects requires a certain level of discipline. While we won’t elaborate on the myriad best practices we’ve implemented to mitigate some of C++'s shortcomings — since these are well documented and <a href="https://google.github.io/styleguide/cppguide.html" target="_blank" rel="noopener noreferrer" class="">widely</a> <a href="https://github.com/cpp-best-practices/cppbestpractices" target="_blank" rel="noopener noreferrer" class="">discussed</a> <a href="https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines" target="_blank" rel="noopener noreferrer" class="">elsewhere</a> — we acknowledge that effective use of the language demands a strong commitment to coding standards and testing methodologies. In particular, the most harsh arguments against using C++, i.e. memory unsafeness, can be significantly mitigated when developing with a certain modern subset of the C++ language.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="going-forward">Going Forward<a href="https://www.eloqdata.com/blog/2024/10/26/why-cpp#going-forward" class="hash-link" aria-label="Direct link to Going Forward" title="Direct link to Going Forward" translate="no">​</a></h2>
<p>At <strong>EloqData</strong>, we strongly adhere to a modular design philosophy, as we are committed to building a lasting system that will support decades of continued improvements. We recognize that effective API interface design is crucial for enhancing software development productivity and maintainability. This principle is not only reflected in the overall architecture of <strong>Data Substrate</strong>, which accommodates various query and storage engines, but is also embedded throughout our software development process. We anticipate that innovations will continue to emerge—such as improved memory allocators, more efficient RPC libraries, and optimized hash-table implementations—and we aim to leverage these innovations as they become available in the future.</p>
<p>It is relatively straightforward for us to experiment with other programming languages in our projects when appropriate. We are eager to replace certain modules with components implemented in type-safe languages like Rust where it makes sense. Rust is an exceptional language with a strong following in the systems community, and we aim to utilize it more in many of our upcoming projects.</p>
<p>In contrast to many startup companies that emphasize rapid iteration, quick feedback loops, and fast prototyping, <strong>EloqData</strong> has taken a different approach. We place a stronger emphasis on doing things right from the onset to avoid future technical debts. While this focus may slow us down a bit, we believe it is a necessary investment. However, it’s worth noting that avoiding future debts is futile if the product and technology lack a viable future to begin with. Ultimately, whether we made the right choice will take time to determine. Regardless, we take pride in the decisions we’ve made and look forward to seeing how our efforts can help our customers tackle their most challenging data problems.</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[ACID in EloqKV : Atomic Operations]]></title>
        <id>https://www.eloqdata.com/blog/2024/09/01/benchmark-transaction</id>
        <link href="https://www.eloqdata.com/blog/2024/09/01/benchmark-transaction"/>
        <updated>2024-09-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this blog, we evaluate EloqKV as an in-memory cache, focusing on its single-node performance.]]></summary>
        <content type="html"><![CDATA[<p>In the previous blog, we discussed the <a class="" href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog">durable feature</a> of <strong>EloqKV</strong> and benchmarked the write performance of <strong>EloqKV</strong> with the Write-Ahead-Log enabled. In this blog, we will continue to explore the transaction capabilities of <strong>EloqKV</strong> and benchmark the performance of distributed atomic operations using the Redis <code>MULTI EXEC</code> commands.</p>
<p>In this blog, we evaluate <em>small scale</em> clusters to show the behavior of <strong>EloqKV</strong> accross servers. Scalability in a larger scale cluster with different number of servers will be evaluated at a later blog. All benchmarks were conducted on AWS (region: us-east-1) EC2 instances, with Ubuntu 22.04. In all tests, we use <strong>EloqKV</strong> version 0.7.4.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="transaction-in-eloqkv">Transaction in EloqKV<a href="https://www.eloqdata.com/blog/2024/09/01/benchmark-transaction#transaction-in-eloqkv" class="hash-link" aria-label="Direct link to Transaction in EloqKV" title="Direct link to Transaction in EloqKV" translate="no">​</a></h3>
<p>Fifteen years ago, the esteemed database researcher and Turing Award Winner <a href="https://en.wikipedia.org/wiki/Michael_Stonebraker" target="_blank" rel="noopener noreferrer" class="">Mike Stonebraker</a> famously wrote an <a href="https://cacm.acm.org/blogcacm/stonebraker-on-nosql-and-enterprises/" target="_blank" rel="noopener noreferrer" class="">article</a> in <em>Communications of the ACM</em> declaring, "No ACID Equals No Interest" for enterprise users. Unfortunately, due to the high costs associated with distributed transactions, many distributed databases avoid full transaction support in favor of better performance. For example, while Redis supports limited transaction operations in single-node mode, it does not support transactions across servers in a cluster.</p>
<p>Thanks to our <a class="" href="https://www.eloqdata.com/blog/2025/07/14/technology">Data Substrate</a> architecture, <strong>EloqKV</strong> is a fully ACID-compliant database. In addition to offering durability, which was discussed in a previous <a class="" href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog">blog post</a>, <strong>EloqKV</strong>'s transaction capabilities support the Redis <em>WATCH, MULTI, DISCARD</em>, and <em>EXEC</em> commands even in a cluster.</p>
<p>In this blog, we focus on benchmarking the <em>MULTI</em> and <em>EXEC</em> commands for <em>PUT/GET</em> operations—specifically, performing a series of read and write operations atomically across a cluster of servers. We believe this workload provides valuable insights into the costs associated with distributed transactions. Although <strong>EloqKV</strong> also supports <em>WATCH, DISCARD</em>, and <em>Lua scripting</em>, creating standard representative test cases for these features is more challenging.</p>
<p>In <strong>EloqKV</strong>, by default, the ACI (Atomicity, Consistency, Isolation) part of ACID is always enabled. No configuration changes are required to enable <em>MULTI</em> and related commands in a cluster. <strong>EloqKV</strong> supports different <a href="https://en.wikipedia.org/wiki/Isolation_(database_systems)#Isolation_levels" target="_blank" rel="noopener noreferrer" class="">levels of isolation</a>, with the default being <a href="https://en.wikipedia.org/wiki/Isolation_(database_systems)#Repeatable_reads" target="_blank" rel="noopener noreferrer" class="">Repeatable Reads</a>, which is the isolation level used in the experiments discussed in this blog.</p>
<p>In Repeatable Reads isolation level, reads and writes are about the same complexity. For read requests, each key must be read and then validated during the transaction commit phase to ensure that no modification happened in between reading and commiting. For write requests, a write lock must be acquired for each key and then released at the commit phase. Both require extra round-trips to accomplish and thus more expensive than non-transactional operations. In <strong>EloqKV</strong>, a single-key operation is executed as a transaction and will not incur additional overhead, regardless of the isolation level.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="experiments">Experiments<a href="https://www.eloqdata.com/blog/2024/09/01/benchmark-transaction#experiments" class="hash-link" aria-label="Direct link to Experiments" title="Direct link to Experiments" translate="no">​</a></h3>
<p>In the first experiment, we compare <strong>EloqKV</strong> and Redis in batch mode across different workloads. We focus on two batch modes:</p>
<ol>
<li class="">
<p><strong>Pipeline</strong>: In this mode, the client sends multiple commands to the server without waiting for responses to previous commands. The server processes these commands sequentially and returns all the responses at once. This batching approach significantly reduces network communication overhead, especially when executing many commands. Notice that each command in the pipeline is executed independently, with potentially other commands executed in between. However, we do enforce that the commands for any given key is executed in the order they appear in pipeline.</p>
</li>
<li class="">
<p><strong>MULTI / EXEC</strong>: This mode ensures that a group of commands is executed as a single atomic operation, meaning either all commands are executed or none are. Please <a class="" href="https://www.eloqdata.com/eloqkv/known-limit">note</a> that the Redis <code>MULTI/EXEC</code> command without <code>WATCH</code> normally cannot fail because Redis execute commands in a single thread on a single server, whereas <strong>EloqKV</strong>, being multi-threaded, can fail a transaction and roll back due to concurrent transaction conflicts.</p>
</li>
</ol>
<p>Redis does not support <code>Multi Exec</code> in cluster mode if keys in a single batch do not fall on the same shard. To work around this, users must use <code>hashtags</code> to ensure keys involved in a transaction are located on the same shard. This can be cumbersome and often cause load imbalance. Moreover, for Redis, <code>Pipeline</code> support in cluster mode is client dependent. It is not a feature supported on all Redis clients. <strong>EloqKV</strong>, on the other hand, does not have these limitations. Transactions and Pipelines work on a cluster of nodes just as on a single node. Though <strong>EloqKV</strong> does support <code>hashtags</code> to colocate keys in order to reduce network overhead.</p>
<p>In the following experiments, <strong>EloqKV</strong> operates in pure memory mode, with persistent storage and write-ahead-log (WAL) disabled.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="hardware-and-software-specification">Hardware and Software Specification<a href="https://www.eloqdata.com/blog/2024/09/01/benchmark-transaction#hardware-and-software-specification" class="hash-link" aria-label="Direct link to Hardware and Software Specification" title="Direct link to Hardware and Software Specification" translate="no">​</a></h3>
<p><strong>Server Machine:</strong></p>
<table><thead><tr><th>Service type</th><th>Node type</th><th>Node count</th></tr></thead><tbody><tr><td>EloqKV 0.7.4</td><td>c7g.8xlarge</td><td>1</td></tr><tr><td>EloqKV 0.7.4 Cluster</td><td>c7g.8xlarge</td><td>3</td></tr><tr><td>Redis 7.2.5</td><td>c7g.8xlarge</td><td>1</td></tr><tr><td>Client eloq-bench</td><td>c6gn.8xlarge</td><td>1</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="experiment">Experiment:<a href="https://www.eloqdata.com/blog/2024/09/01/benchmark-transaction#experiment" class="hash-link" aria-label="Direct link to Experiment:" title="Direct link to Experiment:" translate="no">​</a></h3>
<p>We developed a new benchmarking tool, <code>eloq_benchmark</code>, specifically to test the transaction performance of Redis and <strong>EloqKV</strong>, as <code>memtier_benchmark</code> does not support <code>Multi Exec</code>. You can download <code>eloq_benchmark</code> <a href="https://download.eloqdata.com/eloqkv/tools/eloq_benchmark-0.7.4.zip" target="_blank" rel="noopener noreferrer" class="">here</a></p>
<p>We run <code>eloq_benchmark</code> with the following configuration:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">eloq_benchmark </span><span class="token operator">--</span><span class="token plain">h $server_ip </span><span class="token operator">--</span><span class="token plain">p $server_port </span><span class="token operator">--</span><span class="token plain">numKeys</span><span class="token operator">=</span><span class="token plain">$keynum </span><span class="token operator">--</span><span class="token plain">numConnections</span><span class="token operator">=</span><span class="token plain">$conn </span><span class="token operator">--</span><span class="token plain">getRatio</span><span class="token operator">=</span><span class="token plain">$ratio </span><span class="token operator">--</span><span class="token plain">opType</span><span class="token operator">=</span><span class="token plain">$optype </span><span class="token operator">--</span><span class="token plain">batchSize</span><span class="token operator">=</span><span class="token plain">$batchsize </span><span class="token operator">--</span><span class="token plain">numTestOps</span><span class="token operator">=</span><span class="token plain">$testops</span><br></span></code></pre></div></div>
<ul>
<li class="">
<p><code>--numKeys</code>: Number of entries, which is set to 1000000.</p>
</li>
<li class="">
<p><code>--numConnections</code>: Number of concurrent connnections, which is set to 256 for single-node and 768 for three-node cluster.</p>
</li>
<li class="">
<p><code>--numTestOps</code>: Number of test operations, which is set to 5000000.</p>
</li>
<li class="">
<p><code>--getRatio</code>: Set it to 0 for write-only workload, 0.5 for mixed workload and 1 for read-only workload.</p>
</li>
<li class="">
<p><code>--opType</code>: Set batch mode, set it to <code>pipeline</code> for pipeline mode, set it to <code>tx</code> for <code>MutilExec</code> atomic mode.</p>
</li>
<li class="">
<p><code>--batchSize</code>: Number of <code>Put/Get</code> operations on random keys in a batch, which we set to 6.</p>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="results">Results<a href="https://www.eloqdata.com/blog/2024/09/01/benchmark-transaction#results" class="hash-link" aria-label="Direct link to Results" title="Direct link to Results" translate="no">​</a></h4>
<p>Below are the performance results of batch mode of Redis and <strong>EloqKV</strong> among various workload. Note that the number of operations of <code>PUT/GET</code> in a batch is fixed at <strong>6</strong>.</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/eloqkv_redis_batch_rr-0eedcfc6beecb0a840a65d727fb20cd2.png" alt="EloqKV vs Redis Transaction" style="cursor:pointer;max-width:100%"></div></div></div>
<p><code>X-axis</code>: Represents the different workload types (read/write/mixed) used in the benchmark, simulating a range of real-world scenarios.</p>
<p><code>Y-axis</code>: Throughput in Thousand OPS (Operations Per Second) for the batches. This number should be multiplied by 6 (batch size) to obtain the total KV operations.</p>
<p>On a single node, <strong>EloqKV</strong> significantly outperforms Redis in both pipeline and <code>Multi Exec</code> modes. With a fixed batch size of 6 keys, <strong>EloqKV</strong> achieves a throughput exceeding 200 million KV operations per second (KPS) in both modes on a single server. <code>Multi Exec</code> is slower than <code>Pipeline</code> due to additional book-keeping needed for atomic operations.</p>
<p>The throughput of a three-node <strong>EloqKV</strong> cluster is lower than that of a single-node <strong>EloqKV</strong>. In Pipeline mode, this is because of the additional network round trips. For <code>Multi Exec</code>, additional operations are needed for lock acquisition and releasing. Even so, the performance is quite respectable.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="evaluate-the-impact-of-batch-size">Evaluate the Impact of Batch Size<a href="https://www.eloqdata.com/blog/2024/09/01/benchmark-transaction#evaluate-the-impact-of-batch-size" class="hash-link" aria-label="Direct link to Evaluate the Impact of Batch Size" title="Direct link to Evaluate the Impact of Batch Size" translate="no">​</a></h3>
<p>Transaction size affects the efficiency of distributed transactions. In this experiment, we test <code>eloq_benchmark</code> with batch sizes ranging from 1 to 6.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="result">Result<a href="https://www.eloqdata.com/blog/2024/09/01/benchmark-transaction#result" class="hash-link" aria-label="Direct link to Result" title="Direct link to Result" translate="no">​</a></h4>
<p>Below are the performance results of <strong>EloqKV</strong> <code>Multi Exec</code> command with different batch size among various workload.</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/eloqkv_cluster_batch_size-f8f91914be3a5b8718963ce34823e1bc.png" alt="EloqKV vs Redis Transaction" style="cursor:pointer;max-width:100%"></div></div></div>
<p><code>X-axis</code>: Represents the different workload types (read/write/mixed) used in the benchmark, simulating a range of real-world scenarios.</p>
<p><code>Left Y-axis</code>: Throughput in Thousand OPS (Operations Per Second), shown as the bars.</p>
<p><code>Right Y-axis</code>: Percentage of Transaction Retries, shown as the dashed lines.</p>
<p>As expected, <strong>EloqKV</strong>’s transaction throughput decreases as the batch size increases. This is because larger batch sizes introduce additional work. Notice that the total KV operations carried out in the cluster must be multiplied by the batch size. Currently, <strong>EloqKV</strong> does not perform "query optimization" within a transaction. To guarantee transactional semantics, the operations in a batch are executed sequentially. In the future, we may optimize this by allowing some operations within a batch to be executed in parallel.</p>
<p>In our workload, we selected several random keys from a range to perform <code>PUT/GET</code> operations. As mentioned earlier, the key range was set to 1,000,000 in all experiments. We observed that transaction retries increase with larger batch sizes in both mixed and write-only workloads. This is due to the higher likelihood of transaction conflicts as the batch size grows. Reducing the level of concurrency or expanding the key range can help mitigate these conflicts. Additionally, the more concurrent writes, the more likely conflicts will occur. For read-only workloads, no transaction conflicts arise, so no transaction retries occurred.</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Company" term="Company"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[ACID in EloqKV : Durability]]></title>
        <id>https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog</id>
        <link href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog"/>
        <updated>2024-08-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[An in-depth analysis of EloqKV's durability performance, comparing it with other key-value stores under various workloads.]]></summary>
        <content type="html"><![CDATA[<p>In our previous blogs, we benchmarked <strong>EloqKV</strong> in memory cache mode, discussing both <a class="" href="https://www.eloqdata.com/blog/2024/08/17/benchmark-single-node">single node</a> and <a class="" href="https://www.eloqdata.com/blog/2024/08/22/benchmark-cluster">cluster</a> performances. In this post, we we benchmark <strong>EloqKV</strong> with durability enabled.</p>
<p>All benchmarks were conducted on AWS (region: us-east-1) EC2 instances running Ubuntu 22.04. Workloads were generated using the <a href="https://github.com/RedisLabs/memtier_benchmark" target="_blank" rel="noopener noreferrer" class="">memtier-benchmark</a> tool. In all tests, we use <strong>EloqKV</strong> version 0.7.4.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="understanding-durability">Understanding Durability<a href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog#understanding-durability" class="hash-link" aria-label="Direct link to Understanding Durability" title="Direct link to Understanding Durability" translate="no">​</a></h3>
<p>Durability is crucial for data stores, especially in applications where data loss during server failures is unacceptable. Most persistent data stores achieve durability through <a href="https://en.wikipedia.org/wiki/Write-ahead_logging" target="_blank" rel="noopener noreferrer" class="">Write-ahead Logging (WAL)</a>, ensuring that data is written to disk before responding to users. Additionally, many stores periodically checkpoint the data into a more compact form, allowing the log to be truncated.</p>
<p>However, many key-value (KV) caches prioritize performance over durability. For example, Redis uses an <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/" target="_blank" rel="noopener noreferrer" class="">Append Only File (AOF)</a> to achieve <em>some</em> level of durability by fsyncing data to a log file, either periodically or after each write command. Redis's single threaded architecture significantly increases latencies of other operations if a write operation blocks the thread, therefore, AOF sync writing for every write command is rarely deployed in practice. Consequently, <a href="https://www.dragonflydb.io/docs/managing-dragonfly/aof" target="_blank" rel="noopener noreferrer" class="">DragonflyDB</a> forgoes AOF altogether due to lack of demand, instead offering only periodic checkpointing similar to <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/" target="_blank" rel="noopener noreferrer" class="">Redis's RDB</a>.</p>
<p><strong>EloqKV</strong> is a fully ACID-compliant database, providing full data durability through WAL. Leveraging our decoupled Data Substrate architecture, the WAL for an <strong>EloqKV</strong> server can either be embedded within the same process or run as a separate LogService. The log can be replicated across multiple machines or Availability Zones, can scale using multiple disk devices, and can utilize tiered storage to archive older data on more cost-effective storage.</p>
<p>However, we recognize that durability may not always be necessary for all applications. Currently, <strong>EloqKV</strong>, persistency can be turned on and off through a configuration. In a future release, <strong>EloqKV</strong> durability can be enabled on a per-database basis so that durable and non-durable workload can co-exists on a single <strong>EloqKV</strong> instance. When durability is disabled, <strong>EloqKV</strong> avoids the overhead associated with durability, delivering uncompromised performance as demonstrated in a <a class="" href="https://www.eloqdata.com/blog/2024/08/17/benchmark-single-node">previous blog post</a>. In this blog, we evaluate <strong>EloqKV</strong> with durability enabled.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="comparing-with-kvrocks">Comparing with Kvrocks<a href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog#comparing-with-kvrocks" class="hash-link" aria-label="Direct link to Comparing with Kvrocks" title="Direct link to Comparing with Kvrocks" translate="no">​</a></h3>
<p>In the first experiment, we compare <strong>EloqKV</strong> with Apache <a href="https://kvrocks.apache.org/" target="_blank" rel="noopener noreferrer" class="">Kvrocks</a>, a Redis-compatible NoSQL database that supports persistence. We evaluate the performance of <strong>EloqKV</strong> and Kvrocks under write-intensive and mixed workloads. To ensure data durability, we enable fsync Write-Ahead Logging (WAL) for both databases. For <strong>EloqKV</strong>, both the transaction service and log service are deployed on the same node (c7gi.8xlarge). To fully utilize available disk IO, we start two LogService processes to write WAL logs in <strong>EloqKV</strong>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="hardware-and-software-specification">Hardware and Software Specification<a href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog#hardware-and-software-specification" class="hash-link" aria-label="Direct link to Hardware and Software Specification" title="Direct link to Hardware and Software Specification" translate="no">​</a></h3>
<p><strong>Server Machine:</strong></p>
<table><thead><tr><th>Service type</th><th>Node type</th><th>Node count</th><th>Local SSD</th><th>EBS gp3 volume</th></tr></thead><tbody><tr><td>Kvrocks</td><td>c7gd.8xlarge</td><td>1</td><td>1 x 1900GB NVME</td><td>1</td></tr><tr><td>EloqKV</td><td>c7gd.8xlarge</td><td>1</td><td>1 x 1900GB NVME</td><td>1</td></tr></tbody></table>
<p>For <strong>EloqKV</strong>, we enable persistent storage and turn on WAL (Write-Ahead Logging).</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain"># </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">set</span><span class="token plain"> it to on to turn on persistent storage</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">enable_data_store</span><span class="token operator">=</span><span class="token plain">on</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"># </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">set</span><span class="token plain"> it to on to turn on </span><span class="token constant" style="color:rgb(189, 147, 249)">WAL</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">enable_wal</span><span class="token operator">=</span><span class="token plain">on</span><br></span></code></pre></div></div>
<p>For Kvrocks, we mainly changed two configuration options.</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain"># </span><span class="token maybe-class-name">If</span><span class="token plain"> yes</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> the write will be flushed </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> the operating system</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"># buffer cache before the write is considered complete</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"># </span><span class="token maybe-class-name">If</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">this</span><span class="token plain"> flag is enabled</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> writes will be slower</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"># </span><span class="token maybe-class-name">If</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">this</span><span class="token plain"> flag is disabled</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> and the machine crashes</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> some recent</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"># writes may be lost</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">  </span><span class="token property-access maybe-class-name">Note</span><span class="token plain"> that </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> it is just the process that</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"># </span><span class="token function" style="color:rgb(80, 250, 123)">crashes</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">i</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token property-access">e</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> the machine does not reboot</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> no writes will be</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"># lost even </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> sync</span><span class="token operator">==</span><span class="token boolean">false</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">#</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"># </span><span class="token maybe-class-name">Default</span><span class="token operator">:</span><span class="token plain"> no</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"># rocksdb</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token property-access">write_options</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token property-access">sync</span><span class="token plain"> no</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">rocksdb</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token property-access">write_options</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token property-access">sync</span><span class="token plain"> yes</span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"># </span><span class="token maybe-class-name">The</span><span class="token plain"> number </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">of</span><span class="token plain"> worker's threads</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> increase or decrease would affect the </span><span class="token dom variable" style="color:rgb(189, 147, 249);font-style:italic">performance</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain"># workers </span><span class="token number">8</span><span class="token plain"></span><br></span><span class="token-line" style="color:#F8F8F2"><span class="token plain">workers </span><span class="token number">24</span><br></span></code></pre></div></div>
<p>Disk performance plays a critical role in write-intensive workloads. Therefore, we conducted benchmarks using both local SSDs and Elastic Block Store (EBS). Local SSDs offer low latency and high IOPS, making them ideal for high-performance needs. However, in a cloud setup data on local SSD may get lost if the virtual machine (VM) is stopped. On the other hand, EBS provides high availability, allowing the volume to be attached to a new VM if the original VM fails. Moreover, EBS is elastic, allowing precise control over number of volumes and their sizes. In our case, a 50GB <a href="https://aws.amazon.com/ebs/volume-types/" target="_blank" rel="noopener noreferrer" class="">EBS gp3</a> volume is plenty for our WAL needs. Such a volume only cost $4 per month while providing 3000 IOPS and 125 MB/s throughput. Given the distinct advantages and limitations of local SSDs and EBS, we conducted our experiments with both.</p>
<p>We run <code>memtier_benchmark</code> with the following configuration:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">memtier_benchmark </span><span class="token operator">-</span><span class="token plain">t $thread_num </span><span class="token operator">-</span><span class="token plain">c $client_num </span><span class="token operator">-</span><span class="token plain">s $server_ip </span><span class="token operator">-</span><span class="token plain">p $server_port </span><span class="token operator">--</span><span class="token plain">distinct</span><span class="token operator">-</span><span class="token plain">client</span><span class="token operator">-</span><span class="token plain">seed </span><span class="token operator">--</span><span class="token plain">ratio</span><span class="token operator">=</span><span class="token plain">$ratio </span><span class="token operator">--</span><span class="token plain">key</span><span class="token operator">-</span><span class="token plain">prefix</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"kv_"</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">key</span><span class="token operator">-</span><span class="token plain">minimum</span><span class="token operator">=</span><span class="token number">1</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">key</span><span class="token operator">-</span><span class="token plain">maximum</span><span class="token operator">=</span><span class="token number">5000000</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">random</span><span class="token operator">-</span><span class="token plain">data </span><span class="token operator">--</span><span class="token plain">data</span><span class="token operator">-</span><span class="token plain">size</span><span class="token operator">=</span><span class="token number">128</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">hide</span><span class="token operator">-</span><span class="token plain">histogram </span><span class="token operator">--</span><span class="token plain">test</span><span class="token operator">-</span><span class="token plain">time</span><span class="token operator">=</span><span class="token number">300</span><br></span></code></pre></div></div>
<ul>
<li class="">
<p><code>-t</code>: Number of threads for parallel execution, which we set to 80.</p>
</li>
<li class="">
<p><code>-c</code>: Number of clients per thread. We set it to 5, 10, 20, 40 to evaluate different concurrency levels. This resulted in total concurrency values of 400, 800, 1600, and 3200, calculated as <code>thread_num × client_num</code>.</p>
</li>
<li class="">
<p><code>--ratio</code>: Set:Get ratio is set to 1:0 for write-only workload, and 1:10 for mixed workload.</p>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="results">Results<a href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog#results" class="hash-link" aria-label="Direct link to Results" title="Direct link to Results" translate="no">​</a></h4>
<p>Below are the results of the write-only workload.</p>
<p>X-axis: Represents the varying concurrencies (<code>thread_num × client_num</code>), simulating different levels of concurrent database access.</p>
<p>Left Y-axis: Throughput in Thousand OPS (Operations Per Second).</p>
<p>Right Y-axis: Average latency in milli seconds (ms).</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/eloqkv_kvrocks_set-b7535825bcacbbf9b34a566ba4dfd018.png" alt="EloqKV vs Kvrocks Set" style="cursor:pointer;max-width:100%"></div></div></div>
<p><strong>EloqKV</strong> significantly outperforms Kvrocks on both EBS and local SSD. On EBS, <strong>EloqKV</strong> achieves a write throughput that is 10 times higher than Kvrocks, while on local SSD, it is 2-4 times faster. This performance improvement is due to <strong>EloqKV</strong>'s architecture, which decouples transaction and log services, allowing multiple log workers to write Write-Ahead Logs (WAL) and perform fsync operations in parallel, thereby enhancing overall throughput. Additionally, <strong>EloqKV</strong> maintains significantly lower latency compared to Kvrocks, even under high concurrency.</p>
<p>Below are the results of the mixed workload.</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/eloqkv_kvrocks_setget-c984a007a0bb950415e2e310bd1a2358.png" alt="EloqKV vs Kvrocks SetGet" style="cursor:pointer;max-width:100%"></div></div></div>
<p>Results show that <strong>EloqKV</strong> outperforms Kvrocks on mixed workloads as well. <strong>EloqKV</strong> maintains a read latency of less than 1 ms even under a heavy mixed workload with nearly 900K OPS. In contrast, Kvrocks on EBS exhibits significantly higher latencies, with both read and write latencies exceeding 10 ms even at relatively low concurrency, and rising to over 50 ms as concurrency increases. Even on local SSDs, Kvrocks' read latency remains much higher than that of <strong>EloqKV</strong>. This demonstrates that <strong>EloqKV</strong> can sustain low read latency even when the cluster is under a heavy write workload.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="experiment-ii-scaling-disks-of-wal">Experiment II: Scaling Disks of WAL<a href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog#experiment-ii-scaling-disks-of-wal" class="hash-link" aria-label="Direct link to Experiment II: Scaling Disks of WAL" title="Direct link to Experiment II: Scaling Disks of WAL" translate="no">​</a></h3>
<p><strong>EloqKV</strong>'s decoupled WAL log service can deploy multiple log workers writing WAL logs to multiple disks. Since WAL logs can be truncated once a checkpoint is completed, the required disk size is often quite small. Kvrocks does not support writing redo logs across multiple disks, so this experiment is conducted with <strong>EloqKV</strong> only.</p>
<p><strong>Server Machine:</strong></p>
<table><thead><tr><th>Service type</th><th>Node type</th><th>Node count</th><th>EBS gp3 volume</th></tr></thead><tbody><tr><td>EloqKV Log</td><td>c7g.12xlarge</td><td>1</td><td>up to 10</td></tr><tr><td>client - Memtier</td><td>c6gn.8xlarge</td><td>1</td><td>0</td></tr></tbody></table>
<p>We benchmarked <strong>EloqKV</strong> with different numbers of WAL disks and varying thread counts using the following command:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">memtier_benchmark </span><span class="token operator">-</span><span class="token plain">t $thread_num </span><span class="token operator">-</span><span class="token plain">c $client_num </span><span class="token operator">-</span><span class="token plain">s $server_ip </span><span class="token operator">-</span><span class="token plain">p $server_port </span><span class="token operator">--</span><span class="token plain">distinct</span><span class="token operator">-</span><span class="token plain">client</span><span class="token operator">-</span><span class="token plain">seed </span><span class="token operator">--</span><span class="token plain">ratio</span><span class="token operator">=</span><span class="token number">1</span><span class="token operator">:</span><span class="token number">0</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">key</span><span class="token operator">-</span><span class="token plain">prefix</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"kv_"</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">key</span><span class="token operator">-</span><span class="token plain">minimum</span><span class="token operator">=</span><span class="token number">1</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">key</span><span class="token operator">-</span><span class="token plain">maximum</span><span class="token operator">=</span><span class="token number">5000000</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">random</span><span class="token operator">-</span><span class="token plain">data </span><span class="token operator">--</span><span class="token plain">data</span><span class="token operator">-</span><span class="token plain">size</span><span class="token operator">=</span><span class="token number">128</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">hide</span><span class="token operator">-</span><span class="token plain">histogram </span><span class="token operator">--</span><span class="token plain">test</span><span class="token operator">-</span><span class="token plain">time</span><span class="token operator">=</span><span class="token number">300</span><br></span></code></pre></div></div>
<ul>
<li class=""><code>-t</code>: Number of threads for parallel execution, which we have set to a fixed value of 80.</li>
<li class=""><code>-c</code>: Number of clients per thread. We configured it to 40, 60 and 80, this resulted in total concurrency values of 3200, 4800 and 6400, calculated as <code>thread_num × client_num</code>.</li>
</ul>
<p>In this experiment, we have higher concurrency compared with previous experiments due to increased latency caused by seperating LogService from TxService.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="results-1">Results<a href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog#results-1" class="hash-link" aria-label="Direct link to Results" title="Direct link to Results" translate="no">​</a></h4>
<p>The following graph shows how disk count impacts the performance of <strong>EloqKV</strong>.</p>
<p>X-axis: Represents the varying thread numbers employed during the benchmark, simulating different levels of concurrent database access.</p>
<p>Left Y-axis: The throughput in Thousand Operations Per Second (KOPS)</p>
<p>Right Y-axis: The average Latency in ms.</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/eloqkv_scale_disk_set-7d9b1557c7b95b04fa97983f54120ed1.png" alt="EloqKV Disk Scale Set" style="cursor:pointer;max-width:100%"></div></div></div>
<p>We can observe that as the number of disks increases, the throughput scales near linearly when the disk count is 1, 2, and 4, with a corresponding decrease in latency. Adding even more disks continues to boost throughput, but at a slower rate. For 6 and 8 disks, the throughput levels off and remains nearly the same even under high concurrency.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="experiment-iii-scaling-up-txserver">Experiment III: Scaling Up TxServer<a href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog#experiment-iii-scaling-up-txserver" class="hash-link" aria-label="Direct link to Experiment III: Scaling Up TxServer" title="Direct link to Experiment III: Scaling Up TxServer" translate="no">​</a></h3>
<p>As observed previously, throughput does not increase further when the number of disks exceeds six. This indicates that logging is no longer the bottleneck; to achieve even higher throughput, scaling up the CPU in TxServer could be the next step. Obviously, scaling-out could be another option, but we will leave that to another blog.</p>
<p><strong>Server Machine:</strong></p>
<table><thead><tr><th>Service type</th><th>Node type</th><th>Node count</th><th>EBS gp3 volume</th></tr></thead><tbody><tr><td>EloqKV TX 8x</td><td>c7gi.8xlarge</td><td>1</td><td>1</td></tr><tr><td>EloqKV TX 12x</td><td>c7g.12xlarge</td><td>1</td><td>1</td></tr></tbody></table>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="result">Result<a href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog#result" class="hash-link" aria-label="Direct link to Result" title="Direct link to Result" translate="no">​</a></h4>
<p>The following graph shows how the number of CPU cores affects the performance of <strong>EloqKV</strong> as we have many disks providing logging IOPs.</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/eloqkv_scale_cpu_set-b368ff4cde2b0159aa61b1f4e7a373b1.png" alt="EloqKV CPU Scale Set" style="cursor:pointer;max-width:100%"></div></div></div>
<p>Adding more disks beyond 8 on a 32-vcore CPU does not significantly increase throughput. By scaling up the CPU of TxServer from 32 to 48 vcores, we can achieve a notable increase in throughput and a decrease in latency. Under heavy concurrency, latency decreases significantly from 10ms to under 8ms when more CPU cores are added.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="analysis-and-conclusion">Analysis and Conclusion<a href="https://www.eloqdata.com/blog/2024/08/25/benchmark-txlog#analysis-and-conclusion" class="hash-link" aria-label="Direct link to Analysis and Conclusion" title="Direct link to Analysis and Conclusion" translate="no">​</a></h3>
<p>In this blog, we evaluate <strong>EloqKV</strong> and show its performance when data durability is strongly enforced. On a plain low end server, <strong>EloqKV</strong> can easily sustain over 200,000 writes per second with acceptable latency. While this is lower than the pure in-memory cache performance highlighted in our <a class="" href="https://www.eloqdata.com/blog/2024/08/17/benchmark-single-node">previous blog</a>, it is still quite suitable for many real-world applications. Notice that this performance number is not much different from what a <em>single-process</em> Redis server can achieve on the same hardware in pure memory mode.</p>
<p>Additionally, we showcase <strong>EloqKV</strong>'s architectural advantage by scaling the LogService to enhance write throughput while maintaining resources used by the TxService. This capability is made possible by our <a class="" href="https://www.eloqdata.com/blog/2025/07/14/technology">Data Substrate</a> architecture. Considering a scenario where, despite high volume of updates, the total data volume can easily fit on a single server's memory (an example is high-frequency trading). <strong>EloqKV</strong>'s full scalability is crucial to support such applications without wasting valuable resources.</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Benchmark" term="Benchmark"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[EloqKV Clustering]]></title>
        <id>https://www.eloqdata.com/blog/2024/08/22/benchmark-cluster</id>
        <link href="https://www.eloqdata.com/blog/2024/08/22/benchmark-cluster"/>
        <updated>2024-08-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Benchmarking EloqKV cluster performance and scalability.]]></summary>
        <content type="html"><![CDATA[<p>In our <a class="" href="https://www.eloqdata.com/blog/2024/08/17/benchmark-single-node">previous blog</a>, we benchmarked <strong>EloqKV</strong> to evaluate it as an in-memory cache, focusing on single-node performance. In this blog, we shift our attention to <strong>Eloq</strong> clustering and discuss why it provides a fundmentally better solution.</p>
<p>In this blog, we evaluate <em>small scale</em> clusters to show the behavior of <strong>EloqKV</strong> accross servers. Scalability in a larger scale cluster with different number of servers will be evaluated at a later blog. All benchmarks were conducted on AWS (region: us-east-1) EC2 instances running Ubuntu 22.04. Workloads were generated using the <a href="https://github.com/RedisLabs/memtier_benchmark" target="_blank" rel="noopener noreferrer" class="">memtier-benchmark</a> tool.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="kv-store-clustering">KV Store Clustering<a href="https://www.eloqdata.com/blog/2024/08/22/benchmark-cluster#kv-store-clustering" class="hash-link" aria-label="Direct link to KV Store Clustering" title="Direct link to KV Store Clustering" translate="no">​</a></h2>
<p>For most real-world applications that require a key-value (KV) cache, even single-threaded Redis is often <a href="https://medium.com/hprog99/why-is-redis-incredibly-fast-unpacking-the-secrets-of-its-speed-f10f051b3f23" target="_blank" rel="noopener noreferrer" class="">sufficiently fast</a>. In fact, the limiting factor is usually memory capacity. As a result, KV caches are commonly deployed in <em>cluster mode</em>.</p>
<p>Most KV caches support horizontal scaling and operate in cluster mode by partitioning data into slots and distributing them across shards. In a horizontally scaled cluster, performance and capacity can generally scale almost linearly, though load imbalances and failure cases can introduce challenges. To achieve this scalability, developers must understand the concepts of <em>slots</em>, <em>shards</em>, and <em>tags</em>. And the so called <em>cluster-aware</em> clients need to know the cluster topology to direct requests to the correct server.</p>
<p>Clustering for KV cache is fraught with pitfalls, not the least is because nodes in a cluster may fail. External tools such as <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/" target="_blank" rel="noopener noreferrer" class="">Sentinel</a>, <a href="https://github.com/twitter/twemproxy" target="_blank" rel="noopener noreferrer" class="">Twemproxy</a>, <a href="https://www.dragonflydb.io/docs/managing-dragonfly/cluster-mode" target="_blank" rel="noopener noreferrer" class="">Dragonfly Cloud services</a> are often required to monitor cluster health and handle failovers. How these tools interact with clients are often not well specified. Additionally, a KV cache cluster behaves differently than a single node server. For instance, the "MULTI / EXEC" commands do not work in a clustered environment.</p>
<p>The fundmental issue is that many KV caches were initially designed as single-node servers, with clustering added later as a bolt-on feature. For example, <a href="https://en.wikipedia.org/wiki/Redis" target="_blank" rel="noopener noreferrer" class="">Redis</a> was released on May, 2009, while Sentinel support arrived in Redis 2.8 in December 2013. Redis Cluster became a stable feature only with Redis 3.0 in April 2015. The same can be said for many other KV stores.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-eloqkv-clustering-is-different">Why EloqKV Clustering is Different<a href="https://www.eloqdata.com/blog/2024/08/22/benchmark-cluster#why-eloqkv-clustering-is-different" class="hash-link" aria-label="Direct link to Why EloqKV Clustering is Different" title="Direct link to Why EloqKV Clustering is Different" translate="no">​</a></h2>
<p><strong>EloqKV</strong> eliminates these issues by being designed as a fully distributed transactional database from the start. While it can function as a single-node server and use various clustering tools for horizontal scalability, it is also capable of operating as a cluster without exposing cluster details to clients. Clients can interact with any node in an <strong>EloqKV</strong> cluster without worrying about which server holds the key, how data is sharded, if there’s a failure in the cluster, or whether the cluster is reconfiging to dynamically increase or reduce capacity. This simplifies the development process and reduces the need for cluster-aware clients.</p>
<p>Obviously, making this process transparent to the client and shielding cluster details has cost. In particular, a node redirecting requests will incur an extra network round trip. Therefore, we do allow enabling a flag so that servers follow the Redis cluster protocol and do not redirect requests when data requested is not local. In this case, <em>smart</em> Redis clients will operate as expected. Unlike most KV cache clusters, <strong>EloqKV</strong> clusters are strongly consistent. For example, if a node is dropped from the cluster due to a network partition, it is aware of the situation and can refuse to serve external requests until it rejoins.</p>
<p>We compare the performance of a single-node <strong>EloqKV</strong> instance with that of an <strong>EloqKV</strong> cluster to evaluate the cost of transparent redirection. In this assessment, <strong>EloqKV</strong> operates in pure memory mode, with persistent storage and transactional features disabled.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="hardware-and-software-specification">Hardware and Software Specification<a href="https://www.eloqdata.com/blog/2024/08/22/benchmark-cluster#hardware-and-software-specification" class="hash-link" aria-label="Direct link to Hardware and Software Specification" title="Direct link to Hardware and Software Specification" translate="no">​</a></h2>
<p><strong>Server Machine:</strong></p>
<table><thead><tr><th>Service type</th><th>Node type</th><th>Node count</th></tr></thead><tbody><tr><td>EloqKV 0.7.4</td><td>c7g.8xlarge</td><td>1</td></tr><tr><td>EloqKV 0.7.4 Cluster</td><td>c7g.8xlarge</td><td>3</td></tr><tr><td>Client - Memtier</td><td>c6gn.8xlarge</td><td>3</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="experiment">Experiment:<a href="https://www.eloqdata.com/blog/2024/08/22/benchmark-cluster#experiment" class="hash-link" aria-label="Direct link to Experiment:" title="Direct link to Experiment:" translate="no">​</a></h2>
<p>We conducted performance benchmarks on a single-node <strong>EloqKV</strong> database under varying read-write ratios, utilizing a single <code>memtier-benchmark</code> client. The command used for this benchmarking is as follows:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">memtier_benchmark </span><span class="token operator">-</span><span class="token plain">t </span><span class="token number">32</span><span class="token plain"> </span><span class="token operator">-</span><span class="token plain">c </span><span class="token number">16</span><span class="token plain"> </span><span class="token operator">-</span><span class="token plain">s $server_ip </span><span class="token operator">-</span><span class="token plain">p $server_port </span><span class="token operator">--</span><span class="token plain">distinct</span><span class="token operator">-</span><span class="token plain">client</span><span class="token operator">-</span><span class="token plain">seed </span><span class="token operator">--</span><span class="token plain">ratio</span><span class="token operator">=</span><span class="token plain">$ratio </span><span class="token operator">--</span><span class="token plain">key</span><span class="token operator">-</span><span class="token plain">prefix</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"kv_"</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">key</span><span class="token operator">-</span><span class="token plain">minimum</span><span class="token operator">=</span><span class="token number">1</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">key</span><span class="token operator">-</span><span class="token plain">maximum</span><span class="token operator">=</span><span class="token number">5000000</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">random</span><span class="token operator">-</span><span class="token plain">data </span><span class="token operator">--</span><span class="token plain">data</span><span class="token operator">-</span><span class="token plain">size</span><span class="token operator">=</span><span class="token number">128</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">hide</span><span class="token operator">-</span><span class="token plain">histogram </span><span class="token operator">--</span><span class="token plain">test</span><span class="token operator">-</span><span class="token plain">time</span><span class="token operator">=</span><span class="token number">300</span><br></span></code></pre></div></div>
<p>To assess the performance of a three-node <strong>EloqKV</strong> cluster, we utilized three memtier-benchmark clients in both regular and smart client modes. In regular client mode, users interact with the <strong>EloqKV</strong> cluster as if it were a single node without needing to consider where the keys are stored. The same <code>memtier-benchmark</code> command used for a single-node setup is applied to each regular client, with each client connecting to a different <strong>EloqKV</strong> node in the cluster.</p>
<p>For smart client mode, we ran memtier-benchmark in cluster mode with varying read-write ratios, using the following configuration:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#F8F8F2"><span class="token plain">memtier_benchmark </span><span class="token operator">-</span><span class="token plain">t </span><span class="token number">32</span><span class="token plain"> </span><span class="token operator">-</span><span class="token plain">c </span><span class="token number">16</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">cluster</span><span class="token operator">-</span><span class="token plain">mode </span><span class="token operator">-</span><span class="token plain">s $server_ip1 </span><span class="token operator">-</span><span class="token plain">p $server_port1 </span><span class="token operator">-</span><span class="token plain">s $server_ip2 </span><span class="token operator">-</span><span class="token plain">p $server_port2 </span><span class="token operator">-</span><span class="token plain">s $server_ip3 </span><span class="token operator">-</span><span class="token plain">p $server_port3 </span><span class="token operator">--</span><span class="token plain">distinct</span><span class="token operator">-</span><span class="token plain">client</span><span class="token operator">-</span><span class="token plain">seed </span><span class="token operator">--</span><span class="token plain">ratio</span><span class="token operator">=</span><span class="token plain">$ratio </span><span class="token operator">--</span><span class="token plain">key</span><span class="token operator">-</span><span class="token plain">prefix</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"kv_"</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">key</span><span class="token operator">-</span><span class="token plain">minimum</span><span class="token operator">=</span><span class="token number">1</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">key</span><span class="token operator">-</span><span class="token plain">maximum</span><span class="token operator">=</span><span class="token number">5000000</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">random</span><span class="token operator">-</span><span class="token plain">data </span><span class="token operator">--</span><span class="token plain">data</span><span class="token operator">-</span><span class="token plain">size</span><span class="token operator">=</span><span class="token number">128</span><span class="token plain"> </span><span class="token operator">--</span><span class="token plain">hide</span><span class="token operator">-</span><span class="token plain">histogram </span><span class="token operator">--</span><span class="token plain">test</span><span class="token operator">-</span><span class="token plain">time</span><span class="token operator">=</span><span class="token number">300</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="results">Results<a href="https://www.eloqdata.com/blog/2024/08/22/benchmark-cluster#results" class="hash-link" aria-label="Direct link to Results" title="Direct link to Results" translate="no">​</a></h3>
<p>Below are the results of comparing the throughput of a single-node <strong>EloqKV</strong> instance with that of a three-node <strong>EloqKV</strong> cluster across various workloads. For the three-node cluster, we conducted benchmarks using both a regular client and a smart client. When using the regular client, <strong>EloqKV</strong> automatically redirects requests to other nodes if the requested key is not stored locally. In contrast, with the smart client, all requests are sent directly to the node that holds the key, based on the cached cluster configuration within the smart client.</p>
<p>X-axis: Represents the different workload types (read/write/mixed) used in the benchmark, simulating a range of real-world scenarios.</p>
<p>Y-axis: Measures the OPS (Operations Per Second).</p>
<div align="center"><div style="width:720px;text-align:center"><div style="position:relative;display:inline-block"><img src="https://www.eloqdata.com/assets/images/eloqkv_scale-7e4bec45c302e320f41d96b796f20b17.png" alt="EloqKV Scale" style="cursor:pointer;max-width:100%"></div></div></div>
<p>When using the regular client, the three-node <strong>EloqKV</strong> cluster has slightly lower throughput than the single-node instance due to the added network round trips and scheduling overhead caused by redirection. Despite this, the cluster still achieves over <strong>one million operations per second (OPS)</strong>. Importantly, this requires no changes to application code, allowing developers to easily scale and overcome memory capacity limits without modifying their code or relying on smart clients.</p>
<p>Notice that a single Redis server can only support around 200K OPS (see our <a class="" href="https://www.eloqdata.com/blog/2024/08/17/benchmark-single-node">previous blog</a> for a detailed analysis), thus we believe that <strong>EloqKV</strong> in cluster mode is already fast enough to support most application scenarios without relying on specially designed smart clients. Still, <strong>EloqKV</strong> does support smart clients. As expected, with a smart client, the three-node <strong>EloqKV</strong> cluster supports near three times the throughput of a single-node instance across various workloads. This highlights <strong>EloqKV</strong>'s compatibility with smart clients.</p>]]></content>
        <author>
            <name>EloqData</name>
        </author>
        <category label="Benchmark" term="Benchmark"/>
    </entry>
</feed>