<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
>
<channel>
  <title>Momento</title>
  <atom:link href="https://www.gomomento.com/feed" rel="self" type="application/rss+xml" />
  <link>https://www.gomomento.com/</link>
  <description>An enterprise-ready serverless platform for caching and pub/sub</description>
  <lastBuildDate>Wed, 02 Sep 2026 12:00:00 GMT</lastBuildDate>
  <language>en</language>
  <sy:updatePeriod>hourly</sy:updatePeriod>
  <sy:updateFrequency>1</sy:updateFrequency>
  <image>
    <url>https://www.gomomento.com/wp-content/uploads/2024/06/cropped-favicon-green-32x32.png</url>
    <title>Momento</title>
    <link>https://www.gomomento.com/</link>
    <width>32</width>
    <height>32</height>
  </image>
  <item>
    <title>Momento Cache Cluster and Flex enter limited preview</title>
    <link>https://www.gomomento.com/blog/momento-cache-cluster-and-flex-limited-preview/</link>
  <dc:creator><![CDATA[Dylan Abraham]]></dc:creator>
    <pubDate>Wed, 02 Sep 2026 12:00:00 GMT</pubDate>
  <category><![CDATA[Valkey]]></category>
  <category><![CDATA[Product Update]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/blog/momento-cache-cluster-and-flex-limited-preview/</guid>
    <description><![CDATA[<p>Momento Cache puts high-performance Valkey at your fingertips. Cluster provides direct control over topology, while Flex automatically optimizes resources.</p>]]></description>
    <content:encoded><![CDATA[<p><img src="https://www.gomomento.com/assets/content/blog/momento-cache-cluster-and-flex-limited-preview/hero.png" alt="Momento squirrel flying over a gateway connected to Cluster and Flex capacity."></p>
<p>We’re opening a limited preview of the new <strong>Cluster</strong> and <strong>Flex</strong> configurations for <strong>Momento Cache</strong>. This release conveniently packages the technology and operating experience that power some of the largest Valkey clusters in the world.</p>
<p>Momento Cache is built for fast-moving teams who want optimal Valkey performance without the hassle of babysitting infrastructure. Cluster capacity and Flex capacity bring single-tenant, fully-managed resources to this self-service infrastructure platform.</p>
<p>Cluster provides direct control over instance type, shards, replicas, and availability zones. Flex automatically optimizes resources within specified bounds. Both were designed for workloads that need <strong>stronger isolation</strong>, <strong>more control</strong>, and a <strong>higher performance ceiling</strong> than Momento Cache’s Serverless configuration.</p>
<h2 id="avoiding-the-trap-of-operational-creep">Avoiding the trap of operational creep</h2>
<p>In the AI era, it’s trivial to stand up basic infrastructure at near-zero cost. We’ve all been there: it’s fast, it’s easy, it mostly works. It’s the right solution when you need to ship.</p>
<p>Then, growth hits. Traffic changes shape. Memory fills unevenly. A shard needs to move. A primary fails. Clients stampede. A zero-day patch lands at midnight. Suddenly, <strong>operational creep</strong> has consumed the time and the token budget that you wanted to spend on building.</p>
<p>And the problems only multiply as you add more features, products, and services. Soon your entire team is stuck fighting against infrastructure as latency, cost, and complexity steadily creep up and to the right.</p>
<h2 id="fast-reliable-efficient---pick-all-three">Fast, reliable, efficient - pick all three</h2>
<p>The Momento platform powers critical features for millions of users around the world at companies like Capcom, Coinbase, Paramount, and Snap. Now, Momento Cache puts the full power and flexibility of Valkey at your fingertips, packaging up hard-won production lessons into a streamlined service.</p>
<p>The new Cluster and Flex configurations help you to tailor a Valkey deployment to fit your specific needs. Momento seamlessly operates the lifecycle behind that system: provisioning, health, failover, rolling topology changes, version upgrades, and security patches.</p>
<p>Whether you’re pushing one thousand or one million requests per second, Momento delivers unrivaled performance and resource utilization. As you grow, your infrastructure grows alongside you. For companies with a mature platform org, Momento Cache can even be deployed in a BYOC configuration, as part of your internal developer platform.</p>
<h2 id="transparent-pricing">Transparent pricing</h2>
<p>Momento Cache pricing is designed to be simple, predictable, and cost-effective. Pricing in us-east-1:</p>
<ul>
<li><strong>Flex</strong> starts at $13 per GB-month of physical Valkey storage</li>
<li><strong>Cluster</strong> applies a 25% surcharge to the list price for every deployed instance</li>
<li><strong>Data transfer</strong> includes 200 GB of ingress + egress each month, then $0.05/GB</li>
</ul>
<p>Momento Cache supports rapid autoscaling within a specified capacity range, making it easy to reduce the cost of idle resources.</p>
<h2 id="designing-for-scale">Designing for scale</h2>
<p>Momento Cache employs a two-tiered architecture that significantly improves reliability and efficiency at scale. Each valkey cluster sits behind a gateway that handles the hard traffic problems like hot keys and connection storms before they hit the data layer.</p>
<p>The gateway exposes a RESP endpoint, so Momento Cache is compatible with all standard Valkey and Redis clients. It masks the underlying cluster, presenting a single stable endpoint across any topology changes.</p>
<pre><code class="language-mermaid">block
  app("Redis or Valkey\nclient"):3
  space
  gateway("gateway"):3
  space
  pool("Valkey cluster"):3

  app --> gateway
  gateway --> pool
</code></pre>
<p>The gateway is optimized to quickly process TLS, auth, rate limits, and other traffic management concerns at high concurrency and high throughput. It multiplexes client traffic across a pool of warm connections to the Valkey cluster. This reduces connection latency, and enables advanced capabilities like request coalescing.</p>
<p>The result is an efficient system with deliberate separation of responsibilities. The gateway absorbs traffic concerns, while Valkey nodes focus their resources on handling data.</p>
<h2 id="get-started-with-momento-cache">Get started with Momento Cache</h2>
<p>Try out Momento Cache in a few short steps with the <a href="https://github.com/momentohq/momento-cli">Momento CLI</a>. While the service is still in preview, you’ll also need to request access via the web console.</p>
<p>First, create an API key in the <a href="https://console.gomomento.com/">Momento console</a>, copy the endpoint for your region, and configure the default CLI profile:</p>
<pre><code class="language-sh"># paste the api key and endpoint when prompted
momento configure
</code></pre>
<p>Then, create a Capacity Pool. Be sure to provide valid zone IDs for your region:</p>
<pre><code class="language-sh">momento preview pool create \
  --name example-pool \
  --capacity-gib 32..128 \
  --replicas-per-shard 1..2 \
  --zones use1-az1,use1-az2

momento preview pool describe --name example-pool
</code></pre>
<p>Once the Capacity Pool status is <code>active</code>, you can create a Database:</p>
<pre><code class="language-sh">momento preview database create \
  --name example-db \
  --pool-name example-pool
</code></pre>
<p>Back in the console, open the pool’s <strong>Databases</strong> tab and copy the regional RESP endpoint. Connect any Valkey or Redis client in standalone mode to this endpoint over TLS on port <code>6379</code>. The Database name is the username, and your Momento API key or token is the password. Here, we’ll demonstrate with the official <a href="https://valkey.io/topics/cli/">valkey cli</a>:</p>
<pre><code class="language-sh">valkey-cli -h &#x3C;resp-endpoint> -p 6379 --tls \
  --user example-db --pass &#x3C;momento-api-key>

> SET example-key "ready"
OK
> GET example-key
"ready"
</code></pre>
<p>Congratulations! You now have a high-performance Valkey cluster ready to go.</p>
<p>Next, check out the <a href="https://docs.momentohq.com/product/cache/">docs</a> to learn more about Momento Cache’s capabilities. Or, if you want to push your cache to the limit, load up <a href="https://github.com/cachecannon/cachecannon">cachecannon</a> on a <code>c7g.xlarge</code> instance in the same region and zone as your Database!</p>
<h2 id="up-next">Up next</h2>
<p>This launch begins the next chapter for Momento Cache. Stay tuned as we port more features from our enterprise services into Momento Cache, including VPC peering and S3 integration!</p>
<p>We’re looking for feedback from teams operating demanding Valkey workloads as we refine the product. If you’re building a fast-growing product, operating a large Valkey or Redis cluster, designing an internal caching platform, or helping teams adopt Valkey, we would love to hear what you need next and where we can help out.</p>
<p>We’re grateful to the engineers and partners who turned years of demanding operating experience into a service anyone can start using today. Special thanks to <strong>Dylan Abraham</strong> and <strong>Jason LaPier</strong> for leading the development effort!</p>]]></content:encoded>
  </item>
  <item>
    <title>Stop counting indexes</title>
    <link>https://www.gomomento.com/blog/stop-counting-indexes/</link>
  <dc:creator><![CDATA[Allen Helton]]></dc:creator>
    <pubDate>Thu, 27 Aug 2026 09:00:00 GMT</pubDate>
  <category><![CDATA[AI/ML]]></category>
  <category><![CDATA[Valkey]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/blog/stop-counting-indexes/</guid>
    <description><![CDATA[<p>Fifty extra indexes don't slow down your writes, but one vector field slows them down 11x. I wanted to figure out why valkey-search behaves this way, and it wasn't what I expected.</p>]]></description>
    <content:encoded><![CDATA[<p>Free text search is a beast. Sometimes a user will type in a description of what they’re looking for, like “wireless earbuds under $100,” and other times they’ll copy/paste a SKU for an item they’re replacing. Both are completely valid use cases, but the former requires a vector search and the latter relies on lexical matching. I’ve been spending a lot of time in <a href="https://valkey.io/topics/search/">valkey-search</a> trying to handle both. As you can probably imagine, one index doesn’t cut it, but two indexes on the same keys can.</p>
<p>But doubling the number of indexes I was using made me nervous. I didn’t know what impact that would have on performance. I’m using Valkey because I needed ultra-low latency, I didn’t want to shoot myself in the foot trying to be clever.</p>
<p>And yes, going from one to two indexes is probably not a big deal. But indexes have a way of piling up. You create one to filter products by category, then add price to it a month later, then the search team wants a vector field, then the payment team indexes their keys, then fulfillment indexes theirs. Before you know it, there are a dozen <code>FT.CREATE</code> statements in the repo with no single owner.</p>
<p>My instinct tells me that performance scales inversely with the count. Every index in valkey-search subscribes to a key prefix. When you write a matching key, that index gets an entry in a mutation queue, and your client stays blocked until every entry is processed. That’s what gives you read-after-write consistency. If you have a dozen indexes, you also have a dozen entries, and a dozen times the wait. Right?</p>
<p>So I built a benchmark to see how much additional indexes cost in valkey-search. And I discovered I was very wrong.</p>
<h2 id="the-setup">The setup</h2>
<p>The server was an <code>r7i.4xlarge</code> with 8 physical cores, running Valkey 9.1.1 and valkey-search 1.2.1. valkey-search sized its own writer pool to 8 threads. I turned persistence off, because I didn’t need a background save forking mid-run throwing a wrench into my latency numbers. Load came from a separate <code>c7i.8xlarge</code> in the same placement group, over 384 connections.</p>
<p>Every write is the same 4.6KB product hash that includes a category, a price, a SKU, a title, a description, and a 1024-dimension embedding. The load generator sends on a schedule and times each request from when it was supposed to go out, not when it managed to (more on this later). Every point runs for 25 seconds, three times, and I capture the median.</p>
<h2 id="do-idle-indexes-cost-anything">Do idle indexes cost anything?</h2>
<p>As I mentioned earlier, indexes subscribe to a key prefix. If a key is upserted that doesn’t match on an index, does that slow things down? In other words, my test here determines if the existence of indexes slows down the performance of others.</p>
<p>I ran the same write workload against <code>product:</code> keys in two setups: one index on <code>product:</code>, and another one with the same index plus fifty more indexes on unrelated prefixes.</p>




















<table><thead><tr><th>setup</th><th>sustained writes/sec</th><th>p99 @ 2,000/s</th></tr></thead><tbody><tr><td>1 index on <code>product:</code></td><td>32,000</td><td>2.18ms</td></tr><tr><td>same index + 50 on other prefixes</td><td>32,000</td><td>2.17ms</td></tr></tbody></table>
<p>Nice! I couldn’t measure a difference between them at any rate I tried.</p>
<p>This is because the prefix subscriptions live in a <a href="https://en.wikipedia.org/wiki/Trie">trie</a>. When you write <code>product:88213</code>, valkey-search walks the trie and only notifies the indexes whose prefix matches. An index subscribed to <code>orders:</code> doesn’t hear about it. <code>FT.INFO idx:other0</code> reports a field called <code>mutation_queue_size</code>, and across the entire run it never left zero, which validates the promise of the trie architecture.</p>
<p>So that hodgepodge of indexes in your repo isn’t what’s slowing your writes down. Only indexes whose prefix matches the keys you write are in the path at all.</p>
<h2 id="the-impact-of-matching-indexes">The impact of matching indexes</h2>
<p>So what’s the performance impact of having multiple matching indexes? I ran the same workload again, this time adding multiple identical indexes directly on the <code>product:</code> prefix.</p>






























<table><thead><tr><th>indexes on <code>product:</code></th><th>writes/sec</th><th>indexing jobs/sec</th></tr></thead><tbody><tr><td>0</td><td>80,000</td><td>0</td></tr><tr><td>1</td><td>33,636</td><td>33,636</td></tr><tr><td>2</td><td>20,000</td><td>40,000</td></tr><tr><td>4</td><td>16,818</td><td>67,272</td></tr></tbody></table>
<p>Every matching index gets an indexing job, and the write isn’t done until all of them are. To figure this out, I took Valkey’s <code>search_ingest_hash_keys</code> counter and divided by the writes that completed in the same window. I took this number and multiplied it by the write rate to calculate the indexing jobs/sec column.</p>
<p>The expensive part, naturally, is switching indexing on in the first place. Going from zero indexes to one cost me 58% of my write rate. The second index cost another 41%. Doubling from two to four only cost 16%. Each additional index cost less than the previous one.</p>
<p>Adding indexes pulls more total indexing work per second out of the same server, 33,636 jobs a second at one index and 67,272 at four. So one or two indexes clearly weren’t saturating the writer pool, because it eventually went on to do twice the work.</p>
<p>Indexing jobs fan out across a pool sized to your physical core count, and the blocked-client handles collapse into a single block on your connection. The write waits for the slowest single job, which is why four indexes don’t cost four times what one does.</p>
<p>Unfortunately I don’t know what the limiting factor is. My hunch is that it’s the per-index bookkeeping that happens on the main thread before a job reaches the pool, but I didn’t measure that, so 🤷.</p>
<p><img src="https://www.gomomento.com/blog/2026-08-27_stop-counting-indexes/fanout-p99.webp" alt="p99 write latency against offered write rate, for zero, one, two and four indexes on the product: prefix. All four curves are within about a millisecond of each other below 10,000 writes per second, then separate and turn sharply upward, each at a different rate."></p>
<p><em>NOTE - Adding matching indexes won’t appear to add latency until it’s too late. At 2,000 writes/sec, one index and four indexes were within a millisecond of each other. You won’t catch it watching p99 on a healthy system, because what you’re spending is headroom. You find out it’s gone when you need it.</em></p>
<h2 id="vector-fields-hit-different">Vector fields hit different</h2>
<p>The benchmarks above use TAG and NUMERIC fields, just the normal filter field types. So I went back to the first benchmark, added a single 1024-dimension HNSW vector field to the one-index setup, and re-ran it to find some staggering results.</p>
<p>32,000 writes per second became 2,828. 🤯</p>
<p>That’s an 11x difference in throughput because of a single field. My napkin math says that’s 2-3 milliseconds of CPU per vector insert spread across eight writer threads. To make matters worse, the performance cliffs. I increased the rate on my benchmark by ~40%, and the wheels fell off.</p>




















<table><thead><tr><th>offered writes/sec</th><th>p99</th><th>writer queue depth</th></tr></thead><tbody><tr><td>2,828</td><td>5.65ms</td><td>4</td></tr><tr><td>4,000</td><td>2,046ms</td><td>376</td></tr></tbody></table>
<p>The performance hit goes from five milliseconds to two seconds. The queue went from basically empty to 376 entries deep, and it stayed there for every rate I tried above that. You’re either under the line and fine, or over it and everything is late. If you’re capacity planning, be sure to check whether an index on your hot prefix has a vector field.</p>
<h2 id="do-vector-fields-affect-other-matching-indexes">Do vector fields affect other matching indexes?</h2>
<p>Back to the search feature I was building that required two indexes. A single index can’t do both jobs because of <a href="https://valkey.io/topics/search-data-formats/#stop-word-removal">stop words</a>. The text pipeline is configured for the entire index, and it splits words on punctuation before dropping anything in the stop word list. So <code>IT-500</code> becomes <code>it</code> and <code>500</code>, <code>it</code> is a stop word, so just <code>500</code> is added to the index. Turning on <code>NOSTOPWORDS</code> means your description field will index every <em>the</em>, <em>is</em>, <em>and</em>, and <em>it</em> (plus a lot more) in the catalog. So we need two indexes to have it turned on for one and off for the other.</p>
<p>But that made me wonder what the second index costs when the first one has a vector field. We saw how much of a hit it made to throughput in our earlier benchmarks.</p>




















<table><thead><tr><th>configuration</th><th>sustained writes/sec</th><th>p99</th></tr></thead><tbody><tr><td>semantic (TAG + NUMERIC + HNSW)</td><td>2,828</td><td>5.29ms</td></tr><tr><td>semantic + exact (TEXT, NOSTOPWORDS)</td><td>2,828</td><td>5.44ms</td></tr></tbody></table>
<p>About a three percent difference. The exact match index processed 141,408 text fields during the benchmark run. Both mutations hit the pool together, the write waits for the slower one (the vector). So if you’re already paying the HNSW tax, the lexical index has essentially no additional latency.</p>
<h2 id="my-takeaways">My takeaways</h2>
<p>So it turns out I was asking the wrong question when I started this experiment. I thought the number of indexes I had was going to slow performance down to a crawl. But it doesn’t. The real question is <em>what fields are inside the indexes that match your keys</em>? Which is a relief, when I think about it.</p>
<p>I also learned a couple of things about benchmarking while I was busy answering the wrong question. 😅</p>
<p>Every sustained writes/sec number in this post could have been bigger. At the top rung of my ladder, the no-index setup completed 128,000 writes a second (but my tables show 80,000). At that run rate, it was making every request wait 1.9 seconds in a queue first. A server running at full utilization drains as fast as it fills, so throughput looks perfect, but at a cost to latency. So the number I used in every table is the highest rate where p99 stayed within reason.</p>
<p>It took me three runs to believe what I saw in that four-row table early in this post. The first run stepped the rate by 1.4x per rung, which put two indexes and four indexes on the same 16,000 rung. Which at first made me think indexes three and four had no performance implications. But in reality, they had both fallen apart somewhere between rungs and the ladder couldn’t show me where. So I re-ran it with 1.19x steps and got a cleaner separation. Then I noticed that ladder started at 14,000, which is already well up the curve, so it never measured what latency looks like when the server is idle. My rule for picking a sustainable rate is relative to that idle number, which meant the second run was grading itself on a curve. The third run started at 2,000 and is the one in the table. A rate ladder can’t resolve a difference smaller than its own step, and it can’t tell you where the knee is if it never saw the flat part before it.</p>
<h3 id="try-it-yourself">Try it yourself</h3>
<p>I have the benchmark, scripts, and results <a href="https://github.com/momentohq/valkey-index-bench">available in GitHub</a>. If you want to check my numbers (or disagree with them!), please do and let me know what you find.</p>
<p>If you want to run the same tests on your own Valkey cluster, you can run these three commands:</p>
<pre><code>FT.INFO &#x3C;index> # shows mutation_queue_size (write backlog) for the specified index
INFO search # shows search_writer_queue_size for the whole pool
CONFIG SET search.info-developer-visible yes # unlocks per-field-type counters like search_ingest_field_vector
</code></pre>
<p>It’s cheap to experiment with your existing clusters because these mutations are reversible. <code>FT.DROPINDEX</code> is instant and your data was never in the index to begin with. Add a shape, measure it, then discard it.</p>
<p>Happy coding!</p>]]></content:encoded>
  </item>
  <item>
    <title>Agent Memory on Valkey</title>
    <link>https://www.gomomento.com/blog/agent-memory-on-valkey/</link>
  <dc:creator><![CDATA[Allen Helton]]></dc:creator>
    <pubDate>Wed, 19 Aug 2026 09:00:00 GMT</pubDate>
  <category><![CDATA[AI/ML]]></category>
  <category><![CDATA[Valkey]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/blog/agent-memory-on-valkey/</guid>
    <description><![CDATA[<p>A filter that looks like a query detail can change how Valkey searches your vectors. By the time you notice, the important decisions are already behind you.</p>]]></description>
    <content:encoded><![CDATA[<p>Agent memory sounds like a cut-and-dried vector search problem. You embed the task, find the nearest memories, and give them back to the model. Done.</p>
<p>Unfortunately it’s not that simple. Memories need to be similar, yes, but they also need to be recent. You don’t want memories from a year ago influencing your agent. And outcome is important too. If one approach worked and another failed, you probably want the successful one steering behavior.</p>
<p>I ran into this while building a small demo that puts <a href="https://github.com/momentohq/valkey-agent-memory-demo">Valkey Search inside an agent’s inference loop</a>. Every task the agent finishes gets written as a memory. It persists the task text, the approach that worked, whether it succeeded, when it happened, and a vector of the task. Next time a similar task shows up, the agent recalls what worked instead of rediscovering it.</p>
<p>I ended up learning how valkey-search handles this the hard way, because my first <code>FT.SEARCH</code> call did not work the way I expected:</p>
<pre><code class="language-bash">FT.SEARCH idx:memory "(@outcome:{success} @created_at:[1782900000 +inf])=>[KNN 3 @vector $vec]" PARAMS 2 vec &#x3C;query-vector> DIALECT 2
</code></pre>
<p>Left of the <code>=></code> is an ordinary boolean filter of a tag and a numeric range over two fields I indexed alongside the embedding. Right of it is the vector search.</p>
<p>I thought Valkey was going to find the nearest vectors and apply the tag and timestamp filters afterward. But it doesn’t work like that (in a good way). The filter criteria are actually inputs to the query planner. Depending on how many memories they match, Valkey chooses a different algorithm to search the vector index. In other words, adding a filter changes how the search runs, which meant I needed to rethink my initial memory schema. Turns out my retrieval policy was also an index-design decision.</p>
<h2 id="nobody-post-filters-anymore">Nobody post-filters anymore</h2>
<p>If you’ve used Pinecone, Qdrant, Weaviate, or Milvus, then this should be familiar. All of them decide at query time whether to walk the graph with your filter applied or abandon the graph and brute-force the matching subset instead. Pinecone calls it single-stage filtering. Qdrant calls it query planning. Weaviate calls it a flat search cutoff. Milvus doesn’t really call it anything, it just does it 😂. valkey-search is the same idea, and on the <code>FT.SEARCH</code> vector path it doesn’t implement post-filtering at all.</p>
<p>If you’re a <a href="https://github.com/pgvector/pgvector">pgvector</a> user, however, filtering happens after the index scan. With the default <code>hnsw.ef_search</code> of 40, a filter that keeps roughly 10% of those candidates might leave you with only 4 results. <a href="https://github.com/pgvector/pgvector?tab=readme-ov-file#iterative-index-scans">Iterative index scans</a> were added in 0.8.0 to make that a little better, and they’re off by default.</p>
<p>valkey-search calls it a <a href="https://github.com/valkey-io/valkey-search/blob/main/src/query/planner.cc">query planner</a>. It estimates how many keys your filter matches and picks one of two algorithms to perform the search.</p>
<p>If the estimate is small compared to the index, it pre-filters results by walking the qualified key set, computing each distance directly, and keeping a top-k heap. The <a href="https://en.wikipedia.org/wiki/Hierarchical_navigable_small_world">HNSW graph</a> is never traversed. So valkey-search is essentially doing a brute-force scan over a tiny set.</p>
<p>If the estimate is large, it performs an inline filter. Your predicate is handed to hnswlib as an <code>isIdAllowed</code> functor and evaluated during traversal of the base layer. Non-matching nodes are still visited and expanded, they just don’t get added to the result set.</p>
<p>The cutoff point for one algorithm vs the other is 0.001. So if the number of estimated matching keys is at most .1% of the number of vectors in the index, it will go the brute-force route. Otherwise it uses the inline filter.</p>
<p>For reference, the cutoff point for Milvus is around 7%, which makes valkey-search about 70x stricter. You end up on the inline path more often than you’d think.</p>
<h2 id="make-the-filter-disappear">Make the filter disappear</h2>
<p>On my demo index with a few hundred memories, the threshold works out to well under 1 key, so every recall that matches anything takes the inline path. You’d never notice either way at that scale.</p>
<p>But what would happen with the same schema in production with 1,000,000 memories? The threshold is 1,000 keys. The planner considers the selectiveness of the filter. If <code>@outcome:{success}</code> matches 70% of the index and your <code>@created_at</code> window is also broad, you’re squarely in the inline path. And the planner is right to put you there. If you scope recall to a single tenant with 400 memories, you drop under the threshold, and the query becomes an exact scan. Perfect, fast recall because 400 distance computations is nothing.</p>
<p>Let’s make it more difficult. A filter matching 1% of a large index sits 10x above the cutoff point, so it takes the inline path, where HNSW traverses a significant number of candidates for every one it’s allowed to keep. That results in a lot of extra latency you didn’t account for. And you can’t change that by tuning it, because <code>search.prefiltering-threshold-ratio</code> is immutable unless <code>search.debug-mode</code> is on, and it isn’t in the public configurables table at all.</p>
<p>You can check which path you’re actually on, by the way. valkey-search counts both, named <code>search_prefiltering_requests_count</code> and <code>search_inline_filtering_requests_count</code>. They’re module fields, so you need <code>INFO SEARCH</code> and not plain <code>INFO</code>. You can run your recall query a hundred times to see which one moves.</p>
<p>So your best bet is the index itself. Make the filter act like a namespace. Put a hash tag in the index name, prefix the keys to match, and each tenant’s recall hits one shard against a small index where the filter is mostly irrelevant. Valkey enforces it in both directions, too. A tagged index name requires every prefix to carry the same tag, and an untagged one requires that none of them do. Be careful with this though, because it’s not easy to undo if you change your mind since there’s no <code>FT.ALTER</code> here.</p>
<h2 id="forgetting-is-expensive">Forgetting is expensive</h2>
<p>Removing a vector from an HNSW index calls <code>markDelete</code> and returns. The node isn’t removed from the graph, it’s still visited, it still routes other searches through itself, it just fails the deleted check. <code>search.hnsw-allow-replace-deleted</code> would let the next insert reuse that slot, but it’s false by default and more of a non-production flag. In production the space is stranded until you drop the index.</p>
<p>Luckily, updates are a different story. Modifying an indexed vector routes to an in-place update under the existing label, so the node keeps its slot and just gets its links rewired. Writing a hash field that isn’t the vector doesn’t mess with the graph, and rewriting the vector with identical bytes short-circuits before it gets there.</p>
<p>So updates are cheap, but deletion is where it gets expensive. That includes <code>DEL</code>, eviction, and expiry.</p>
<p>That’s a little scary, because expiry <em>is</em> the recency policy. valkey-search subscribes to generic, expired, and evicted keyspace notifications, so a TTL’d memory really does leave the vector index when the key goes away. Which is what you want, but it’s also what makes graph nodes stranded.</p>
<p>Which means you have to change how you key your memories. Creating a new key every run feels like a safe and reasonable default, and it’s what my demo does with its <code>memory:&#x3C;id></code> per completed task. But add a TTL to keep things fresh, and every expiring key leaves a node behind. That’s an expensive default at scale. Instead, use a stable key derived from a task fingerprint, and update it in place as the agent learns more about that kind of task. This means the task costs only one node instead of one per attempt.</p>
<p>There are two recency controls here to consider. The <code>@created_at</code> range decides what you’re willing to believe on any given query, and it doesn’t delete anything. The TTL decides what you’re willing to pay to store. Range should be your first lever and the TTL should be the slower fallback.</p>
<h2 id="decide-before-you-have-data">Decide before you have data</h2>
<p>This demo surprised me twice: the filter I wrote as a query detail decides which algorithm runs, and the TTL I (almost) added to keep memories fresh led to stranding the index.</p>
<p>Everything here is discoverable, at least. The planner code is a short function to read and understand. The filtering comes from hnswlib. Even the deleted node behavior is a comment in the source, and the counters are available in <code>INFO SEARCH</code>. You don’t have to take my word for any of it (but you should 😜).</p>
<p>What you can’t read your way out of is when you have to make decisions. How the index is scoped and how a memory is keyed are day-one calls, made before you have a single memory to check them against. Outside of that dev-only flag, a rebuild is the only way to reclaim space that deletion stranded. Get those wrong and you’re reindexing.</p>
<p>The demo <a href="https://github.com/momentohq/valkey-agent-memory-demo">is on GitHub</a> if you want somewhere to start. <code>docker compose up -d</code> gets you Valkey with the search module and an agent that writes its own memories. Run a few tasks through it, then look at <code>INFO SEARCH</code> and find out which path your queries are actually taking.</p>
<p>Happy coding!</p>]]></content:encoded>
  </item>
  <item>
    <title>BuffConf 2026: Signal Over Noise, One Year Later</title>
    <link>https://www.gomomento.com/blog/buffconf-2026-signal-over-noise-one-year-later/</link>
  <dc:creator><![CDATA[Lionel Bringuier]]></dc:creator>
    <pubDate>Wed, 05 Aug 2026 09:00:00 GMT</pubDate>
  <category><![CDATA[Events]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/blog/buffconf-2026-signal-over-noise-one-year-later/</guid>
    <description><![CDATA[<p>A recap of two days of honest engineering conversations, practical lessons, and the community behind BuffConf 2026.</p>]]></description>
    <content:encoded><![CDATA[<p><img src="https://www.gomomento.com/assets/content/blog/buffconf-2026-signal-over-noise-one-year-later/recap-blog-banner.png" alt="Audience members seated in a theater for BuffConf 2026."></p>
<p>Last year, <a href="https://buffconf.gomomento.com/past-sessions">the first edition of Buffer-Free Video)^AI</a> left me in disbelief that a scrappy ten-week sprint had pulled 150 streaming professionals into one room. This year the question in my head was different. Not “can we do it again?” but “can we keep the signal-to-noise ratio that made the first one special?”</p>
<p>I believe we did, and the numbers back it up.</p>
<h2 id="the-room-we-wanted">The Room We Wanted</h2>
<p>Here is the stat I am proudest of: we had 117 attendees show up out of 124 registrations. That is a 94.4% show-rate. If you have ever organized a tech conference, you know the typical number hovers around 60%. People register, life happens, half the badges never get picked up.</p>
<p>That did not happen here, and it tells you something about who was in the room. Definitely not people collecting free T-shirts, stickers, Mo plushies or vintage green Elemental swag (thanks Kiran!). Of the seven no-shows, six were from sponsoring organizations who had other folks covering the event.</p>
<p>Those 117 people represented 41 unique organizations, from the largest broadcasters, tech platforms, video infrastructure providers… the full spectrum of who actually builds and runs media workloads at scale. When you get that kind of density of real operators in one place, the hallway conversations become as valuable as the sessions. Actually, maybe even more.</p>
<p><img src="https://www.gomomento.com/assets/content/blog/buffconf-2026-signal-over-noise-one-year-later/in-the-room-buffconf-2026.png" alt="The BuffConf 2026 audience."></p>
<h2 id="we-pre-screened-the-pitches-out">We Pre-Screened the Pitches Out</h2>
<p>Here is the thing we are strict about. BuffConf is not a place to sell. Every proposed session got human-screened, and anything that read like a vendor pitch got sent back. We prioritized end-user and practitioner voices, the people who have actually been paged at 3am when things flinched.</p>
<p>The result was 22 technical sessions with 29 speakers from 17 companies. And critically, 40% of our speakers were non-vendors. We are talking real production stories from operators like Paramount, Netflix, FOX, CBS Interactive, Meta and Red Bull Media House. Not “here is our roadmap.” More like “here is what we tried, what failed and what finally held up.”</p>
<p>It would be easy to fill an agenda with vendors who have marketing budgets and room for another video nerd conference (all the more when the traditional bigger one is seemingly canceled silently this year… of course you know what I’m talking about!). It is much harder, and also much more valuable, to get the operators to stand up and tell the unvarnished truth about their infrastructure. When they do, everyone in the room learns something they can use in their own work.</p>
<h2 id="what-the-presentations-covered">What The Presentations Covered</h2>
<p>Three main themes ran through the two days, and a handful of sessions captured each one.</p>
<p><strong>Production workflows at scale.</strong> I opened with “104 Matches. Five Billion Viewers. Zero Margin for Error,” walking through the FIFA 2026 World Cup numbers and what global live scale actually demands, with no excuses. Spencer Shanson from Paramount then told the story of scaling a FAST platform from a handful of channels to thousands. That kind of journey, from a few streams to an entire catalog of linear channels, is exactly the operational reality most of the room is living on a daily basis, and hearing it from someone who has actually done it is worth more than any architecture diagram. And when “production at scale” means “at Netflix’s scale”, who better than Sunjeet Singh and William Schor could explain how Netflix handles over one billion RPS on 120,000 nodes?</p>
<p><img src="https://www.gomomento.com/assets/content/blog/buffconf-2026-signal-over-noise-one-year-later/spencer-shanson-buffconf-2026.png" alt="Spencer Shanson at BuffConf 2026."></p>
<p><strong>Practical AI, and honest boundaries around it.</strong> This was the part I found most refreshing. Nobody stood up and told us AI was going to solve everything. It’s still a journey, and our keynote speaker, Manish Rao, showed us all the progress that AWS Elemental Inference has made since NAB. It’s staggering how three months can mean a world of difference at the current speed of AI innovation. Edwin Rivera from CBS Interactive showed how CBS Sports built an AI-powered “For You Page,” blending editorial curation with embedding-based recommendations to actually move engagement. Content discovery in sports is one of the richest problem spaces in our industry right now. And Jeffrey Kember from NVIDIA walked through what accelerated computing is doing for encoding, quality, and real-time media workflows. A reminder that we are genuinely at the beginning of AI for media, not the end. Across the AI sessions, we also saw token efficiency work borrowed straight from video compression concepts, automated QC loops, and an edge anti-piracy simulation harness. Real implementations, with the scars to prove it, paired with hands-on framework training so engineering teams could take something home and build.</p>
<p><img src="https://www.gomomento.com/assets/content/blog/buffconf-2026-signal-over-noise-one-year-later/edwin-rivera-buffconf-2026.png" alt="Edwin Rivera at BuffConf 2026."></p>
<p><strong>The client-side reality.</strong> Our panel on Optimizing Playback SDKs and Device Capabilities brought together Jonathan Colwell from FOX, Joshua Lamb from Red Bull Media House, and David Van der Voort “DVD” from Paramount. Three very different takes on the same daily chaos: supporting every device under the sun, with every usage pattern. That fragmentation is a grind everyone in playback knows intimately, and getting three operators to compare notes in the open was exactly the kind of session you cannot get anywhere else.</p>
<p><img src="https://www.gomomento.com/assets/content/blog/buffconf-2026-signal-over-noise-one-year-later/panel-buffconf-2026.png" alt="The BuffConf 2026 playback panel."></p>
<p>We kept the format balanced through all of it. Deep technical talks, real networking time, and interactive workshops where people could get their hands dirty. Room to think, argue, and go deeper.</p>
<h2 id="the-qa-is-still-the-best-part">The Q&#x26;A Is Still the Best Part</h2>
<p>Same as last year, the Q&#x26;A sessions were electric. A technical crowd asked technical questions, and every speaker rose to meet them. Those exchanges are where a prepared talk turns into something better, where you get past the slides into the messy details that only come out when a peer in the audience pushes on exactly the right point.</p>
<p>That is the whole reason we do this in person. You cannot replicate that online.</p>
<h2 id="thank-you">Thank You</h2>
<p>A conference only works because of the people who believe in it before it exists.</p>
<p>To our speakers, from Paramount, Netflix, CBS Interactive, FOX, Red Bull Media House, NVIDIA, Meta, AWS, Visionular, Bitmovin and every company that sent someone to share hard-won lessons: thank you for choosing substance over spin. You are the reason people fly in.</p>
<p>To JP Saibene and Nicolas Gonzalez from Uruguay, to Ali Begen from Tűrkiye and to everyone who traveled a long way to be in Seattle: thank you for bringing the curiosity and the generosity that makes this community what it is.</p>
<p>To the Momento crew who pulled this together, and in particular Hannah, Mike, Kelsey, Maéline and everyone who spent months making a two-day event look effortless: thank you, I’m incredibly grateful and lucky to be working with you all.</p>
<p>And to the people who showed up: this community was the event. Your questions, your stories, your willingness to say “actually, that broke for us too” is what makes this community worth gathering. The streaming landscape keeps moving fast, especially where AI meets live video. We were all here to discuss the future of video, one honest conversation at a time.</p>
<p><a href="https://buffconf.gomomento.com/">See you next year.</a></p>]]></content:encoded>
  </item>
  <item>
    <title>The most expensive word in inference is &quot;now&quot;</title>
    <link>https://www.gomomento.com/blog/the-most-expensive-word-in-inference-is-now/</link>
  <dc:creator><![CDATA[Allen Helton]]></dc:creator>
    <pubDate>Thu, 30 Jul 2026 09:00:00 GMT</pubDate>
  <category><![CDATA[AI/ML]]></category>
  <category><![CDATA[Performance]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/blog/the-most-expensive-word-in-inference-is-now/</guid>
    <description><![CDATA[<p>Async agents don't have a human staring at the screen, and that changes the economics of inference more than you'd think. Practical takeaways after watching Meryem Arik's talk on inference for async agents.</p>]]></description>
    <content:encoded><![CDATA[<p>A few weeks ago I handed off a pretty hefty refactor to Claude, closed my laptop, and went out to do some farm chores. I had no idea how long the job was going to take, and I didn’t care. I walked back in a couple hours later and it was done.</p>
<p>That should feel stranger than it does. When we typically think about inference, speed is a whole thing. Time to first token (TTFT), tail latency, and shaving milliseconds off the round trip so a human can stay in the flow are all you see in articles these days. Yet there I was running one of the most token-hungry things from my laptop, and I didn’t really care how long it took.</p>
<p><a href="https://www.linkedin.com/in/meryemarik/">Meryem Arik</a>, CEO of <a href="https://doubleword.ai/">Doubleword</a>, was able to articulate really well how I felt about it in her recent talk <a href="https://youtu.be/M4J940vgjgI"><em>Inference for Async Agents in Production</em></a>. She posed the simple question “<em>who’s waiting?</em>” that made it all click for me.</p>
<p>When nobody’s waiting, the cost of inference looks entirely different.</p>
<h2 id="schedule-inference-like-nobodys-watching">Schedule inference like nobody’s watching</h2>
<p>In a synchronous system like chat, a human is directly in the flow. Make them wait without any feedback and they start clicking around wondering if anything is happening. In an async system, an agent might fan out, run for thirty minutes, and ping you when it’s done. Nobody is watching the screen. In many instances, it doesn’t really matter whether it comes back in 35 minutes or 45, because the user has gone off to do something else.</p>
<p>To me, this reads like a UX detail, but it drives almost every other decision in your stack.</p>
<p>“Who’s waiting?” is the right place to start. More specifically, <em>how long is the work allowed to wait and what is blocked behind it?</em> Even without a person staring at the screen, a call on an agent’s critical path still has a deadline. Async workflows just give us more room to decide what that deadline actually is.</p>
<p>A human waiting is the most expensive clock in the system. “Fast enough for a person” forces things like how you provision, how much you batch, which chips you buy, and how much idle capacity you keep on standby. Saying “I want an answer now” makes everything expensive. But if you take the human out of the loop, that clock loosens its grip, along with the price tag it was setting for everything else.</p>
<h2 id="we-reinvented-batch-processing">We reinvented batch processing</h2>
<p>We’ve done this a time or two in the past with things like interactive database queries versus overnight analytics jobs. We prioritize the web request a user is staring at versus the batch job that runs at 2 a.m. The tech industry has been separating “someone is waiting” from “nobody is waiting” since before I was born. 😅</p>
<p>Inference is speed-running what databases and job queues learned decades ago, but in a good way. When a young field’s hardest problem is something the infrastructure world already recognizes, you can jump straight into borrowing patterns that are proven to work.</p>
<p>Meryem frames the core tension as the old trade-off triangle: latency, quality, throughput. You get to pick two of them.</p>
<p><img src="https://www.gomomento.com/blog/2026-07-30_the-most-expensive-word-in-inference-is-now/tradeoff-triangle.avif" alt="Tradeoff triangle between quality, low latency, and high throughput"></p>
<p>Chat apps pick latency and quality. Async agents pick quality and throughput, and they let latency slide. A long-running agent mostly needs the smartest model it can get so it stays coherent across a two-hour piece of work while it manages subagents and juggles tasks. Delivering tokens at conversational speed doesn’t buy it anything.</p>
<h3 id="batching-is-really-a-scheduling-problem">Batching is really a scheduling problem</h3>
<p>Batch-size math can be tricky. In one example from Meryem’s talk, a model running at batch size one and tuned for maximum interactivity costs roughly $4.65 per million tokens. Push enough concurrent work through the same model and tune for throughput instead, and that cost comes out closer to nine cents per million tokens.</p>
<p>In that example, the same weights and quality produce a 50x difference in cost. That makes a substantial difference to your unit economics.</p>
<p>But there’s a catch. Bigger batches produce their best economics only when you have enough compatible work waiting to keep them full. An empty slot in a batch is silicon you’re paying for that isn’t producing tokens. The hard part is keeping batches full while demand stays bursty and wildly unpredictable.</p>
<p>I’ve made this argument from the other direction before, that <a href="https://www.gomomento.com/blog/gpus-are-the-most-expensive-resource-in-tech/">GPUs are the most expensive resource in tech and we use them badly</a>. Batch size is the same thing. High utilization is paramount, and you win or lose it in the scheduler.</p>
<h3 id="the-queue-is-the-product">The queue is the product</h3>
<p>I really liked how Meryem put it: most providers lose money on real-time serverless endpoints. It comes back to the “now” tax. To promise a human fast responses against lumpy traffic, you have to provision for the peak and then eat the idle troughs in between. That idle compute is money on fire.</p>
<p>If the work has room to wait, the batch queue becomes a powerful tool. You can push work into the low periods and backfill the troughs that are already being paid for. Spot instances become fair game for soaking up spare compute when the work can tolerate interruption or be checkpointed. SLA-aware routing lets you jump an urgent request ahead of the rest. You can even reorder the queue so requests that hit the same mixture-of-experts weights run back to back and skip reloading those experts every time.</p>
<p>Once you stop committing to “now”, the scheduler/router/whatever is holding the queue becomes the center of the system. It’s  where the value collects, and it’s where everything is heading. As hardware and models commoditize, the advantage moves up into routing, placement, and orchestration, and whoever owns the “what runs next” decision owns the economics.</p>
<h2 id="start-labeling-the-wait">Start labeling the wait</h2>
<p>I mark everything real-time out of habit. Every call gets treated as if a human is holding their breath on the other end, even when the “human” is an agent off doing six other things. That results in an unnecessary “now” tax on work that doesn’t need it.</p>
<p>Doubleword has categorized latency into real-time, async (about a minute), and batch (24 hours) buckets. The 50× cost gap we mentioned earlier comes down to which bucket you pull from, and many of us (myself included) reach straight for the expensive one by reflex.</p>
<p>One of the most boring-yet-useful things you can do immediately after reading this article is going through every call your agent makes one at a time and ask whether a human is waiting on each one. You might be surprised how few are. The background call fetching context isn’t waiting. The sub-agent grinding through step four of eleven isn’t either. The nightly summarization job definitely isn’t.</p>
<p>In the near future, we’ll have routers smart enough to infer a request’s latency tier from its context, dependencies, and deadline, so we don’t have to label everything by hand. Once we figure out how to do that efficiently, system costs drop dramatically because the scheduler can optimize around how long each piece of work is actually allowed to wait. Until then, start by giving every model call a realistic latency budget instead of letting the entire workload take on “now” by default.</p>
<p>Everything we’re talking about has been around in the infrastructure world for years. The same instinct drives shared caching at Momento: preserve expensive state, reuse work you have already paid for, and keep scarce resources productive instead of idle. Inference gives those old ideas a very large new price tag.</p>
<p>If you’re wrestling with any of this, we’d love to <a href="https://www.gomomento.com/contact-us/">compare notes</a>.</p>
<p>Happy coding!</p>]]></content:encoded>
  </item>
  <item>
    <title>Consistency compounds: Valkey's journey to 200 Gbps</title>
    <link>https://www.gomomento.com/blog/consistency-compounds-valkeys-journey-to-200-gbps/</link>
  <dc:creator><![CDATA[Khawaja Shams]]></dc:creator>
    <pubDate>Thu, 23 Jul 2026 16:00:00 GMT</pubDate>
  <category><![CDATA[Caching]]></category>
  <category><![CDATA[Valkey]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/blog/consistency-compounds-valkeys-journey-to-200-gbps/</guid>
    <description><![CDATA[<p>Across four releases, I/O-threading changes removed a serial copy bottleneck, cut p99 latency, and brought large GETs to line rate on our 200 Gbps test rig.</p>]]></description>
    <content:encoded><![CDATA[<p><img src="https://www.gomomento.com/assets/content/blog/consistency-compounds-valkeys-journey-to-200-gbps/read-bandwidth.svg" alt="Valkey GET bandwidth by value size across versions 7.2, 8.1, 9.0, and 9.1, with the 200 Gbps NIC limit marked"></p>
<p>If you came here looking for a post from me on consistency models, I have to disappoint you. Today, I want to talk about the value of consistency in life. In life, effort is additive, but consistency is multiplicative.</p>
<p>Over the last few years, I have had the honor of watching the Valkey project blossom from an idea into an inspirational, community-driven effort with consistent improvements in each release. These improvements range from memory efficiency to availability at scale to substantial performance gains. These small improvements compound.</p>
<p>This compounding effect and the power of a driven community is perhaps best illustrated by the journey of the I/O-threading architecture and its impact on large objects from 1 MB to 64 MB in Valkey. Larger items are becoming increasingly important for inference KV caches, 4K video streaming, and other enticing use cases. At the very least, the impact of large objects should not be overlooked, as they can <a href="https://www.gomomento.com/blog/large-objects-in-valkey-9-0/">impact everyone else’s latency</a>; this post measures how fast the large objects themselves go.</p>
<p>Buckle up, because it’s about to get spicy.</p>
<h2 id="a-brief-history-of-io-threads">A brief history of I/O threads</h2>
<p>Valkey 7.2 inherited an I/O-thread model from a software stack doing its best to stay single-threaded. The main thread and I/O threads worked in coordinated phases, separated by synchronization barriers, and the <a href="https://github.com/valkey-io/valkey/blob/7.2/valkey.conf">7.2 config file</a> is candid about the result: “Usually threading reads doesn’t help much.”</p>
<p>Valkey 8.0 delivered a fundamental rearchitecture of I/O threads, <a href="https://valkey.io/blog/unlock-one-million-rps/">tripling throughput to over a million requests per second</a>. This idea was not new. In August 2023, seven months before the fork, Dan Touitou filed <a href="https://github.com/redis/redis/issues/12489">redis#12489</a>, laying out exactly this design in detail, benchmarks included.</p>
<blockquote>
<p>Redis let #12489 sit. The issue is still open in the tracker today, unassigned and without a milestone.</p>
</blockquote>
<p>Within days of the fork, the Valkey community copied the proposal verbatim into <a href="https://github.com/valkey-io/valkey/issues/22">issue #22</a>, greeted it as “a true gem,” and shipped it. The new architecture enabled continuously running I/O threads connected by queues, so reads, parses, and writes proceed on separate cores while the main thread executes commands. Valkey 8.1 delivered TLS handshake offload to I/O threads in <a href="https://github.com/valkey-io/valkey/pull/1338">#1338</a>.</p>
<p>Redis shipped a <a href="https://redis.io/blog/redis-8-0-m03-is-out-even-more-performance-new-features/">strikingly similar asynchronous I/O-threading model</a> in Redis 8 in May 2025, a year after the fork and 21 months after the design landed in its own tracker. Around the same time, we put <a href="https://www.gomomento.com/blog/valkey-turns-one-how-the-community-fork-left-redis-in-the-dust/">Valkey 8.1 and Redis 8.0 head to head</a> on small objects. Valkey 8.1 outran Redis 8.0 by 37% on writes and 16% on reads.</p>
<p>Valkey 9.0 brought <a href="https://github.com/valkey-io/valkey/pull/2078">reply copy avoidance</a>, changing the performance of large items entirely. Before this change, the main thread copied the entire object into a connection reply buffer before moving to the next command. While a large item is being copied, the entire pipeline stalls. Small objects are not serviced until the copy finishes.</p>
<p>Valkey 9.0 instead passes a reference to the I/O threads and keeps the object alive with a reference count. The I/O worker for that connection hands the object’s memory directly to <code>writev()</code>. This shortens the handoff to the I/O threads and gets the main thread back to handling requests.</p>
<p>Valkey 9.1 then redesigned communication between the main thread and I/O threads around <a href="https://github.com/valkey-io/valkey/pull/3324">lock-free queues</a>, credited in the release notes with an 8-17% throughput gain.</p>
<p>Based on these changes, we expected 8.0 to lift reads and writes for smaller items but still struggle to fill the network link for larger items. We expected 9.0 to bring GETs to line rate and 9.1 to improve writes.</p>
<p>This is what open-source competition buys everyone, including teams that never leave Redis. A performance design that sat for seven months as an unassigned issue became table stakes for both projects within two years of being filed.</p>
<h2 id="show-me-the-numbers">Show me the numbers</h2>
<p>We swept values from 1 MB to 64 MB across four Valkey releases on two nodes with 200 Gbps of bandwidth between them. The full setup is below.</p>
<p><img src="https://www.gomomento.com/assets/content/blog/consistency-compounds-valkeys-journey-to-200-gbps/8mb-release-journey.svg" alt="GET and SET bandwidth for 8 MB values across Valkey 7.2, 8.1, 9.0, and 9.1"></p>
<h3 id="valkey-72-reads-capped-at-35-gbps-writes-at-55">Valkey 7.2: reads capped at 35 Gbps, writes at 55</h3>
<p><img src="https://www.gomomento.com/assets/content/blog/consistency-compounds-valkeys-journey-to-200-gbps/release-7-2-bandwidth.svg" alt="GET and SET bandwidth by value size for Valkey 7.2.13, with GET shown as a solid line and SET shown as a dashed line"></p>
<p>An 8 MB value delivers 21 Gbps on reads and 23 Gbps on writes, with p99 latencies of 174 and 164 ms. Turning on <code>io-threads-do-reads</code> moved only the 1 MB read cell.</p>
<h3 id="valkey-80-and-81-writes-take-off-large-reads-stay-put">Valkey 8.0 and 8.1: writes take off, large reads stay put</h3>
<p><img src="https://www.gomomento.com/assets/content/blog/consistency-compounds-valkeys-journey-to-200-gbps/release-8-1-bandwidth.svg" alt="GET and SET bandwidth by value size, comparing Valkey 7.2.13 in red with Valkey 8.1.8 in orange"></p>
<p>The threading rebuild lifts our 8 MB write from 23 to 137 Gbps, six times faster, and 1 MB reads reach 183 Gbps. Larger reads settle at 30-33 Gbps whether the value is 8 MB or 64 MB. That flat floor points to a serial, per-byte bottleneck. Valkey 8.0.9 and 8.1.8 measured the same at every size, so one line carries both.</p>
<h3 id="valkey-90-large-gets-jump-to-line-rate">Valkey 9.0: large GETs jump to line rate</h3>
<p><img src="https://www.gomomento.com/assets/content/blog/consistency-compounds-valkeys-journey-to-200-gbps/release-9-0-get-bandwidth.svg" alt="GET bandwidth by value size, comparing Valkey 8.1.8 in orange with Valkey 9.0.4 in green"></p>
<p>Every size from 1 MB to 64 MB reads at 190-201 Gbps. The 8 MB p99 falls from 112 to 24 ms, and a 64 MB read drops from just under a second to 230 ms. Writes do not move because the ingest path was never copy-bound. That ceiling waits for 9.1.</p>
<h3 id="valkey-91-more-headroom-for-writes">Valkey 9.1: more headroom for writes</h3>
<p><img src="https://www.gomomento.com/assets/content/blog/consistency-compounds-valkeys-journey-to-200-gbps/release-9-1-set-bandwidth.svg" alt="SET bandwidth by value size, comparing Valkey 9.0.4 in dark green with Valkey 9.1.0 in light green"></p>
<p>With reads pinned at the network limit, the gain surfaces on writes. The 8 MB write rises from 134 to 166 Gbps, a 24% gain, while values from 12 MB to 64 MB gain 11-19%.</p>
<h2 id="the-short-run-held">The short run held</h2>
<p>To make sure the 15-second runs were not catching a lucky window, I reran every 8 MB GET and SET cell for 15 minutes. Valkey 9.1 is a good example: GET held 200.8 Gbps and SET held 165.5 Gbps, right on top of the original 201 and 166 Gbps results. Nothing sagged as the runs went on.</p>
<picture>
  <source media="(max-width: 640px)" srcset="https://www.gomomento.com/assets/content/blog/consistency-compounds-valkeys-journey-to-200-gbps/long-run-stability-mobile.svg">
  <img src="https://www.gomomento.com/assets/content/blog/consistency-compounds-valkeys-journey-to-200-gbps/long-run-stability.svg" alt="Valkey 9.1 GET and SET throughput holding steady over 15 minutes" loading="lazy">
</picture>
<h2 id="detailed-results">Detailed results</h2>
<h3 id="reads-get">Reads (GET)</h3>
<p>GET-only, 32 connections, 100% hit rate.</p>
<h4 id="get-bandwidth-gbps">GET bandwidth (Gbps)</h4>



























































<table><thead><tr><th>Value size</th><th align="right">7.2.13</th><th align="right">8.1.8</th><th align="right">9.0.4</th></tr></thead><tbody><tr><td>1 MB</td><td align="right">32</td><td align="right">183</td><td align="right">200</td></tr><tr><td>2 MB</td><td align="right">35</td><td align="right">55</td><td align="right">193</td></tr><tr><td>4 MB</td><td align="right">26</td><td align="right">45</td><td align="right">197</td></tr><tr><td>8 MB</td><td align="right">21</td><td align="right">33</td><td align="right">200</td></tr><tr><td>12 MB</td><td align="right">21</td><td align="right">32</td><td align="right">191</td></tr><tr><td>16 MB</td><td align="right">21</td><td align="right">32</td><td align="right">201</td></tr><tr><td>32 MB</td><td align="right">21</td><td align="right">32</td><td align="right">196</td></tr><tr><td>64 MB</td><td align="right">22</td><td align="right">31</td><td align="right">191</td></tr></tbody></table>
<p>Note: Valkey 9.1 reads also hold line rate, so the table stops at 9.0.</p>
<h4 id="get-p99-latency-ms">GET p99 latency (ms)</h4>



























































<table><thead><tr><th>Value size</th><th align="right">7.2.13</th><th align="right">8.1.8</th><th align="right">9.0.4</th></tr></thead><tbody><tr><td>1 MB</td><td align="right">13</td><td align="right">2.2</td><td align="right">2.4</td></tr><tr><td>2 MB</td><td align="right">20</td><td align="right">19</td><td align="right">5.1</td></tr><tr><td>4 MB</td><td align="right">58</td><td align="right">41</td><td align="right">9.4</td></tr><tr><td>8 MB</td><td align="right">174</td><td align="right">112</td><td align="right">24</td></tr><tr><td>12 MB</td><td align="right">244</td><td align="right">166</td><td align="right">56</td></tr><tr><td>16 MB</td><td align="right">329</td><td align="right">238</td><td align="right">56</td></tr><tr><td>32 MB</td><td align="right">531</td><td align="right">489</td><td align="right">116</td></tr><tr><td>64 MB</td><td align="right">948</td><td align="right">1,020</td><td align="right">230</td></tr></tbody></table>
<h3 id="writes-set">Writes (SET)</h3>
<p>SET-only, 32 connections.</p>
<h4 id="set-bandwidth-gbps">SET bandwidth (Gbps)</h4>




































































<table><thead><tr><th>Value size</th><th align="right">7.2.13</th><th align="right">8.1.8</th><th align="right">9.0.4</th><th align="right">9.1.0</th></tr></thead><tbody><tr><td>1 MB</td><td align="right">51</td><td align="right">201</td><td align="right">201</td><td align="right">201</td></tr><tr><td>2 MB</td><td align="right">53</td><td align="right">200</td><td align="right">200</td><td align="right">201</td></tr><tr><td>4 MB</td><td align="right">55</td><td align="right">200</td><td align="right">200</td><td align="right">201</td></tr><tr><td>8 MB</td><td align="right">23</td><td align="right">137</td><td align="right">134</td><td align="right">166</td></tr><tr><td>12 MB</td><td align="right">23</td><td align="right">135</td><td align="right">139</td><td align="right">166</td></tr><tr><td>16 MB</td><td align="right">24</td><td align="right">137</td><td align="right">139</td><td align="right">165</td></tr><tr><td>32 MB</td><td align="right">24</td><td align="right">139</td><td align="right">134</td><td align="right">159</td></tr><tr><td>64 MB</td><td align="right">24</td><td align="right">130</td><td align="right">134</td><td align="right">149</td></tr></tbody></table>
<h4 id="set-p99-latency-ms">SET p99 latency (ms)</h4>




































































<table><thead><tr><th>Value size</th><th align="right">7.2.13</th><th align="right">8.1.8</th><th align="right">9.0.4</th><th align="right">9.1.0</th></tr></thead><tbody><tr><td>1 MB</td><td align="right">8.7</td><td align="right">3.3</td><td align="right">3.2</td><td align="right">3.2</td></tr><tr><td>2 MB</td><td align="right">17</td><td align="right">6.5</td><td align="right">6.7</td><td align="right">5.3</td></tr><tr><td>4 MB</td><td align="right">35</td><td align="right">12</td><td align="right">13</td><td align="right">14</td></tr><tr><td>8 MB</td><td align="right">164</td><td align="right">41</td><td align="right">49</td><td align="right">34</td></tr><tr><td>12 MB</td><td align="right">289</td><td align="right">62</td><td align="right">57</td><td align="right">52</td></tr><tr><td>16 MB</td><td align="right">357</td><td align="right">77</td><td align="right">70</td><td align="right">60</td></tr><tr><td>32 MB</td><td align="right">1,580</td><td align="right">118</td><td align="right">143</td><td align="right">107</td></tr><tr><td>64 MB</td><td align="right">2,580</td><td align="right">237</td><td align="right">213</td><td align="right">206</td></tr></tbody></table>
<h2 id="the-setup">The setup</h2>
<p><strong>Machines.</strong> Two <code>c8gn.16xlarge</code> instances with Graviton4 and 200 Gbps networking in the same availability zone and cluster placement group, running Amazon Linux 2023.</p>
<p><strong>Server.</strong> We tested the official <code>valkey/valkey</code> Docker image at versions <code>7.2.13</code>, <code>8.1.8</code>, <code>9.0.4</code>, and <code>9.1.0</code>. We also measured <code>8.0.9</code>. It matched <code>8.1.8</code> within run-to-run noise at every size, so the tables show 8.1 as the 8.x column. Valkey 7.2 ran with the same flags as the newer versions. A control with <code>io-threads-do-reads yes</code> changed only the 1 MB GET cell, from 32 to 41 Gbps. Each version ran in a fresh container with host networking:</p>
<pre><code class="language-sh">docker run --network host --cpuset-cpus 8-23 \
  --ulimit nofile=32768:65536 valkey/valkey:&#x3C;version> \
  --save '' --appendonly no --io-threads 16 \
  --protected-mode no --maxmemory 60gb
</code></pre>
<p>Persistence was off. The 16 threads in Valkey’s <code>io-threads</code> count were one main thread plus 15 I/O workers. The process was pinned to cores 8-23 so it never fought the kernel for the cores doing network interrupt work.</p>
<p><strong>Interrupts.</strong> <code>irqbalance</code> was off on both machines. The ENA NIC was configured with four combined queues and their IRQs pinned to cores 0-3. Without this step, results wander from run to run as the kernel shuffles interrupts onto whatever cores the server or client threads happen to be using. If you benchmark at these speeds, pin your IRQs first and thank yourself later.</p>
<pre><code class="language-sh"># Run on both machines. ens50 is the ENA interface name on these instances.
sudo systemctl stop irqbalance
sudo ethtool -L ens50 combined 4
i=0
for irq in $(grep ens50 /proc/interrupts | awk '{print $1}' | tr -d ':'); do
  echo $((i%4)) | sudo tee /proc/irq/$irq/smp_affinity_list > /dev/null
  i=$((i+1))
done
</code></pre>
<p><strong>Client.</strong> We used <a href="https://github.com/cachecannon/cachecannon">valkey-lab</a>, built on cachecannon, with 16 worker threads pinned to cores 4-19, 32 connections, and pipeline depth 1. Reads and writes were measured in separate passes. Read passes prefilled the keyspace and ran at a 100% hit rate. Each short-run cell was a 15-second measurement after a five-second warmup, over a 500-key keyspace with 16-byte keys.</p>
<p>We kept concurrency at 32 connections because AWS caps a single TCP flow at roughly 9.5 Gbps. We verified 9.53 Gbps with iperf3. Saturating a 200 Gbps NIC requires spreading the load across flows.</p>
<p>One invocation per cell, with <code>-s</code> swept across the value sizes:</p>
<pre><code class="language-sh"># GET pass: prefill the 500-key keyspace, then measure at a 100% hit rate.
valkey-lab -h &#x3C;server> -c 32 -P 1 -t 16 --cpu-list 4-19 \
  -n 500 -r 100:0 --prefill --warmup 5s -d 15s \
  -s &#x3C;value_bytes> --key-size 16

# SET pass.
valkey-lab -h &#x3C;server> -c 32 -P 1 -t 16 --cpu-list 4-19 \
  -n 500 -r 0:100 --warmup 5s -d 15s \
  -s &#x3C;value_bytes> --key-size 16
</code></pre>
<p><strong>Scope of the result.</strong> This is a single-node, no-TLS, persistence-disabled test with 32 closed-loop connections, pipeline depth 1, and a 100% hit rate. Most table cells are one 15-second measurement after warmup. The results describe this rig and workload. Broader production claims need repeated runs and additional configurations.</p>
<p><em>We benchmarked whole releases rather than individual changes, so we treat the pull requests named in this post as leading explanations rather than proof of causality.</em></p>
<h2 id="what-this-means-if-you-run-valkey">What this means if you run Valkey</h2>
<p>Valkey 9.x delivers roughly 6x the 64 MB read bandwidth of 8.x while cutting p99 from just under a second to about 230 ms on this single-node, no-TLS test. Our earlier mixed-workload test also found far less collateral latency for small requests when a large read arrived.</p>
<p>The practical takeaway is narrower than “Valkey is always faster.” Valkey 8.1 is essentially flat on this workload, Valkey 9.0 changes the large-GET path, and Valkey 9.1 improves large SETs on this rig. If large values matter to your workload, test 9.x with your object-size distribution, concurrency, TLS, and persistence settings rather than extrapolating from a small-object benchmark, or from this one.</p>
<p>Valkey has consistently improved performance, memory efficiency, and availability at scale. Each version brings about a new set of improvements driven by issues faced by real users in production. A vibrant community where nobody is incentivized to withhold features for the sake of revenue and everyone is incentivized to chase continuous improvement is what makes Valkey truly special. Effort is additive. Consistency is multiplicative.</p>
<p>Serving megabyte-sized objects at wire speed is what I’ve been spending a lot of time on lately. If you are wrangling larger objects, let’s talk. <a href="https://valkey.io/slack/">Join me on Valkey Slack</a>.</p>]]></content:encoded>
  </item>
  <item>
    <title>The concurrency cliff is a memory limit</title>
    <link>https://www.gomomento.com/blog/the-concurrency-cliff-is-a-memory-limit/</link>
  <dc:creator><![CDATA[Khawaja Shams]]></dc:creator>
    <pubDate>Fri, 26 Jun 2026 00:00:00 GMT</pubDate>
  <category><![CDATA[Inference]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/blog/the-concurrency-cliff-is-a-memory-limit/</guid>
    <description><![CDATA[<p>Latency looks fine on p50 until the KV cache fills. Then p99 jumps 15x in one step while your average dashboard shows nothing wrong. The cliff is memory.</p>]]></description>
    <content:encoded><![CDATA[<p>Add concurrent users to an inference server and latency usually creeps up. KV cache serving does not creep. It holds flat, then falls off a cliff.</p>
<p>On a single L4 running a Qwen3-4B coding agent, the server holds 12 concurrent sessions at p99 under 2.6 seconds. Add two more and p99 jumps to 39 seconds, a 15× increase in a single step. The cliff is the moment the KV cache fills.</p>
<h2 id="an-agentic-coding-workload-on-commodity-hardware">An agentic coding workload on commodity hardware</h2>
<p>Our setup is a single <code>g6.4xlarge</code> EC2 instance with one NVIDIA L4 GPU (24 GB VRAM), running <a href="https://huggingface.co/Qwen/Qwen3-4B">Qwen3-4B</a> with FP8 weights on vLLM 0.20.2, automatic prefix caching (APC) enabled.</p>
<p>The workload models a lightweight coding agent mid-task. Each session opens with a 10,000-token shared system prompt (repository context plus agent instructions), followed by a 12-turn conversation where each turn appends about 1,000 tokens of unique context (tool call inputs, code snippets, responses). At turn 12, total context reaches roughly 22,800 tokens per session. Output is capped at 75 tokens per turn. The workload is heavily input-dominated, as agentic workloads tend to be.</p>
<pre><code>System prompt:          10,000 tokens  (shared across sessions → APC cached)
Per-session turns:      ~12,800 tokens across 12 turns  (unique per session)
Output per turn:        75 tokens
—————————————————
Total at turn 12:       ~22,800 tokens
</code></pre>
<p>APC caches the system prompt once and amortizes its KV cost across all sessions (the blocks still occupy GPU memory, but only one copy exists). Within a session, APC also caches the growing turn history. Turn n+1 extends the exact prefix from turn n, so each turn only prefills the new ~1,000 tokens, as long as the prior turns’ KV blocks survive in cache. But the per-session turn history is unique (different code, different tool outputs) and cannot be shared across sessions. When concurrency pressure forces eviction of a session’s blocks, the next request in that session has to re-prefill the full accumulated history (up to ~12,800 tokens at turn 12). That re-prefill cost drives the TTFT cliff.</p>
<p>We swept concurrency from 1 to 48 sessions, measuring TTFT at each level, across three KV cache precisions, fp16, fp8, and TurboQuant 4-bit. We also ran best-case (all context cached) and worst-case (all context re-prefilled) bounds to bracket where realistic performance should land.</p>
<h2 id="the-concurrency-cliff">The concurrency cliff</h2>
<p>The chart below shows TTFT percentiles (p50, p95, p99) and throughput across the full concurrency sweep with fp8 KV cache. The Y axis is logarithmic, and even on a log scale the cliff is sharp.</p>
<p><img src="https://www.gomomento.com/blog/2026-06-26_automatic-prefix-caching/ttft-percentiles.avif" alt="chart: TTFT p50/p95/p99 + throughput vs concurrent sessions, fp8 KV">
<em>TTFT p50, p95, and p99, with throughput on the right axis, versus concurrent sessions. fp8 KV cache, Qwen3-4B FP8 on a g6.4xlarge (L4). The shaded band marks the 12 to 16 session collapse, and the dashed line marks the KV cache filling at 14c.</em></p>
<p>Below 12 concurrent sessions, p99 TTFT stays under 2.6 seconds and throughput climbs to a peak of 1.36 req/s. At 14 sessions, p99 jumps to 38.9 seconds in a single step, a 15× increase, and throughput drops 23 percent at the same time.</p>
<p>At 12 sessions, everything fits in KV cache. At 14, filling it forces eviction of another session’s blocks. That evicted session has to re-prefill its full 12,800 tokens of unique context at its next turn, and the cascade collapses latency.</p>
<p>The cliff sits between 12 and 14 sessions, and throughput never recovers past the peak at 12c. For a 2-second p99 SLA, the safe operating point is 6 concurrent sessions.</p>
<h3 id="after-the-cliff-p50-lies-to-you">After the cliff, p50 lies to you</h3>
<p>Above the cliff, p50 and p99 live in different regimes. At 20c, p50 is 2.2 seconds, which looks manageable, while p99 is 44.8 seconds, which is not. The distribution is bimodal, because APC creates two populations of requests. Lucky requests hit warm cache entries for their session context, prefill only the latest turn, and finish fast. Unlucky requests arrive after their session’s blocks were evicted, re-prefill the full 12,800 tokens, and take 30 to 50 seconds under load.</p>
<p>The p50 reflects the lucky cohort and the p99 reflects the unlucky one. A single TTFT average is meaningless past the cliff. You have to look at the tail to see the failure.</p>
<p>A few points from the sweep show the whole shape, flat through 12 sessions then the cliff at 14 and the widening p50/p99 gap past it.</p>





























































<table><thead><tr><th>Sessions</th><th>TTFT p50</th><th>TTFT p95</th><th>TTFT p99</th><th>req/s</th></tr></thead><tbody><tr><td>1</td><td>388</td><td>487</td><td>493</td><td>0.42</td></tr><tr><td>6</td><td>1.06s</td><td>1.43s</td><td>1.75s</td><td>1.18</td></tr><tr><td>10</td><td>1.19s</td><td>1.89s</td><td>2.31s</td><td>1.33</td></tr><tr><td>12</td><td>1.25s</td><td>2.16s</td><td>2.60s</td><td>1.36</td></tr><tr><td>14</td><td>1.29s</td><td>8.93s</td><td>38.9s</td><td>1.05</td></tr><tr><td>20</td><td>2.25s</td><td>25.5s</td><td>44.8s</td><td>0.68</td></tr><tr><td>48</td><td>5.74s</td><td>40.5s</td><td>43.2s</td><td>0.89</td></tr></tbody></table>
<h2 id="kv-cache-precision-moves-the-knee">KV cache precision moves the knee</h2>
<p>Running the same workload with 16-bit KV cache (vLLM defaults to the model’s dtype, bf16 for Qwen3, when <code>--kv-cache-dtype</code> is not set) halves the token capacity, and the knee shifts left in proportion.</p>

































<table><thead><tr><th>KV dtype</th><th>Bits/element</th><th>Token capacity</th><th>Knee (sessions)</th><th>2s p99 ceiling</th></tr></thead><tbody><tr><td>bf16/fp16</td><td>16</td><td>~89K</td><td>~8</td><td>~4</td></tr><tr><td>fp8</td><td>8</td><td>~178K</td><td>~14</td><td>~6</td></tr><tr><td>TurboQuant 4-bit</td><td>~4.2</td><td>~275K</td><td>~23 (est.)</td><td>pending</td></tr></tbody></table>
<p>The fp16 to fp8 shift is confirmed, with fp16 knees at about 8 and fp8 at about 14, a 1.75× shift for a 2× capacity increase. The slight compression below 2× is expected, since KV management overhead and block table fragmentation consume some of the headroom regardless of precision.</p>
<p><img src="https://www.gomomento.com/blog/2026-06-26_automatic-prefix-caching/ttft-p99.avif" alt="chart: TTFT p99, fp8 KV vs fp16 KV, knees marked">
<em>TTFT p99 for fp8 KV against fp16 KV, same workload and GPU. The markers sit at the observed knees, 8 sessions for fp16 and 14 for fp8. The tq4 knee near 23 is a capacity-based estimate, pending the experiment.</em></p>
<p>Quantization buys concurrency headroom directly. Halving KV precision from fp16 to fp8 nearly doubles how many concurrent sessions fit before the cliff. TurboQuant 4-bit (about 3.8× fewer bytes per element than fp16, partially offset by the lower <code>gpu_memory_utilization</code> it needs for autotuning scratch space) predicts a knee at about 23c, roughly 3× more concurrent sessions than fp16.</p>
<p>The accuracy tradeoff may be small. KV cache quantization at 4-bit typically reports low single-digit perplexity impact, though the exact effect depends on model and task. The knee shifts from about 8 sessions (fp16) to about 14 (fp8) to an estimated 23 (tq4), roughly 3× more sessions before eviction onset, from the same GPU.</p>
<h2 id="best-case-worst-case-realistic">Best case, worst case, realistic</h2>
<p>To separate the latency budget that is fundamental (prefill compute) from the part that is avoidable (cache misses), we ran two controlled bounds alongside the realistic workload.</p>
<p>In the best case (<code>miss_rate=0.0</code>), every request hits the same cached content. APC holds the full 12,800-token session context, so only about 200 unique tokens need prefilling, which is perfect KV utilization.</p>
<p>The worst case (<code>miss_rate=1.0</code>) gives every request a unique prefix that breaks APC for the user context. The 10K system prompt still hits the cache, but all ~12,800 tokens of per-session turn history are re-prefilled on every request. Every miss lands at peak session depth (turn 12), forcing the maximum re-prefill cost each time.</p>
<p><img src="https://www.gomomento.com/blog/2026-06-26_automatic-prefix-caching/ttft-p50.avif" alt="chart: TTFT p50, best vs realistic vs worst, log scale">
<em>TTFT p50 for best (fully cached), realistic (natural APC), and worst (always re-prefilled), on a log scale. The band between best and worst is the envelope any real workload lands in.</em></p>
<h3 id="what-the-bounds-tell-us">What the bounds tell us</h3>
<p>At a single concurrent session, with zero contention, the three bounds separate cleanly.</p>

























<table><thead><tr><th>Workload</th><th>TTFT p50</th><th>What’s happening</th></tr></thead><tbody><tr><td>Best (cached)</td><td>57 ms</td><td>Only ~200 unique tokens prefilled; rest is cached</td></tr><tr><td>Realistic (APC)</td><td>388 ms</td><td>System prompt cached; 12,800 unique tokens prefilled</td></tr><tr><td>Worst (evicted)</td><td>2,500 ms</td><td>System prompt cached; ~12,800 user tokens re-prefilled at peak depth every request</td></tr></tbody></table>
<p>On an absolute scale, realistic (388 ms) is much closer to best (57 ms) than to worst (2,500 ms). But realistic is still 7× slower than best. That gap is the cost of prefilling about 12,800 tokens of per-session unique context on each request. APC removes the system prompt cost, but the per-session turn history still has to be computed.</p>
<p>The gap between realistic and worst is about miss depth. In the realistic workload, cache misses happen at any turn. A session evicted at turn 3 re-prefills about 3,000 tokens, while eviction at turn 12 costs about 12,800. The worst case forces every miss to peak session depth, paying the maximum re-prefill on every request. Real traffic produces a distribution of miss depths, which is why realistic latency stays close to best.</p>
<p>The best-case result is the surprising one. With perfectly cached session context, the L4 handles more than 48 concurrent sessions within a 2-second p99 SLA. The 6 session realistic ceiling is the cost of per-session context uniqueness, the turn histories that cannot be shared. It is not a GPU compute limit.</p>
<p>The worst case grows linearly at about 2.1 seconds per additional concurrent session, reaching 102 seconds at 48c. Throughput saturates at 0.32 req/s from 6 sessions onward. The GPU is fully consumed re-prefilling 12,800 tokens per request, and extra concurrency just lengthens the queue.</p>
<h2 id="why-the-knee-is-where-it-is">Why the knee is where it is</h2>
<p>The L4 has 24 GB of VRAM, but far less than that is available for KV cache. The memory that actually holds KV cache is roughly half of raw VRAM.</p>
<h3 id="where-the-memory-goes">Where the memory goes</h3>
<p>vLLM’s <code>gpu_memory_utilization</code> was set to 0.9 for the fp8 and fp16 experiments, reserving about 21.6 GB. After model weights, CUDA graph capture, activation tensors, and block table overhead, about 13 GB remains for KV cache. The TurboQuant experiment used 0.8 (it needs about 2 GB of extra scratch for torch.inductor autotuning at startup), leaving about 10.6 GB.</p>
<h3 id="kv-cache-per-token">KV cache per token</h3>
<p>Qwen3-4B uses GQA with 36 layers, 8 KV heads, and head_dim 128. The per-token KV cache size depends on precision.</p>
<pre><code>2 (K+V) × 36 layers × 8 KV heads × 128 head_dim × bytes_per_element

FP16/BF16: ... × 2 bytes = 147,456 bytes/token  → ~89K tokens in ~13 GB
FP8:       ... × 1 byte  =  73,728 bytes/token  → ~178K tokens in ~13 GB
TQ4:       ~0.53 B effective (4-bit + quantization metadata)
           =  ~38,700 bytes/token  → ~275K tokens in ~10.6 GB
</code></pre>
<h3 id="the-capacity-arithmetic-with-apc">The capacity arithmetic (with APC)</h3>
<p>With APC, the 10,000-token system prompt is stored once and shared. Only the per-session unique context (about 12,800 tokens at peak depth) needs its own blocks.</p>
<pre><code>FP8 KV
Token budget:     ~178K
Shared prefix:     10K (1×)
Available:        ~168K
Per-session:      ~12.8K
Max sessions:   168K / 12.8K ≈ 13      Observed knee: ~14c

FP16 KV
Token budget:      ~89K
Shared prefix:     10K (1×)
Available:         ~79K
Per-session:      ~12.8K
Max sessions:    79K / 12.8K ≈ 6       Observed knee: ~8c

TQ4 KV (estimated)
Token budget:     ~275K (0.8 util)
Shared prefix:     10K (1×)
Available:        ~265K
Per-session:      ~12.8K
Max sessions:   265K / 12.8K ≈ 21      Predicted knee: ~23c
</code></pre>
<p>The arithmetic predicts the knees within 1 to 2 sessions of the observed values. The slight overshoot (observed 14 sessions against predicted 13) is because sessions are not all at peak depth at once. Earlier turns have smaller contexts, which buys a few extra sessions before capacity runs out.</p>
<p>In this setup, the concurrency cliff is a memory limit. The binding constraint is how many sessions’ KV caches fit in VRAM at once. The best-case bound supports this. With perfect caching, the same GPU handles more than 48 sessions within 2-second p99. Compute, scheduling, and continuous batching also contribute, but memory capacity sets the ceiling.</p>
<h2 id="how-to-find-the-knee-for-your-workload">How to find the knee for your workload</h2>
<p>The knee location depends on three variables. Available KV cache memory is total VRAM minus model weights, CUDA graphs, activations, and fragmentation, typically about half of raw VRAM, and vLLM reports the exact number at startup. Per-session unique context is the total session tokens at peak depth, minus any shared prefix cached by APC. KV precision is the bytes per element, where halving it from fp16 to fp8 to 4-bit roughly doubles token capacity at each step and shifts the knee right.</p>
<p>The estimate is max concurrent sessions ≈ (token capacity − shared prefix) / per-session unique context.</p>
<p>For this setup (Qwen3-4B, L4, 22.8K-token agentic sessions with a 10K shared prefix), the arithmetic predicts about 13 sessions (fp8) and 6 (fp16). The observed knees are about 14 and 8. The arithmetic gives a first-order estimate, and a concurrency sweep gives the precise number. The gap between estimate and observation comes from session depth staggering, block fragmentation, and APC reuse patterns.</p>
<p>Different workloads shift each variable. A single-turn QA workload with 2K tokens per session has a much higher knee. A code review agent with 50K-token inputs has a much lower one. A GPU with more VRAM (A100, H100) raises the budget. The method is the same. Estimate the budget, divide by per-session cost, then verify with a sweep.</p>
<h2 id="what-this-means-for-deployment">What this means for deployment</h2>
<p>Know your KV budget before you set your concurrency limit. Below the knee you get the best throughput with stable latency and effective caching. Above it you get worse throughput, worse latency, and a wasted APC investment.</p>
<p>KV quantization is a direct concurrency multiplier. On this L4, switching from fp16 to fp8 KV cache moves the 2-second p99 SLA ceiling from about 4 to 6 (50 percent more sessions) and the eviction knee from about 8 to 14 (75 percent more sessions). The gain is a direct consequence of halving the bytes per KV element. Quantization buys memory, and memory buys concurrency.</p>
<p>Monitor tail latency, not averages. After the cliff, p50 looks manageable while p99 is catastrophic. The bimodal distribution means some users get sub-second responses while others wait 40-plus seconds, and an average-based dashboard hides it until users complain.</p>
<p>If some of your sessions are latency-tolerant background work, run them off the interactive path. They do not need to compete for cache memory, and keeping them off it frees KV budget for the sessions that need low TTFT.</p>
<p>Several caveats temper the numbers. These experiments use synthetic token content, not real code. The workload has a fixed 12-turn structure, while real agent sessions vary widely in depth. Poisson arrivals do not capture bursty agentic traffic, where agents send follow-up requests immediately. p99 at high concurrency is noisy, since with about 200 requests per run it is only the second-worst request. Chunked prefill, not enabled here, could smooth the knee transition. The numbers are specific to a single L4 with Qwen3-4B. Larger models, multi-GPU setups, and different context lengths shift the absolute numbers while the pattern holds.</p>
<p>KV cache behaves like a systems problem, and the concurrency knee is where that meets a specific GPU, a specific model, and a specific workload shape. The math is simple. The discipline is running it before production tells you the hard way.</p>]]></content:encoded>
  </item>
  <item>
    <title>Your KV cache benchmark is “hi hi hi”</title>
    <link>https://www.gomomento.com/blog/your-kv-cache-benchmark-is-hi/</link>
  <dc:creator><![CDATA[Khawaja Shams]]></dc:creator>
    <pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate>
  <category><![CDATA[Inference]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/blog/your-kv-cache-benchmark-is-hi/</guid>
    <description><![CDATA[<p>Default KV cache benchmarks run on repetitive "hi hi hi" text, which makes compression and transfer look far better than they do on real workloads.</p>]]></description>
    <content:encoded><![CDATA[<p>Before you commit to a KV cache offloading system, you benchmark it to make sure it performs well below your SLA. You see that it has excellent compression and cheap transfers. Seems like an easy win.</p>
<p>But there’s some trouble with what standard KV cache benchmarks run on.</p>
<p>LMCache ships with a <a href="https://docs.lmcache.ai/getting_started/benchmarking.html">long-document benchmark</a> for measuring KV cache offloading performance. Run it without a corpus file and it generates documents like this:</p>
<pre><code class="language-python">warmup_prompts = [
  str(i) + " " + " ".join(["hi"] * args.document_length)
  for i in range(args.num_documents)
]
</code></pre>
<p>A 10,000-token document comes out looking a little underwhelming.</p>
<p><code>0 hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi …</code></p>
<p>Technically speaking, it is the 10K token count you were looking for, but it doesn’t represent a real 10K token workload.</p>
<p>KV cache systems do not run on token count alone. Compression ratios, activation patterns, transfer sizes, and cache behavior all depend on the shape of the input. Two documents of the same length can be two entirely different workloads.</p>
<p>Unfortunately, much of the current KV cache ecosystem is benchmarked on synthetic inputs that look nothing like the workloads people run.</p>
<h2 id="the-benchmark-is-not-representative">The benchmark is not representative</h2>
<p>The default benchmark document contains a numeric identifier and the token “<em>hi</em>” repeated thousands of times.</p>
<p>Transformers do not produce identical activations for repeated tokens. Positional encoding, attention mixing, and residual connections keep every position distinct. To an LLM, <em>distinct</em> and <em>varied</em> are not the same thing. Repeating a single token produces far more regular activation patterns than diverse text does.</p>
<p>Compression improves, transfer sizes shrink, and cache behavior becomes easier to predict with non-varied workloads. The benchmark is measuring <em>something</em>, but it is not measuring a realistic production workload.</p>
<p>Benchmarking KV cache offloading with “hi hi hi” is like benchmarking a database with <code>SELECT 1</code>. The numbers come back fast, but they do not tell you much about real workloads.</p>
<h2 id="the-difference-shows-up-immediately">The difference shows up immediately</h2>
<p>We compared the default benchmark document against a realistic medical document using <a href="https://huggingface.co/Qwen/Qwen3-8B-FP8">Qwen3-8B-FP8</a>. Both ran about 10,000 tokens. The synthetic one carried two unique tokens, a token diversity of 0.02 percent. The medical one carried 1,329, or 13.3 percent. The token count is the same. Everything else is different.</p>
<p>Token diversity affects activation patterns. Activation patterns affect tensor value distributions. Tensor distributions affect compression ratios and transfer sizes. A benchmark built from highly repetitive inputs can diverge sharply from one built on realistic text.</p>
<h2 id="compression-behaves-differently-too">Compression behaves differently too</h2>
<p>We first ran into this while building a Valkey-backed KV cache connector. We followed the LMCache tutorials and used dummy weights, and compression ratios looked excellent. Then we switched to Qwen3-8B-FP8 with real trained weights, and the results were night and day.</p>
<p>Real model weights produce tensor values that behave more like high-entropy floating-point data. General-purpose compression still helps, but the gains are smaller than they appear with dummy weights. Repetitive inputs create more structured activation patterns that compress more effectively, making the benchmark results appear better than they actually are.</p>
<h2 id="so-we-built-a-more-realistic-corpus">So we built a more realistic corpus</h2>
<p>To see how KV cache systems behave under representative inputs, we built a corpus of 30 long-form documents across medical and legal domains. We wanted documents that resemble the structure, formatting, vocabulary, and variability that real systems process.</p>
<p><img src="https://www.gomomento.com/blog/2026-06-24_kv-cache-benchmark/corpus.avif" alt="Example medical and legal document text"></p>
<p>Medical documents averaged 14.2 percent token diversity. Legal documents averaged 9.4 percent. Both are hundreds of times more diverse than the synthetic baseline at 0.02 percent.</p>
<p>The corpus holds 300,000 tokens and was generated with Qwen3-8B-FP8. The corpus and generation scripts are <a href="https://github.com/momentohq/kvcache-corpus">open source</a> for those interested.</p>
<h2 id="benchmarking-the-workload-you-actually-have">Benchmarking the workload you actually have</h2>
<p>While token count is easy to generate, it can also be the least informative. Match the diversity, structure, and vocabulary of the text your system serves, and the compression ratios and transfer sizes start to represent values you can trust.</p>
<p>The input comes first. Before ranking cache connectors, compression schemes, or storage backends, run them on inputs that look like your traffic. Compare them on “hi hi hi” and you are ranking them on a workload nobody runs.</p>
<p>Before you evaluate your next KV cache offloading system, be sure to ask “Are the benchmark documents representative of the workload I actually run?”</p>]]></content:encoded>
  </item>
  <item>
    <title>vLLM's Hash Chain and Why Prefix Caching Is Still Prefix Caching</title>
    <link>https://www.gomomento.com/blog/prefix-caching-is-still-prefix-caching/</link>
  <dc:creator><![CDATA[Khawaja Shams]]></dc:creator>
    <pubDate>Mon, 22 Jun 2026 00:00:00 GMT</pubDate>
  <category><![CDATA[Inference]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/blog/prefix-caching-is-still-prefix-caching/</guid>
    <description><![CDATA[<p>Automatic prefix caching, hash chains, radix trees. The data structures get cleverer, but we haven’t moved past shared prefixes.</p>]]></description>
    <content:encoded><![CDATA[<p>Automatic Prefix Caching sounds like it should solve a bigger problem than plain prefix caching. Requests are hashed, cache entries are discovered automatically, and shared work never has to be tracked by hand. It looks like a system that can find reusable computation wherever it appears.</p>
<p>But it’s still the same type of reuse we’ve always had. Shared prefixes are reusable. Shared content that is not a prefix is not. That rule sets the biggest limit on what today’s inference infrastructure can reuse.</p>
<p>Most of the recent work in KV caching has focused on finding prefixes more efficiently. Hash chains, radix trees, and automatic discovery all improve the mechanics of reuse. But they don’t change what can be reused. The workloads where it succeeds, and where it falls short, show why.</p>
<h2 id="when-prefix-caching-is-enough">When prefix caching is enough</h2>
<p>Reusing the KV cache only pays off when requests share computation, so the question is how much of a real workload today’s infrastructure can reuse.</p>
<p>For many agentic workloads, the answer is often “enough”. Stable system prompts, tool definitions, and conversation scaffolding create long shared prefixes, and inference engines are good at finding and reusing them.</p>
<p>Plenty of workloads share more than just prefixes, though. RAG pipelines are where it breaks down. The system prompt stays fixed while the retrieved documents change from request to request. Two requests might carry the same five documents in a different order. The meaning is almost identical. The token sequence is not, and the cache matches on the token sequence. Same content, different positions, no reuse.</p>
<h2 id="why-prefix-caching-remains-prefix-bound">Why prefix caching remains prefix-bound</h2>
<p>vLLM’s <a href="https://docs.vllm.ai/en/v0.20.1/features/automatic_prefix_caching/">Automatic Prefix Caching</a> uses content hashing to remove the need for explicit prefix tracking. The KV cache is divided into fixed-size blocks, and each block gets a SHA-256 hash. The hash for block N folds in the hash of every preceding block along with the content of the current block. Chained together, every block fingerprints not only its own content but the entire token history before it.</p>
<p>When a request arrives, vLLM hashes each block-sized chunk of input and checks whether a matching block already exists. Matching blocks reuse previously computed KV cache. Missing blocks trigger fresh computation. Lookup is effectively constant-time, eviction is straightforward, and fixed-size blocks map cleanly onto GPU memory.</p>
<p>But because each block hash depends on every block before it, one divergence changes every hash that follows. Take two requests that share the first 3,000 tokens and split at token 3,001. The block holding token 3,001 hashes differently, and so does every block after it. Reuse stops at the point of divergence. The system discovers shared prefixes on its own, and it can’t discover shared content that appears once requests have diverged.</p>
<p>Reuse happens only at block boundaries. If two requests share 1,000 tokens and a block holds 16, vLLM reuses 62 whole blocks, or 992 tokens, and recomputes the remaining 8. For long prefixes that waste is negligible. For short or irregular shared segments it’s more substantial.</p>
<p>There is no matching inside a block, either. Two blocks that share 15 of their 16 tokens still hash to completely different values, so reuse is all-or-nothing at the block level. These are reasonable tradeoffs that keep the implementation simple and fast, but they still leave you with the same limitation: reuse follows exact prefix structure.</p>
<p>A different cache structure might help. <a href="https://docs.sglang.io/">SGLang</a> takes that route. Instead of hashing fixed-size blocks, it keeps cached state in a <a href="https://www.lmsys.org/blog/2024-01-17-sglang/">radix tree indexed by token sequences</a>. When a request arrives, the runtime walks the tree and finds the longest matching cached prefix, and matches can fall on arbitrary token boundaries rather than fixed block ones. That helps workloads with variable-length turns, branching conversations, and irregular prefix lengths.</p>
<p>The radix tree still searches for the longest shared prefix, though. Once two requests diverge, the content they share later in the sequence stays out of reach. SGLang improves how prefixes are discovered, but it does not extend reuse past prefixes.</p>
<h2 id="beyond-prefixes">Beyond prefixes</h2>
<p>Prefix caching tells us that KV reuse clearly works. The more open question now is how much reuse survives divergence. Today’s runtimes are tuned for shared prefixes. The next generation goes after shared segments, cache repair, and the reuse that prefix matching cannot reach.</p>
<p>So much of the current research now focuses on cache repair and segment-level reuse. The goal has shifted from proving that KV reuse is valuable to recovering the work that today’s prefix-based systems still leave behind.</p>]]></content:encoded>
  </item>
  <item>
    <title>Disaggregation makes KV cache a system primitive</title>
    <link>https://www.gomomento.com/blog/disaggregation-makes-kv-cache-a-system-primitive/</link>
  <dc:creator><![CDATA[Khawaja Shams]]></dc:creator>
    <pubDate>Fri, 19 Jun 2026 00:00:00 GMT</pubDate>
  <category><![CDATA[Inference]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/blog/disaggregation-makes-kv-cache-a-system-primitive/</guid>
    <description><![CDATA[<p>Prefill and decode want different hardware. Separate them and the KV cache becomes the state that connects the two, which turns it from an implementation detail into a system design concern.</p>]]></description>
    <content:encoded><![CDATA[<p>Inference is scaling faster than the serving architectures around it. Prefill and decode are often treated as one pipeline, but they are fundamentally different workloads.</p>
<p>Prefill is compute-heavy, with an order of magnitude more arithmetic intensity than decode. Decode is sensitive to memory bandwidth and to latency. Prefill wants high-FLOPS accelerators. Decode wants large, fast memory. Put both on the same GPU and you tune it for one profile while it absorbs the other, and under load prefill interferes with decode. Neither phase gets the hardware it would choose.</p>
<h2 id="the-cache-becomes-the-connection">The cache becomes the connection</h2>
<p>Disaggregation separates these two phases. Prefill nodes do prefill, decode nodes do decode, each on hardware suited to its bottleneck.</p>
<p>Separation removes the interference, but it opens a gap. Inside one accelerator, prefill flows straight into decode and the intermediate state never leaves the chip. Pull the two onto different machines and that state, the KV cache, has to be handed across. Prefill produces it, decode consumes it, and disaggregation turns it into the object that connects them.</p>
<p>On a single node, the KV cache is largely an implementation detail the inference engine manages for you. But once prefill and decode are separate systems, the cache is the connection between them, and every request depends on getting it from one to the other.</p>
<h2 id="what-the-split-asks-of-the-cache">What the split asks of the cache</h2>
<p>Once the cache has to travel between machines, it takes on the requirements of any object moving through a distributed system.</p>
<p>The cache has to move from prefill nodes to decode nodes, which makes transfer latency, serialization format, and network bandwidth first-order concerns. It has to land in the right place, so deciding which decode node receives which cache turns routing into a scheduling problem. It has to expire, which raises the question of who evicts an entry and when, work the engine handled in a colocated system and that now needs coordination. And it has to live somewhere across GPU memory, host memory, NVMe, and remote storage, each tier with its own latency and capacity tradeoffs.</p>
<p>These are distributed systems problems that present themselves whenever you separate storage and compute. What was colocated becomes independently addressable, connected by a transfer layer. The techniques for solving them, placement, routing, eviction, and tiered storage, are well understood. What is new is applying them to the KV cache inside inference serving.</p>
<h2 id="from-implementation-detail-to-system-design">From implementation detail to system design</h2>
<p>The major inference stacks are already built around this split. <a href="https://developer.nvidia.com/dynamo">NVIDIA Dynamo</a> describes disaggregated inference as a prefill engine that computes the prefill phase and generates KV cache, hands that cache to a decode engine, and lets the decode engine run the decode phase. AWS is building the split into its infrastructure, with <a href="https://aws.amazon.com/machine-learning/trainium/">Trainium</a> for compute-heavy prefill, and the <a href="https://www.cerebras.ai/blog/cerebras-is-coming-to-aws">Cerebras partnership</a> likely points the same way, since wafer-scale SRAM suits memory-bound decode.</p>
<p>In each of these, the KV cache is the object the tiers hand between them. On a single node it was an optimization you could run, skip, or tune, and the architecture did not care. Disaggregation moves those same problems up a level, from implementation details the engine used to hide to system design concerns the architecture has to own. The cache now has to be transferred, routed, stored, and expired across machines, and the whole system is built around getting it from prefill to decode.</p>
<p>What began as transient state inside a single request becomes the handoff between two systems. Once we have that handoff, cache management becomes a first-class primitive of the architecture.</p>]]></content:encoded>
  </item>
  <item>
    <title>KV Caching Pays Off Under Load</title>
    <link>https://www.gomomento.com/blog/kv-caching-pays-off-under-load/</link>
  <dc:creator><![CDATA[Khawaja Shams]]></dc:creator>
    <pubDate>Tue, 16 Jun 2026 07:00:00 GMT</pubDate>
  <category><![CDATA[Inference]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/blog/kv-caching-pays-off-under-load/</guid>
    <description><![CDATA[<p>KV cache starts out as an implementation detail of inference. As inference systems evolve, it is becoming a first-class systems primitive.</p>]]></description>
    <content:encoded><![CDATA[<p>KV caching looks like a bad trade on paper. Memory, complexity, and operational surface area, all spent to shave a few percent off a request.</p>
<p>The benchmarks do not rescue it.</p>
<p>We’ve seen teams leave it at that. KV cache is necessary inside a single forward pass, so you keep it for the life of the request and move on. Keeping it alive beyond that, reused across requests, starts to sound like a serving-layer luxury. You picture the memory it would pin, the eviction logic, the extra moving parts in a stack that is already hard enough to operate. Set that against a few percent of latency and the trade does not look worth making.</p>
<p>Understandably so. Run the numbers on a single request and long-lived KV caching underwhelms. We ran them, and at first it was a very unflattering story. But a single request is the wrong unit to judge this on, and once you measure at production load the economics turn. </p>
<h2 id="the-single-request-savings-are-bounded">The single-request savings are bounded</h2>
<p>In one of our <a href="https://ollama.com/library/qwen3:30b-a3b">Qwen3-30B-A3B</a> runs, a 1K input / 512 output request came in around 135 ms TTFT and about 2.5 seconds of total request latency. TTFT carries scheduling and queueing overhead on top of raw prefill compute, so call the prefill portion roughly 100 ms. Erase it completely, the most a perfect cache hit can do, and you save about 4 percent of the request. Treat that as the ceiling, not the everyday case.</p>
<p>As context grows, so does prefill’s share of the total request latency. At 16K input / 512 output, TTFT was about 769 ms of 3,200 ms total, which puts it near 24 percent. That is a real step up from the 1K case. The input/output ratio affects prefill more than the context length does. KV cache earns the most when a request carries a large input and returns a small output, because prefill is then a bigger share of the bill. In scenarios where you have short input and long output, decode takes over while the cache has little room to help.</p>
<p>On its own, a 4 or 24 percent share looks modest. Latency is measured at a target throughput, GPU capacity is scarce, and as throughput climbs, utilization, queueing, and pipeline stalls increase the cost of redoing prefill. So in production, prefill becomes a capacity and tail-latency problem once thousands of requests compete for the same accelerators.</p>
<p>So the skepticism is fair, for the single request. If the only question is whether one cache hit meaningfully cuts one request’s latency, the answer is often no, and it depends on the input/output ratio and how much of the request prefill actually owns. At that level, KV caching is not an automatic win.</p>
<p><img src="https://www.gomomento.com/blog/2026-06-16_kv-caching-pays-off-under-load/prefill-vs-decode.avif" alt="Prefill vs decode ratios"></p>
<p>Large-input, small-output workloads are getting more common, not less. Agentic workloads are multi-turn by nature. Context grows as chat history, tool-call results, and retrieval chunks pile up, so each new turn carries a larger input against a small output. Exactly the type of workload where prefill dominates and where reusing the cache has the most to give.</p>
<p>For application teams, reusing the cache shows up as lower TTFT, tighter p95, and lower cost per request. For the platform teams running the GPUs, it shows up as higher utilization and more capacity per dollar. </p>
<h2 id="expensive-to-hold-hard-to-reuse">Expensive to hold, hard to reuse</h2>
<p>That said, the KV cache is not free to keep. Hold it in GPU memory and it competes with active inference for the scarcest space you have. Move it to host memory and it is cheaper but still bounded, with DRAM prices trending the wrong way. Push it out to remote memory or storage and you take on transfer latency, placement problems, and more operational surface. KV caching is a bet. You are spending scarce memory on the wager that future requests reuse the work you are holding.</p>
<p>But that’s only half the problem. Even when you are willing to pay for the memory, the reuse you get back is limited. The production-friendly option today is prefix caching: if a later request begins with the exact same prefix, the engine reuses the KV cache already computed for it. The rule is strict, exact prefix match or nothing. Plenty of real workloads share meaning without sharing a prefix. Reordered retrieval chunks, varying tool results, and shifting user context carry the same semantic content in different positions, and none of it counts as a hit, so hit rates suffer. </p>
<p>It might seem like KV caching has a lot going against it. The single-node latency win is bounded. The memory cost is high. The reuse model is narrow. Evaluate it as an isolated optimization on one node and the honest question is whether the complexity earns its place. In isolation, often it does not. But isolation is the wrong frame because inference is not the system it was when those objections were formed. Each of them was measured against a single node running prefill and decode together, holding a cache that was large and expensive to keep. Two things have shifted since. The first is structural, in where prefill and decode run. The second is economic, in what the cache costs to hold and to move. Each one undercuts a different piece of the case against. </p>
<h2 id="inference-is-becoming-a-distributed-systems-problem">Inference is becoming a distributed systems problem</h2>
<p>Prefill and decode are not the same kind of work. Prefill is compute-heavy. Decode is sensitive to memory bandwidth and to latency. Put both on the same accelerator and you force a compromise on one to serve the other. Split them, and you create a clean boundary between two workloads that want different things. The KV cache, however, has to cross that boundary.</p>
<p>When prefill and decode live on different hardware, the KV cache becomes a first-class distributed systems primitive, something you transfer, place, and manage a lifecycle for. NVIDIA Dynamo and the disaggregated stacks coming out of AWS and Cerebras are building the split into the infrastructure itself, which is what forces developers to think about how the KV cache moves, where it lives, and how long it stays alive.</p>
<h2 id="the-economics-are-starting-to-move">The economics are starting to move</h2>
<p>The second shift is economic. The KV cache itself is getting more efficient to store and to move. A surprising amount of recent model progress is really KV cache innovation, and the last 18 months have been striking. </p>
<p><a href="https://arxiv.org/abs/2405.04434">DeepSeek-V2 and V3</a> introduced <a href="https://mccormickml.com/2025/04/26/inner-workings-of-mla/">Multi-head Latent Attention (MLA)</a>. MLA compresses keys and values into a shared low-rank latent vector before anything gets cached. For V3, that takes the per-token cache from roughly 16,384 scalar dimensions under standard multi-head attention down to 576, a 512-dimensional latent plus 64 dimensions for decoupled RoPE. Against an MHA baseline that is about a 28x reduction. Against the GQA baseline most modern models already use, it is smaller, roughly 4 to 8x depending on group size, and MLA gets there while holding MHA-level quality, which GQA gives up.</p>
<p><a href="https://qwen.ai/blog?id=qwen3.5">Qwen 3.5</a> goes a different way with a <a href="https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/08_deltanet/README.md">Gated DeltaNet hybrid</a>. It replaces 75 percent of its attention layers with Gated DeltaNet linear attention, layers that hold a fixed-size state matrix, 128 by 128 per head, and update it incrementally with each token. The state does not grow with sequence length. Only the remaining 25 percent, full softmax attention with GQA, still needs a traditional KV cache. At long contexts, where the KV cache usually dominates memory, this removes most of the growth. The payoff scales with context: substantial at 256K tokens, modest at 1K, where a fixed-size state costs about what a small KV cache would anyway.</p>
<p><a href="https://research.google/blog/turboquant-redefining-ai-efficiency-with-extreme-compression/">TurboQuant and PolarQuant</a>, from Google at ICLR 2026, take yet another angle. Instead of changing the attention mechanism, they quantize the KV cache itself to 3 or 4 bits per coordinate with no measurable accuracy loss on standard benchmarks. PolarQuant rotates vectors with a random orthogonal matrix so the coordinates follow a known distribution, then applies an optimal Lloyd-Max scalar quantizer, and QJL adds a 1-bit residual correction. At 4 bits the paper reports up to 8x faster attention on an H100. At 3 bits, roughly 6x memory reduction.</p>
<p>The exact numbers depend on baselines and configurations, but the direction is obvious. MLA shrinks the cache dramatically. Hybrid architectures such as Qwen’s Gated DeltaNet reduce cache growth across much of the network. Quantization approaches such as TurboQuant reduce memory requirements further without changing the model architecture. Different tradeoffs, same trend: the object is getting smaller. </p>
<p>Memory cost is the usual objection to KV caching, but this recent work almost makes it moot. Shrink the cache by 6x to an order of magnitude and the economics look very different. More entries fit in the same budget. Transfers from remote memory, SSD, or another node get faster. There’s a misconception that the cache has to become trivially small. But it only has to get small enough that the economics cross over for the workloads people run in production.</p>
<p>The storage hierarchy is changing as well. The previous thought was if the KV cache is not in GPU memory, it is too slow to matter. That is getting harder to say. Fast interconnects and local NVMe continue to improve. Now the question is whether moving or loading the cache can free the decode GPU from repeated prefill work and keep it pointed at the latency-sensitive part of the pipeline. If a storage-backed cache lowers prefill pressure and keeps accelerator capacity on decode, it can pay off quickly.</p>
<p>The workload is the main success driver. For a given model and context length, the system weighs the time to recompute prefill against the time to move the cache over the network, the time to read it from SSD, the cost of reserving the memory or storage, and the odds the cache gets reused at all. When transfer or load time comes in well under recompute time and reuse is likely enough, the cache earns its place. When it does not, the cache is a cost with no return.</p>
<h2 id="prefix-caching-under-load">Prefix caching under load</h2>
<p>We saw this behavior in an experiment we recently ran. We used Qwen3-1.7B on an L40S, a 10K-token prompt, and the number of shared prefix tokens varied from 0 to 10K across several concurrency levels. As the shared prefix grows, the vLLM prefix cache hit ratio climbs from 0 to 1. Throughput and request latency were monitored at each concurrency level.</p>
<p><img src="https://www.gomomento.com/blog/2026-06-16_kv-caching-pays-off-under-load/plot_cache_hit_ratio.avif" alt="Plot graph of cache hit ratio"></p>
<p><em>Prefix cache hit ratio grows linearly with shared prefix length, from 0 to 10K tokens against the fixed 10K-token prompt.</em></p>
<p><img src="https://www.gomomento.com/blog/2026-06-16_kv-caching-pays-off-under-load/plot_rps.avif" alt="Plot graph of shared prefix and concurrency"></p>
<p><em>Throughput rises as the shared prefix grows and is sharpest at high concurrency, where skipping repeated prefill lets the system sustain more requests per second.</em></p>
<p><img src="https://www.gomomento.com/blog/2026-06-16_kv-caching-pays-off-under-load/plot_request_latency.avif" alt="Plot graph of request latency"></p>
<p><em>Mean request latency falls as reuse increases, with the high-concurrency settings improving most.</em> </p>
<p>At low concurrency, a higher hit ratio helps, but only a little. At high concurrency, the same increase results in a much larger system effect. Requests per second climb sharply as more of the prompt comes from cache, and mean latency decreases along with it, dropping fastest at the higher concurrency levels. That is the behavior one would expect if KV caching is a systems optimization rather than a single-request latency trick.</p>
<p>The experiment is a small model on a single node, so it does not prove the disaggregated-architecture argument on its own. But it does verify that prefix cache hits remove prefill work from the serving path, and the system-level benefit grows with concurrency. The disaggregation thesis is that this gets stronger when prefill and decode run on separate hardware and the KV cache moves between them as a first-class object.</p>
<h2 id="prefix-caching-already-works-for-the-right-workloads">Prefix caching already works for the right workloads</h2>
<p>Prefix caching generally has a narrow sharing mode. For agentic workflows it fits more naturally than you might expect, though the reason it fits changes by category of context.</p>
<p>System prompts are the easy case. They are stable across requests, they sit at the front of the prompt, and are a textbook prefix hit. An agent making a series of tool calls against the same backend reuses the same 2K to 8K token system prompt on every request. A multi-turn conversation with a fixed system prompt reuses the whole instruction block. A code-generation agent with stable repository context reuses the project description and file summaries. For this category, cross-request caching is straightforward.</p>
<p>Other kinds of context ask for more care. Chat history grows and shifts from turn to turn. Tool-call exemplars get reordered or swapped. Retrieval chunks change with every RAG query. These often share material across requests <em>without sharing an exact prefix</em>, so the effectiveness of the cache comes down to how much of the context is positionally stable (which prefix caching handles), versus variable (which needs something like CacheBlend to unlock). </p>
<h2 id="research-for-a-better-solution">Research for a better solution</h2>
<p>For messier patterns, like RAG with retrieval chunks that vary by query or tool results that differ between calls, two requests can share a great deal of material without sharing the exact same prefix, and classic prefix caching returns a miss in those cases even when most of the computation could have been reused.</p>
<p><a href="https://arxiv.org/abs/2405.16444">CacheBlend</a> is one of the research directions in this area. It is exploring the idea of <em>cache repair</em>, which takes a semantically similar cached entry to what the current request needs, and selectively recomputes only the parts that differ. If repair is cheap enough, individual caches become reusable across more requests and hit rates rise without spending more memory.</p>
<p>This is still in open research, it’s not solved yet. No major inference framework ships chunk-level KV reuse today. The selective recomputation carries its own latency, quality preservation depends on the workload, and the methodology for measuring these tradeoffs is still maturing. But the direction is promising. More flexible cached entries raise the effective hit rate inside the same memory budget.</p>
<p>Prefix caching answers “<em>does caching work</em>?” for a growing number of workloads. The open question is how much of the rest can be brought into the cacheable regime, and cache repair is where we are working that out.</p>
<h2 id="from-per-request-state-to-systems-primitive">From per-request state to systems primitive</h2>
<p>If you evaluate KV caching as an isolated optimization on a single node, it doesn’t make much sense. The memory cost is high and prefix-based reuse is limited.</p>
<p>But the architecture underneath is changing. Disaggregated prefill and decode create the right interface. Better attention mechanisms shrink the object you have to store and move. Faster networks and SSDs reduce transfer costs. Cache repair could push reuse beyond strict prefixes. Scarce GPU capacity makes repeated prefill work harder to justify.</p>
<p>Together, these shifts are turning the KV cache from a temporary intermediate state into an inference systems primitive.</p>]]></content:encoded>
  </item>
  <item>
    <title>Beyond the Goals, Three Ways Momento Scales the Football World Cup in Real Time</title>
    <link>https://www.gomomento.com/blog/beyond-the-goals-three-ways-momento-scales-the-football-world-cup-in-real-time/</link>
  <dc:creator><![CDATA[Lionel Bringuier]]></dc:creator>
    <pubDate>Wed, 03 Jun 2026 16:14:30 GMT</pubDate>
  <category><![CDATA[Media & Entertainment]]></category>
    <guid isPermaLink="true">https://www.gomomento.com/blog/beyond-the-goals-three-ways-momento-scales-the-football-world-cup-in-real-time/</guid>
    <description><![CDATA[<p>When the world&amp;#039;s biggest sporting event kicks off, every millisecond matters. Learn how Momento helps broadcasters and sports platforms deliver faster, smarter, and more secure fan experiences at global scale.</p>]]></description>
    <content:encoded><![CDATA[<p><img src="https://www.gomomento.com/wp-content/uploads/2026/06/Lionel-Banner-1024x512.png" alt=""></p>
<p><em><a href="https://www.gomomento.com/wp-content/uploads/2026/06/FIFA-infographic.pdf">Don’t have five minutes? This infographic summarizes the key takeaways from this blog.</a></em></p>
<p> The FIFA World Cup 2026 is not just a tournament for Momento. It is a live fire test of what real-time data at global scale actually means.</p>
<p>In stadiums and on sofas, hundreds of millions of fans will see goals, cards, and heartbreak. Behind the scenes, three of Momento’s largest customers will be doing something just as intense: pushing a real-time data platform to the limit across live origination, content protection, and AI-powered personalization.</p>
<p>Beneath these vastly different workloads lies the same fundamental principle that decisions must be made instantly, at global scale, with zero excuses.</p>
<h2 id="what-do-we-mean-by-momento-is-a-real-time-data-platform">What Do We Mean by “Momento is a Real-Time Data Platform”?</h2>
<p>“Data platform” is one of those phrases that can mean anything from a gigantic static data warehouse to a firehose of events in flight. When we say Momento is a real-time data platform, we mean something specific as we combine:</p>
<ul>
<li><strong>A sub-millisecond in-memory data engine</strong>: This is the RAM-cache based data plane that serves reads and writes in less than a millisecond, even under massive load.</li>
<li><strong>An intelligent control plane</strong>: This layer automatically handles sharding, scaling, partitioning, and hot-key management, so app teams do not have to be distributed systems experts</li>
</ul>
<p>In practice, customers can treat Momento like a simple API to store and retrieve state in real time: segments and manifests, concurrency counters, user events, AI embeddings, and more. They describe their data model and policy. Momento makes it fast, durable, and observable.</p>
<p>The FIFA World Cup is a perfect way to show what that actually looks like when the stakes are highest. Think of it as a hat-trick of real-time use cases.</p>
<h2 id="1-a-live-origin-that-just-does-not-flinch">1/ A Live Origin That Just Does Not Flinch</h2>
<p><strong>Who:</strong> A large UK broadcaster holding FIFA rights
<strong>Problem:</strong> Their live origin service (AWS Elemental MediaStore) was deprecated before the World Cup. They needed the same low latency, failover behavior and observability, at World Cup scale, without rewriting their encoder, packager, video player or CDN configurations.</p>
<p><strong>How Momento helps:</strong><a href="https://www.gomomento.com/solutions/momento-media-storage/">Momento Media Storage</a> is their new live origin. It gives them:</p>
<ul>
<li><strong>Predictable, low latency:</strong> Consistent latency for reads and writes at the live edge.</li>
<li><strong>Granular TTL control:</strong> TTL settings on manifests/segments to preserve automatic failover capabilities.</li>
<li><strong>Per-Service Limits and Metrics:</strong> Observability and metrics are built in so high traffic events don’t impact the rest of their 24/7 channels.</li>
</ul>
<p>For viewers, nothing “looks” different. For their ops teams, the origin is now actively developed, supported, faster, and ready for tens of millions of concurrent fans.</p>
<p>🔗 Deep dive <a href="https://www.gomomento.com/blog/a-new-live-streaming-origin-built-for-global-scale/">on How a major UK broadcaster moved its World Cup live origin to Momento ↗</a></p>
<h2 id="2-content-protection-through-concurrency-tracking">2/ Content Protection Through Concurrency Tracking</h2>
<p><strong>Who:</strong> A major US broadcaster holding FIFA rights
<strong>Problem:</strong> Pirates leverage stolen accounts and run illegal restreaming operations on top of the broadcaster’s own CDN. Traditional DRM and short-lived tokens verify a device can decrypt, but are completely blind to whether an account is behaving like a bot farm. Content rights holders are concerned about piracy and are asking broadcasters to implement server-side control over account behavior to shut down the illegal streams at the source.</p>
<p>**How Momento helps:**The broadcaster runs a server-side <a href="https://www.gomomento.com/wp-content/uploads/2025/04/Momento_Concurrency_Overview_OnePager_FINAL_3.17.25.pdf">concurrency service</a> backed by Momento. For each stream, a lightweight verification loop executes three steps:</p>
<ul>
<li><strong>Receive the heartbeat:</strong> The player calls a Momento-powered service with identifiers for the subscriber’s account, their device, the content they want to access, their geography, etc.</li>
<li><strong>Enforce stream limits:</strong> Verify limits on concurrent streams per account and per event.</li>
<li><strong>Deliver instant decisions:</strong> Make allow/deny decisions in single-digit milliseconds, inline with playback.</li>
</ul>
<p>If one account suddenly powers hundreds of devices on the same game, the system can automatically shut it down, protecting revenue, CDN bills, and QoE for legitimate fans.</p>
<p>🔗 Details on the architecture and anti-leeching approach:
<a href="https://www.gomomento.com/blog/stop-cdn-leeching-with-concurrency-tracking/">Stop CDN Leeching with Concurrency Tracking ↗</a></p>
<h2 id="3-ai-powered-personalized-feeds-for-a-sports-app">3/ AI-Powered Personalized Feeds for a Sports App</h2>
<p><strong>Who:</strong> A US-based popular Sports content Super App
<strong>Problem:</strong> Just ahead of the World Cup, this content provider wanted to relaunch their app, with a brand new User Experience. Beyond the traditional “click to watch” from their editorial content, they needed a real-time, personalized feed that mixes editorial pieces, highlights, YouTube clips, and content from popular social networks, tuned to each fan’s behavior as it happens. Think of it as a TikTok “For You Page”, for Sports.</p>
<p>**How Momento helps:**Momento serves as the real-time event collection and embedding layer, making real-time machine learning models visible to the app users at massive scale:</p>
<ul>
<li><strong>Dynamic content ingestion:</strong> New content from newsrooms, creators, social networks, and athletes is ingested and turned into <a href="https://youtu.be/DlKiWkHtsXE?si=LJAcq01lzxwOCJlb">AI embeddings</a>.</li>
<li><strong>Real-time signal streaming:</strong> User signals including emoji reactions, comments, watch time, and scroll depth stream into Momento in real time.</li>
<li><strong>Sub-millisecond personalization:</strong> Recommendation services query Momento’s sub-millisecond data plane to match fresh content to each fan’s evolving interests.</li>
</ul>
<p>The result: A feed that feels instantly relevant and keeps improving as fans interact with their personalized feed, at the scale of millions of concurrent users.</p>
<p>🔗 Video explainer on real-time embeddings with Momento:
<a href="https://youtu.be/DlKiWkHtsXE?si=LJAcq01lzxwOCJlb">Momento AI embeddings &#x26; event collection explainer ↗</a></p>
<h2 id="lets-watch-football-not-infrastructure">Let’s Watch Football, Not Infrastructure</h2>
<p>Three very different workloads, all powered by the same underlying platform: a low latency data plane with an intelligent control layer that simplifies real-time data usage for application teams.</p>
<p>When the first match kicks off, the fans will be watching football, not infrastructure. But inside control rooms, NOCs, and product teams, our customers will know. They will see cleaner dashboards, stronger protections, and faster feedback loops. They will see a real-time data platform doing exactly what it was built to do.</p>
<p>We are excited to be part of their World Cup story. And once the final whistle blows, these capabilities will not disappear. They will become the new baseline for what fans expect from live sports, everywhere.</p>]]></content:encoded>
  </item>
</channel>
</rss>
