<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ReadySet</title>
    <description>The latest articles on DEV Community by ReadySet (@readyset).</description>
    <link>https://dev.to/readyset</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1232832%2Fad488c0d-7def-48e6-a0e2-b8ad5355fabe.png</url>
      <title>DEV Community: ReadySet</title>
      <link>https://dev.to/readyset</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/readyset"/>
    <language>en</language>
    <item>
      <title>Less Work per Query: Speeding Up Readyset’s Text Protocol</title>
      <dc:creator>ReadySet</dc:creator>
      <pubDate>Mon, 02 Nov 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/readyset/less-work-per-query-speeding-up-readysets-text-protocol-2pkg</link>
      <guid>https://dev.to/readyset/less-work-per-query-speeding-up-readysets-text-protocol-2pkg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn4q8116vq2gt76rrwnr2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn4q8116vq2gt76rrwnr2.png" alt="Less Work per Query: Speeding Up Readyset’s Text Protocol" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Readyset is in the business of making your queries faster. If you are trying to improve your application’s performance, or reduce load on your database, one of the first things people will tell you is to use prepared statements. What if you can’t? Readyset’s latest release improves performance for ad hoc queries, and this post talks about some of the optimizations we made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why are prepared statements faster?
&lt;/h2&gt;

&lt;p&gt;Prepared statements (sometimes "binary protocol”) are faster than ad hoc statements (sometimes "text protocol”) for a simple reason: they avoid repeating work. Instead of parsing, planning, and optimizing a query every time it executes, you do that work once in a &lt;code&gt;PREPARE&lt;/code&gt; step. You get a handle to the prepared statement, and can &lt;code&gt;EXECUTE&lt;/code&gt; it as many times as you want. You pay once for (most of) the work that is common to every statement execution, and each execution (mostly) only pays for the part that matters: actually getting the data.&lt;/p&gt;

&lt;p&gt;Usually, by the time a customer comes to Readyset looking to soothe their database pains, they are already using prepared statements. The problem is, they require application changes. Many customers are trying to improve performance or reduce load on applications that are already legacy, or they don’t have control over (infra teams instead of dev teams), or for some other reason could not be expected to modify queries. That’s why we offer a transparent caching layer in the first place. However, until now, we have not spent much time optimizing our ad hoc statement path. So if one of those customers’ existing applications happened to be using ad hoc statements instead of prepared statements, they should still see Readyset’s signature massive speedup… but it wouldn’t be quite as impressive as the speedup they would get if they were also using prepared statements.&lt;/p&gt;

&lt;p&gt;In our latest release, we have done some optimization of the text protocol path. We knew there would be overhead to trim down when each query arrives as SQL text that has to be handled afresh every time. We don't have space to discuss all ~12 significant improvements (depending on how you count), so this post will cover 4 interesting examples. Then, a sequel to this post will describe our new parse cache which is an even bigger jump and brings ad hoc query performance &lt;em&gt;much&lt;/em&gt; closer to the prepared statement path.&lt;/p&gt;

&lt;p&gt;To give you an idea of the scale of improvement just from these ~12 small changes: in a warmed cached point-read test with 1024 clients, this release handled about 2.7 times more throughput than the previous release when using ad hoc statements. That’s our fast case, and we care a lot about making it faster. But that difference only reflects a portion of all the changes. Often, stuff off the happy/hot path can matter just as much. (Have you ever profiled your error paths?) Anyway, let’s get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does Readyset process a query?
&lt;/h2&gt;

&lt;p&gt;I suppose if you want to understand the improvement we've made to handling queries when they reach the text protocol (or, for that matter, the first time you &lt;code&gt;PREPARE&lt;/code&gt; them), we had better understand what the handling process looks like. Once we know which cache to hit and which key to look up, the cached-read path is already pretty well optimized. But getting from SQL text to that cache and key can involve quite a bit of work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Freadyset.io%2Fimages%2Fblog%2Ftext-protocol%2Farchitecture-simple.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Freadyset.io%2Fimages%2Fblog%2Ftext-protocol%2Farchitecture-simple.svg" alt="Text-protocol query path from SQL decoding through parsing, rewriting, routing and a cached-result lookup to encoded results." width="1400" height="660"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Readyset has two caching modes. Pull-based caching is a TTL-based cache of results retrieved from the upstream database. Push-based caching uses partially-stateful dataflow (&lt;a href="https://pdos.csail.mit.edu/papers/noria:osdi18.pdf" rel="noopener noreferrer"&gt;&lt;em&gt;Noria&lt;/em&gt; OSDI ’18&lt;/a&gt;) to maintain query results from database changes only for results you care about based on a CDC stream from the upstream database.&lt;/p&gt;

&lt;p&gt;Reading those results starts with finding the corresponding cache, if one exists. This has to respect SQL semantics and things like schema search path/current database. Then we need the cache key: in a prepared statement, the values come from bound parameters, but for an ad hoc query, we have to extract literals from the AST and map them to cache key positions and values. Pull- and push-based caching use similar processes to identify the cache and key, and some parts are shared. (It doesn’t matter much, but technically the following examples assume a push-based cache.)&lt;/p&gt;

&lt;p&gt;For example, with a &lt;code&gt;users(id, name, active)&lt;/code&gt; table, Readyset needs to auto-parameterize the literal &lt;code&gt;42&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- original SQL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- auto-parameterize&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it can look up the corresponding cache for that shape, and then look up the value for key &lt;code&gt;42&lt;/code&gt;. If this were a prepared statement, the &lt;code&gt;42&lt;/code&gt; wouldn’t need to be auto-parameterized.&lt;/p&gt;

&lt;p&gt;As you can imagine, there is a lot of complexity hiding in and around those “parse, rewrite, route" boxes in the diagram above. That’s why I very cleverly asked Claude to add an “etc.” box too. Session variables and current DDL state affect how a query is rewritten and what the lookup key should be, some literals should be auto-parameterized and not others, and so on. A more detailed diagram is at the end of this post, but even that mostly only calls out the boxes where we made improvements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why were we applying SQL rewrites that were no-ops?
&lt;/h2&gt;

&lt;p&gt;Auto-parameterization isn’t the only SQL AST rewrite pass. Even for this simple query, we need to expand stars and resolve columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- auto-parameterized&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- normalize: expand stars, resolve columns, etc.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This brings us to one of the ~12 improvements (depending on how you count) we made in this release: aren’t some SQL rewrites no-ops for some queries? For example, our support for subqueries in push-based dataflow relies on an “inline derived tables” rewrite. It might look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- original SQL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;active_users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;active_users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- normalize: inline derived tables, etc. (parameters: 42, true)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rewrite passes have to do some fairly nontrivial work: resolve columns to the right table in the right subquery, pick aliases to avoid conflicts, move the inner filter into the outer filter, and so on. For more detail, see &lt;a href="https://readyset.io/blog/how-readyset-rewrites-your-sql-inside-the-query-transformation-pipeline" rel="noopener noreferrer"&gt;how Readyset rewrites SQL&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our original point query doesn’t have a derived table, so it doesn’t need to do any of that work. In this case, it’s obvious. For some of our rewrite passes, it’s less obvious when it can be safely skipped, so we were applying the rewrites even when they were ultimately no-ops.&lt;/p&gt;

&lt;p&gt;This was one of the first things to show up in flamegraphs when we started this optimization effort. But if all we had seen was “rewrites take a good chunk of the CPU cycles for each query”, we might nonetheless have looked elsewhere. However, this was surprising, because this was showing up in flamegraphs with point selects like the first example, too, not just arbitrary query workloads.&lt;/p&gt;

&lt;p&gt;Optimization is all about deciding what to measure. This means both the specific observable metrics you collect and compare, whether you are profiling CPU or instrumenting memory allocations, etc.; but also the workload you run. We used a variety, and saw different problems become more prominent in flamegraphs, throughput measurements, lock contention, etc. depending on which workload was running. In this case, point selects generally represent the &lt;em&gt;best case&lt;/em&gt; for the "parse, rewrite, route” part of processing a text-protocol query. As a result, looking at a flame graph for a point select with no derived tables and seeing the derived table rewrite taking up a non-negligible amount of time raised our eyebrows, and took us down that path.&lt;/p&gt;

&lt;p&gt;Here is what the deep rewrites part of the flamegraph looked like profiling point select queries with and without skipping no-op rewrites, saving about 23% of CPU cycles on rewrites:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SQL rewrite CPU cycles per query. Hover or tap to highlght correlated frames.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Of course, that isn't going to affect queries that &lt;em&gt;do&lt;/em&gt; need those rewrites, but most queries will at least skip some rewrites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why were unrelated tables affecting query latency?
&lt;/h2&gt;

&lt;p&gt;The query itself isn’t the only part of the workload that matters. In a stateful system, state tends to accumulate. Could unrelated state impinge on the handling of a query? What happens if we run the same point select, but add a few hundred tables into the same database that the query doesn’t even reference?&lt;/p&gt;

&lt;p&gt;To rewrite a query, Readyset needs information about table schemas. In our example, it needs to know which columns &lt;code&gt;users&lt;/code&gt; has to expand &lt;code&gt;SELECT *&lt;/code&gt;, and where &lt;code&gt;id&lt;/code&gt; comes from. But the work we were doing to make that information available also depended on tables that had nothing to do with the query. Effectively, we were building a hashmap of &lt;em&gt;all&lt;/em&gt; relations and all their columns before we started the rewrite passes. A point select against one table was building a hashmap for tens or hundreds of tables. We changed this to look up schema information as the rewrite needs it.&lt;/p&gt;

&lt;p&gt;This also meant the larger your database, the wider the gap after it was fixed. With 500 unrelated tables, median latency dropped by 8x at a fixed query rate, and without the rate cap, throughput increased by 7x. Bet Marketing would love it if that were the headline! On the other hand, at 200 tables it’s the difference between 452 µs and 122 µs, and at 10 unrelated tables any difference was lost to run-to-run noise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Freadyset.io%2Fimages%2Fblog%2Ftext-protocol%2Fextra-schema-latency.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Freadyset.io%2Fimages%2Fblog%2Ftext-protocol%2Fextra-schema-latency.svg" alt="Throughput and p50/p99 latency versus unrelated table count for stable-260924 and stable-261022." width="1440" height="1100"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Throughput and latency by number of unrelated tables.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why were we updating statuses that haven't changed?
&lt;/h2&gt;

&lt;p&gt;After we’ve processed a query, Readyset will store (some might say "cache”) some information about it so we don't have to check again next time we execute it. Is there a cache? Should it go upstream? We store and retrieve that information in the &lt;code&gt;QueryStatusCache&lt;/code&gt;. However, when we started measuring throughput under high concurrent load on the text protocol, we discovered we were taking a global write lock to update query status for an existing entry even when it hadn’t changed (and it rarely does). We had never noticed while optimization was focused on the binary protocol, because prepared statements would only take the write lock once at &lt;code&gt;PREPARE&lt;/code&gt; time and then reuse the handle.&lt;/p&gt;

&lt;p&gt;We simply added a condition to an &lt;code&gt;if&lt;/code&gt;: check whether there is actually anything to change before taking the global write lock. Lock contention hazards like this often show themselves as a flattening (or worse yet, a cliff) in the throughput curve as concurrency increases, and in one benchmark we saw that starting as low as 96 concurrent clients. But that was with &lt;em&gt;none&lt;/em&gt; of the changes in this release.&lt;/p&gt;

&lt;p&gt;The previous graph was comparing release-to-release, where you can hopefully still see where specific areas of the product improve. But several different changes removed nonspecific throughput barriers like this one, so I honestly couldn't find a good way to isolate the effect of just this change, other than by hacking in a runtime toggle for that new if condition. That lets the effect of this change be seen more clearly, instead of leaving us at such low throughput that we wouldn't notice much of a difference. So although overall throughput under the same benchmark went up 2.7x from release to release, the portion due to &lt;em&gt;this&lt;/em&gt; particular change was only ~18%: throughput at 1,024 clients went from 510k to 602k queries per second, and p99 latency fell from 4.18 to 3.68 ms. The throughput curve was able to go up further in this test, and the fact that without this guard (but with other changes) throughput is already going &lt;em&gt;down&lt;/em&gt; from 384 to 768 means that although it was a small change on its own, it was a big part of the overall performance gain in this release.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Freadyset.io%2Fimages%2Fblog%2Ftext-protocol%2Fstatus-lock-ablation-sweep.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Freadyset.io%2Fimages%2Fblog%2Ftext-protocol%2Fstatus-lock-ablation-sweep.svg" alt="Text-protocol throughput from 48 to 1,024 clients in stable-261022 with the status guard off and on." width="1536" height="904"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Text-protocol throughput with the status guard off and on.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;By this point we’re hitting the box’s CPU capacity and the curve is flattening out, with both the load generator and Readyset running on it. While &lt;em&gt;I&lt;/em&gt; enjoyed measuring the impact of this 1 line diff on a shared box, &lt;em&gt;you&lt;/em&gt; probably want to see how far the cached-read path can really go. For that, check out &lt;a href="https://readyset.io/case-studies/how-we-reached-43-million-qps-for-mysql-and-postgresql-workloads-on-a-single-readyset-node" rel="noopener noreferrer"&gt;Marcelo’s post&lt;/a&gt; with separate load generators and a 10 Gbps NIC. That benchmark uses prepared statements (see the first paragraph of this post), so it’s measuring a different path from the text-protocol work here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why were we accumulating excessive query logs?
&lt;/h2&gt;

&lt;p&gt;Similar to unrelated tables/queries affecting processing of one query, not all of the work we do for a query is needed to return a result. We also log query information asynchronously, so queries don’t have to wait for the logger. But what happens if queries produce that information faster than the logger can consume it? Seems like it should be unlikely, and we wouldn’t notice if logging is only on &lt;code&gt;PREPARE&lt;/code&gt;. Plus... by the time we got to this item, ad hoc queries were a lot faster.&lt;/p&gt;

&lt;p&gt;Without a bound on the queue, we could keep accepting queries while accumulating gigabytes of logging work. We bounded the queue, allowing some telemetry to be dropped when the logger falls behind. In an overload test, transient memory use just from the queue went from about 15 GB to 310 MB. Throughput also went up from 1.24 million to 1.35 million queries per second. About 4% of telemetry was dropped in that run. That’s a tradeoff, but life is full of tradeoffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion and results: how fast is Readyset’s text protocol now?
&lt;/h2&gt;

&lt;p&gt;These might seem like odd examples for a post about making queries faster. It was mostly just &lt;em&gt;not doing&lt;/em&gt; things we didn't need to. In fact, arguably much of these didn’t actually have to do with &lt;em&gt;serving the query&lt;/em&gt; but with other tangential parts of the system (though we made plenty of other changes to the hot path too).&lt;/p&gt;

&lt;p&gt;Okay, where did we end up? Well, at the risk of repeating the number 2.7 yet again… 2.7 times higher throughput! Specifically in the warmed cached point-select benchmark which was the worst case for text protocol overhead. At 1,024 clients. At 192 clients, median latency fell by 69%, p95 by 48%, and p99 by 20%. Yeah, I’ve picked these numbers out basically at random, and could have gotten pretty similar numbers from basically every benchmark we ran that didn’t use prepared statements.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Freadyset.io%2Fimages%2Fblog%2Ftext-protocol%2Ftext-protocol-release-summary.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Freadyset.io%2Fimages%2Fblog%2Ftext-protocol%2Ftext-protocol-release-summary.svg" alt="Throughput from 48 to 1,024 clients and p50/p95/p99 latency at 192 clients: stable-260924 in black and stable-261022 in purple." width="1440" height="760"&gt;&lt;/a&gt; &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Freadyset.io%2Fimages%2Fblog%2Ftext-protocol%2Ftext-protocol-release-summary-stacked.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Freadyset.io%2Fimages%2Fblog%2Ftext-protocol%2Ftext-protocol-release-summary-stacked.svg" alt="Throughput from 48 to 1,024 clients and p50/p95/p99 latency at 192 clients: stable-260924 in black and stable-261022 in purple." width="600" height="1140"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Text-protocol throughput by client count; latency at 192 clients.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: can we make Readyset’s text protocol even faster?
&lt;/h2&gt;

&lt;p&gt;Well, after harping about not doing stuff for so long, you must be convinced that laziness is indeed a virtue. And in a follow-up post, we’ll go another level deeper: what if we can avoid doing &lt;em&gt;any&lt;/em&gt; of the work we just discussed, and make Readyset’s text-protocol performance even closer to the prepared statement path?&lt;/p&gt;

&lt;p&gt;In the meantime, here is a more detailed diagram which shows the places we made some other noticeable improvements in this release.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Text-protocol query path. Hover or focus a numbered marker to highlight its component.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Commits on GitHub: &lt;a href="https://github.com/readysettech/readyset/commit/28a841191f252370cc5761d531fc346b4ab5d40d" rel="noopener noreferrer"&gt;1. Read less schema&lt;/a&gt;, &lt;a href="https://github.com/readysettech/readyset/commit/6dfcf4ce4df8e955ba458e870dc739c2de796589" rel="noopener noreferrer"&gt;2. Skip no-op rewrites&lt;/a&gt;, &lt;a href="https://github.com/readysettech/readyset/commit/1a969c0d1d6ee4453637d79c999f8f58370b6f59" rel="noopener noreferrer"&gt;3. Combine passes&lt;/a&gt;, &lt;a href="https://github.com/readysettech/readyset/commit/c4d4a3b266e35ea564b14eab1b482226f554c246" rel="noopener noreferrer"&gt;4. Streaming hashing&lt;/a&gt;, &lt;a href="https://github.com/readysettech/readyset/commit/7c66b2e0b143edf3efd129fbf43151b9301245fc" rel="noopener noreferrer"&gt;5. Skip unused errors&lt;/a&gt;, &lt;a href="https://github.com/readysettech/readyset/commit/cea21387a85acdf070bfc3c6bd547b160bc5c2c9" rel="noopener noreferrer"&gt;6. Faster schema access&lt;/a&gt;, &lt;a href="https://github.com/readysettech/readyset/commit/1fdce46d72898e61a18439b6c769606bde8cf2c4" rel="noopener noreferrer"&gt;7. Skip unchanged updates&lt;/a&gt;, &lt;a href="https://github.com/readysettech/readyset/commit/5b8224109fd099d7a5cb9f54975945cc5f1c4fb8" rel="noopener noreferrer"&gt;8. Stamp only on change&lt;/a&gt;, &lt;a href="https://github.com/readysettech/readyset/commit/0d73ab2aa32406d211bbb195fe3d96ba72758396" rel="noopener noreferrer"&gt;9. Keep logging bounded&lt;/a&gt;, &lt;a href="https://github.com/readysettech/readyset/commit/3f31159c3c1ee6e71ba7e65ed17a77137887ed24" rel="noopener noreferrer"&gt;10. Connect with fewer trips&lt;/a&gt;, &lt;a href="https://github.com/readysettech/readyset/commit/30902ec0384a6f15e3c886a477c4daccd6315c11" rel="noopener noreferrer"&gt;11. Copy only samples&lt;/a&gt;, &lt;a href="https://github.com/readysettech/readyset/commit/1c05ab567aeb4f1b8883580778903b70fdab7145" rel="noopener noreferrer"&gt;12. Reuse result buffers&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclaimer: This post’s prose was machine-generated by a 36-key Beekeeb Piantor Pro operated by an artificially intelligent bio-appendage prone to hallucination. The diagrams’ SVG were handwritten by some people named Claude and Astra.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>engineering</category>
    </item>
    <item>
      <title>October Product Update 2026</title>
      <dc:creator>ReadySet</dc:creator>
      <pubDate>Mon, 02 Nov 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/readyset/october-product-update-2026-2mhm</link>
      <guid>https://dev.to/readyset/october-product-update-2026-2mhm</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhdu9r4vhthr2zfvheq5s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhdu9r4vhthr2zfvheq5s.png" alt="October Product Update 2026" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;October was a big release month at Readyset, with an exciting new RDST capability and the longest list of features and fixes we've shipped in a release this year. Let's catch up.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The RDST Agent Is Here&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;RDST now features an always-on agent that watches your Postgres or MySQL database for queries that are getting slower, busier, or changing plans, and flags them with a recommended fix.&lt;/p&gt;

&lt;p&gt;The agent reads your database's query stats every minute and keeps a day-by-day record of each query shape. Instead of a lifetime average that buries yesterday's slowdown under three months of fast runs, it checks every five minutes whether each busy query is slower or busier than it usually is. When something changes, RDST agent investigates on its own: it runs &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; with real values from live traffic, explains what happened, and hands you the fix. Once you apply the fix, it confirms it worked.&lt;/p&gt;

&lt;p&gt;In our walkthrough, a deploy made one query 30x slower. The agent flagged it within nine minutes, found the missing index, and confirmed the fix took the query from 184 ms to 3.8 ms.&lt;/p&gt;

&lt;p&gt;Read-only, so it never writes to your database. Free and open source. Available now in the &lt;a href="https://readyset.io/downloads" rel="noopener noreferrer"&gt;RDST desktop app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://readyset.io/blog/meet-the-rdst-agent" rel="noopener noreferrer"&gt;Read the full launch post here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;💬 Product Updates&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A quick naming note: Readyset's two caching modes, previously called shallow and deep caching, now go by pull-based and push-based caching, named after how each one keeps results fresh. A pull-based cache pulls new results from the database on a schedule, while a push-based cache has every write pushed into the cached results as it happens. Same products, just going by new names.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;New Admin SQL Port for Readyset:&lt;/strong&gt; Readyset now ships a dedicated admin SQL port. Admin sessions connect to the &lt;code&gt;readyset&lt;/code&gt; schema instead of the upstream database, keep their own credentials, and run commands that affect Readyset itself rather than queries against your data. The first admin-only command, &lt;code&gt;ALTER READYSET SHUTDOWN [RESET]&lt;/code&gt;, lets operators shut down a Readyset instance, configure the port with &lt;code&gt;--admin-address&lt;/code&gt; (default &lt;code&gt;127.0.0.1:6036&lt;/code&gt;) and &lt;code&gt;--admin-credentials&lt;/code&gt; (default &lt;code&gt;admin:admin&lt;/code&gt;), or disable it with &lt;code&gt;--admin-address disabled&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Session-Variable Isolation for Pull Caches:&lt;/strong&gt; A batch of fixes so pull cache entries no longer leak between sessions whose session variables would produce different query results. Pull cache entries are now keyed on the MySQL upstream's global value of the session variables that affect results, and on each PostgreSQL connection's &lt;code&gt;IntervalStyle&lt;/code&gt; (whether inherited from the upstream or set in the session), &lt;code&gt;timezone_abbreviations&lt;/code&gt;, and &lt;code&gt;default_text_search_config&lt;/code&gt;. Sessions with different values now see their own cached results instead of each other's.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dual-Password Support for MySQL:&lt;/strong&gt; &lt;code&gt;ALTER READYSET MODIFY USER ... RETAIN CURRENT PASSWORD&lt;/code&gt; keeps the old password valid alongside the new one, and &lt;code&gt;... DISCARD OLD PASSWORD&lt;/code&gt; retires it. Naming a user twice in &lt;code&gt;--allowed-users&lt;/code&gt; starts a deployment mid-rotation. The &lt;code&gt;readyset.users&lt;/code&gt; view reports rotations in progress in a new &lt;code&gt;has_old_password&lt;/code&gt; column, so operators can run safe password rotations without a window where auth breaks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-Upstream MySQL and Smarter Connection Handling:&lt;/strong&gt; &lt;code&gt;--upstream-db-url&lt;/code&gt; and &lt;code&gt;--cdc-db-url&lt;/code&gt; now accept a comma-separated list of MySQL servers, each with an optional &lt;code&gt;|&amp;lt;weight&amp;gt;&lt;/code&gt; suffix. Added &lt;code&gt;--upstream-connect-timeout&lt;/code&gt; to limit how long the adapter waits for an upstream connection, &lt;code&gt;--client-connect-timeout-ms&lt;/code&gt; to disconnect MySQL and PostgreSQL clients that don't complete the handshake within 10 seconds by default, and &lt;code&gt;--listen-backlog&lt;/code&gt; (default 4096, up from 128) so connection bursts are less likely to leave clients stalled until they time out.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Safer Memory Limit Defaults:&lt;/strong&gt; &lt;code&gt;--memory-limit&lt;/code&gt; now defaults to 80% of the memory available to Readyset (the container limit, if set) instead of unlimited. Set &lt;code&gt;--memory-limit&lt;/code&gt; to 0 to keep the previous unlimited behavior). In push and push-then-pull cache modes, the pull cache now uses half of &lt;code&gt;--memory-limit&lt;/code&gt; when &lt;code&gt;--pull-memory-percent&lt;/code&gt; isn't set, instead of being unbounded.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wider Pull Cache Coverage on PostgreSQL:&lt;/strong&gt; Pull caches now serve queries over views created after Readyset started (previously these were never pull cached), and support &lt;code&gt;point&lt;/code&gt;, &lt;code&gt;box&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;, and &lt;code&gt;cidr&lt;/code&gt; values over the extended query protocol. Fixed a case where Readyset couldn't start in pull mode with RLS enabled against a PostgreSQL upstream whose certificate was signed by a private CA.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Faster Text Queries:&lt;/strong&gt; Improved throughput for queries sent as text, which previously serialized on two locks: the schema catalog lock and another shared lock. Workloads with heavy text-protocol query traffic now see higher throughput under concurrency.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;More Queries Served From Cache:&lt;/strong&gt; A batch of fixes expanding what Readyset caches instead of proxying upstream. MySQL caches created with &lt;code&gt;?&lt;/code&gt; placeholders are now served from cache rather than sent to the upstream. Row comparisons with literals or placeholders on the left side (such as &lt;code&gt;($1, $2) = (a, b)&lt;/code&gt;) are now cached as lookups, and row comparisons mixing literals and columns (such as &lt;code&gt;('a', b) = ('c', 'd')&lt;/code&gt;) are now cached instead of rejected with a placeholder error. Push caches created from a query ID in &lt;code&gt;SHOW PROXIED QUERIES&lt;/code&gt; using &lt;code&gt;CREATE CACHE FROM &amp;lt;query id&amp;gt;&lt;/code&gt; are also correctly recreated on restart.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus 15 additional fixes and improvements. Check out the full release &lt;a href="https://github.com/readysettech/readyset/releases/tag/stable-261022" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  New on the Blog &amp;amp; YouTube
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://readyset.io/blog/less-work-per-query-speeding-up-readysets-text-protocol" rel="noopener noreferrer"&gt;Less Work per Query: Speeding Up Readyset’s Text Protocol&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Michael Victor Zink writes about making Readyset’s ad hoc queries faster by doing less work per query (skipping no-op rewrites, ignoring unrelated tables, avoiding needless locks, and bounding the query log) for 2.7x higher throughput at 1,024 clients.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://readyset.io/blog/what-is-a-wire-compatible-sql-caching-proxy" rel="noopener noreferrer"&gt;What Is a Wire-Compatible SQL Caching Proxy?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Caching SQL can live in two places: scattered through your application code with Redis, or in a layer that speaks the database's own wire protocol. The second option moves cache keys, invalidation logic, and the hit-or-miss decision out of the codebase entirely. This post walks through what wire compatibility actually requires at the MySQL and PostgreSQL protocol level, where Redis still fits, and why incremental view maintenance makes this different from a store you fill and empty by hand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://readyset.io/blog/readyset-ha-proxysql-mysql" rel="noopener noreferrer"&gt;Readyset HA with ProxySQL and MySQL&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vinicius Grippa builds a highly available Readyset cache behind a 106 ms report query, using two &lt;code&gt;docker run&lt;/code&gt; commands and two &lt;code&gt;INSERT&lt;/code&gt; statements in ProxySQL's admin interface. Nothing changes in the application, the schema, or MySQL. The query drops to 0.716 ms, a 149x improvement, and the setup survives losing a Readyset node or a ProxySQL node with no downtime. Also covers the three ways to confirm the cache is actually serving reads instead of silently falling through to the database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=BsziEgFDIK0" rel="noopener noreferrer"&gt;Watch: "Building the Invisible Cache"&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tanmay Sinha’s talk from MySQL BR Conf 2026 is now up on YouTube. His talk covers why the original MySQL query cache failed, why hand-rolling one on Redis leaves the hardest parts to your team, and what wire-compatible caching changes about that, and is followed by a demonstration by Vinicius Grippa.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://readyset.io/blog/black-october-what-actually-happened-and-what-to-patch" rel="noopener noreferrer"&gt;Black October: What Actually Happened, and What To Patch&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A primary-source review of the 28 "Black October" security incidents. Only two involved AI-driven attacks. The real damage came from long-lived credentials stored in platform environment variables and shared vendor systems. Includes the specific database-tier patches to apply this week and six steps for cutting credential risk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://readyset.io/blog/a-ttl-cache-needs-to-know-the-query" rel="noopener noreferrer"&gt;A TTL Cache Needs To Know the Query&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Marcelo Altmann on why cache-aside on Redis struggles with SQL once a workload hits real load: a key-value store can't re-run a query, so it can't refresh hot keys, collapse miss stampedes, weigh a 900 ms report against a 2 ms lookup, or key by session settings and user permissions. The strongest evidence for the argument comes from Redis itself, which ships two separate products (Smart Cache and Redis Data Integration) aimed at the exact gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Involved
&lt;/h2&gt;

&lt;p&gt;Questions, bugs, ideas, or just want to say hi? Find us on &lt;a href="https://go.readyset.io/community" rel="noopener noreferrer"&gt;Slack&lt;/a&gt;. Looking to contribute? Our &lt;a href="https://github.com/readysettech/readyset/issues?q=is%3Aissue+is%3Aopen+label%3Afirst-issue" rel="noopener noreferrer"&gt;newcomer issues&lt;/a&gt; are a good place to start.&lt;/p&gt;

</description>
      <category>productupdates</category>
      <category>postgres</category>
      <category>mysql</category>
      <category>product</category>
    </item>
    <item>
      <title>A TTL Cache Needs To Know the Query</title>
      <dc:creator>ReadySet</dc:creator>
      <pubDate>Thu, 29 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/readyset/a-ttl-cache-needs-to-know-the-query-35lk</link>
      <guid>https://dev.to/readyset/a-ttl-cache-needs-to-know-the-query-35lk</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7oijmfwlm43srpw2xg15.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7oijmfwlm43srpw2xg15.png" alt="A TTL Cache Needs To Know the Query" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Plenty of teams cache query results with a TTL. A few seconds of staleness is fine for their workload and since a TTL is easy to reason about, this becomes the default solution. The trouble however, is the primitive underneath. Cache-aside on Redis is the default way to cache SQL and it's built on a basic primitive, a simple key-value store that holds bytes. We hold the view that a SQL query cache should not only hold the results, but also &lt;em&gt;the query&lt;/em&gt; that generated them and preferably additional metadata. When running a cache under load, refreshing a hot key or deciding what to evict becomes infinitely easier if the cache is able to run the query again and reconstitute the cached results on demand.&lt;/p&gt;

&lt;p&gt;Since a TTL cache built on Redis or Memcached doesn't know what a query is, it can't re-run one to refresh a hot key, collapse a burst of misses into a single call, or differentiate a 900 ms result from a 2 ms one. That work is left up to your application.&lt;/p&gt;

&lt;p&gt;We've written before about &lt;a href="https://readyset.io/blog/dont-use-kv-stores" rel="noopener noreferrer"&gt;why key-value stores struggle to cache relational data&lt;/a&gt; when freshness is the goal. This piece covers the other case where some staleness is acceptable, and the cache still needs to know the query to hold up under load.&lt;/p&gt;

&lt;p&gt;A note on names. Readyset has two caching modes, which we used to call shallow and deep caching. We now call them &lt;strong&gt;pull-based&lt;/strong&gt; and &lt;strong&gt;push-based&lt;/strong&gt; caching, after how each one keeps results fresh. A pull-based cache pulls new results from the database on a schedule, while a push-based cache has every write pushed into the cached results as it happens. The products are the same, and earlier posts use the old names.&lt;/p&gt;

&lt;p&gt;Adding hardware is a harder way out than it used to be. Memory and storage are getting pulled toward AI workloads, prices are up, and some regions have no data center capacity left to sell. &lt;a href="https://readyset.io/blog/your-database-bill-is-climbing-faster-than-your-traffic" rel="noopener noreferrer"&gt;Database bills are climbing faster than traffic&lt;/a&gt;, so &lt;a href="https://readyset.io/case-studies/how-we-reached-43-million-qps-for-mysql-and-postgresql-workloads-on-a-single-readyset-node" rel="noopener noreferrer"&gt;getting more out of the database&lt;/a&gt; you already run is worth real engineering effort.&lt;/p&gt;

&lt;p&gt;All of this matters more every month. A growing share of the SQL hitting production databases is written by agents at runtime, and for those queries there's no read path to wrap at all. We'll come back to that at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a key-value store knows about your data
&lt;/h2&gt;

&lt;p&gt;Very little. Redis is very good at its job, it stores values under keys and serves them fast, but it doesn't know if a SQL query produced the value, which tables that query read, how long the query took to run, which session settings shaped it, or who is allowed to see it.&lt;/p&gt;

&lt;p&gt;A cache-aside implementation fills the gap in application code. Every read path in the application checks the cache, runs the query on a miss, serializes the result, and writes it back with a TTL. Each of those steps is a decision your team makes by hand, query by query.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fljqp95vsixp40gbal4vt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fljqp95vsixp40gbal4vt.png" alt="Comparison of cache-aside on a key-value store, where your application sits between Redis and the database and owns the cache logic, versus Readyset, which sits between your application and the database so SQL stays unchanged and Readyset handles misses and refresh" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A query cache starts from the query. Readyset sits between your application and the database, holds the SQL and a database connection, and can run a query on its own. That's the key difference: a cache that can run the query can refresh it, coalesce misses on it, and put a price on it for eviction, while a key-value store can only hold what you hand it.&lt;/p&gt;

&lt;p&gt;Outside databases, caching already works in this way. A reverse proxy like Varnish, or a CDN, keeps the URL, so it can refetch from the origin, serve a stale copy while it refreshes (HTTP's &lt;code&gt;stale-while-revalidate&lt;/code&gt;), and collapse a burst of misses into one origin request. Readyset's pull-based cache does the same for SQL.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Cache-aside on a key-value store&lt;/th&gt;
&lt;th&gt;Readyset pull-based cache&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What it stores&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bytes your app serialized, under a key your app chose&lt;/td&gt;
&lt;td&gt;Query results, keyed by SQL, parameters, schema, and session settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Read path&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cache-aside code in every read: check, query on miss, serialize, write back&lt;/td&gt;
&lt;td&gt;Unchanged; the app sends SQL as before&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;On a miss&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your app runs the query and fills the cache&lt;/td&gt;
&lt;td&gt;Readyset forwards the query to the database and fills the cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Refresh&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A job you build, since the store can't run a query&lt;/td&gt;
&lt;td&gt;Readyset re-runs the query in the background&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stampedes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A lock you build&lt;/td&gt;
&lt;td&gt;Concurrent misses collapse into one query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Eviction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LRU or LFU, access patterns only&lt;/td&gt;
&lt;td&gt;Weighs size, query cost, and read frequency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Session and tenant safety&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only as good as your key design&lt;/td&gt;
&lt;td&gt;Session settings and security context are part of the key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where a TTL cache falls short
&lt;/h2&gt;

&lt;p&gt;We ran into each of the problems below while building Readyset's pull-based cache, which we &lt;a href="https://readyset.io/blog/readyset-shallow-cache" rel="noopener noreferrer"&gt;introduced earlier this year&lt;/a&gt; under the shallow cache name. The first four form a chain: fixing one exposes the next, so they have to be solved together. The last four show up in every TTL cache regardless. Every fix depends on something a key-value store doesn't have.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgq7uahe9miqy7a9lh2m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgq7uahe9miqy7a9lh2m.png" alt="Two-column map of TTL cache problems to what Readyset does: hot keys expire and p99 spikes maps to sliding TTL, hot keys never expire maps to background refresh, every reader triggers a refresh maps to one refresh in flight per key, one schedule wastes database time maps to adaptive refresh with a load cap; then stampedes map to request coalescing, LRU evicts expensive results maps to cost-aware eviction, a session gets another's result maps to session-aware cache keys, and revoked users still get data maps to a permission cache with RLS-aware keys" width="800" height="744"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Hot keys expire under load
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fcms.readyset.io%2Ffiles%2Fuploads%2Fd39f0c6f-4b84-4e2b-878d-ec0094f58b6d.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fcms.readyset.io%2Ffiles%2Fuploads%2Fd39f0c6f-4b84-4e2b-878d-ec0094f58b6d.gif" alt="TTL refresh animation" width="600" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A fixed TTL forces a specific tradeoff. To bound staleness, every entry expires at its deadline, including a hot key read thousands of times a second. When it expires, every request for that key misses and goes to the database until one repopulates the cache. Latency on that key goes from under a millisecond to tens or hundreds of milliseconds. The p50 looks fine. Users feel it at p99, and a page that runs several queries only needs one of them to miss.&lt;/p&gt;

&lt;p&gt;This is also one reason why hit rate is not always the right metric to watch. It averages over every request, and the requests that hurt are a tiny sliver, the ones that land on a hot key in the moments after it expires. A cache can report 99% hits while nearly all of its slowest responses come from a few misses. Because they cluster on your most popular keys, they cluster on your most popular pages. Watch p99 latency on cached queries instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What teams do today:&lt;/strong&gt; accept the spikes, or lengthen the TTL and accept staler data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Readyset:&lt;/strong&gt; the TTL counts from the last read, not from when the entry was created. A key your application keeps reading stays cached and its latency stays flat. Keys that go quiet, age out and free memory. &lt;em&gt;Uses: read tracking per cached result.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Hot keys that never expire go stale
&lt;/h3&gt;

&lt;p&gt;Sliding TTL fixes the spike but it opens a new gap. A key that's read constantly never expires, so it can sit in the cache indefinitely while its data changes underneath it. A refresh also takes time. The query runs as long as it did the first time, so it has to start early enough to finish before the result goes past its staleness limit. For a 900 ms query, that lead time is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What teams do today:&lt;/strong&gt; this problem only appears once expiry is sliding, which is why most TTL caches stay fixed and keep the spikes from problem 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Readyset:&lt;/strong&gt; while a key is still being read, Readyset re-runs its query against the database and swaps in the new result. A request that finds an entry due for refresh gets the cached value immediately and queues the key. All of it runs on Readyset's own internal workers, off the request path.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh1zw7j5yqgjxqga85am3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh1zw7j5yqgjxqga85am3.png" alt="Timeline of one hot key read constantly: a fixed TTL misses at every expiry and sends all readers to the database, while a sliding TTL with background refresh keeps readers hitting the cache as refresh runs in the background" width="799" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Refresh multiplies database load
&lt;/h3&gt;

&lt;p&gt;Once refresh exists, a hot key with thousands of readers can trigger thousands of refreshes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What teams do today:&lt;/strong&gt; nothing, because without refresh there's nothing to deduplicate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Readyset:&lt;/strong&gt; one refresh per key is in flight at a time. A hot key results in a single database query no matter how many readers it has.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. One refresh schedule fits nobody
&lt;/h3&gt;

&lt;p&gt;Keys change at different rates. Some results move (i.e. they are updated) every few seconds, while others sit unchanged for days. Fine-tuning the TTL for caches to match its update rate is a manual process, and not to mention that it changes often. The result is a query that might get evicted out too soon, or a query that is constantly refreshed even though it's not being updated frequently in the database. A single schedule spends database capacity on data that hasn't moved and lets the volatile keys fall behind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What teams do today:&lt;/strong&gt; pick one TTL for everything, usually tuned for the most volatile data, and pay for it in database load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Readyset:&lt;/strong&gt; each refreshed result is compared with the previous one. Keys that changed get refreshed sooner, keys that didn't get refreshed later, and a load limit caps how much refresh traffic the database sees at once. In our &lt;a href="https://readyset.io/blog/adaptive-cache" rel="noopener noreferrer"&gt;Adaptive Cache benchmarks&lt;/a&gt;, this cut p95 staleness by 97% compared with a straight TTL, for about 10% more refreshes. &lt;em&gt;Uses: each key's history of changes.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Misses arrive all at once
&lt;/h3&gt;

&lt;p&gt;Refresh covers keys already in the cache. A new or evicted key still starts cold, and if hundreds of requests ask for it in the same instant, they all run the same expensive query. That's a cache stampede.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What teams do today:&lt;/strong&gt; absorb it with database headroom, which is capacity paid for and idle most of the time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Readyset:&lt;/strong&gt; concurrent misses on one key collapse into a single upstream query. The rest wait for that result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyd94deydvlwifs5fppyu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyd94deydvlwifs5fppyu.png" alt="Four concurrent requests for the same cold key: without coalescing all four miss and run the same expensive query against the database, while with Readyset the four misses collapse into one query and the rest wait for its result" width="800" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Eviction drops the wrong results
&lt;/h3&gt;

&lt;p&gt;LRU and LFU look only at access patterns. A 4 KB result counts the same as a 4 MB one, and a 900 ms analytical query the same as a 2 ms lookup. When memory fills, the store can throw out a result that took a second to compute to make room for one that takes two milliseconds. LRU was designed for memory pages, where every entry is the same size and costs the same to bring back. That assumption holds for a CPU or an operating system's page cache. It breaks for query results, where one entry can be a thousand times larger or slower to rebuild than its neighbor. Evicting by recency alone treats a 900 ms report and a 2 ms lookup as interchangeable, and they aren't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What teams do today:&lt;/strong&gt; add memory until eviction rarely happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Readyset:&lt;/strong&gt; every entry records its size and how long its query took on the database. At the memory limit, Readyset weighs those against how often the entry is read. A new result gets in only if it's worth more than what it would push out, so a burst of cheap one-off queries leaves your expensive results in place. &lt;em&gt;Uses: result size and query cost.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  7. The same query returns different answers
&lt;/h3&gt;

&lt;p&gt;The same SQL with the same parameters can return different results depending on the session. Character set and collation change how text compares and sorts. Timezone changes how timestamps come back. SQL mode decides what happens on division by zero or an invalid date. A key built from the query and its parameters ignores all of that, and a session gets a result meant for someone else's settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What teams do today:&lt;/strong&gt; mostly nothing. It rarely shows up until two clients with different settings share a cache, and then it looks like a data bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Readyset:&lt;/strong&gt; the key includes the statement, its parameters, the schema it resolves against, and the session settings that affect the result. A hit returns what the database would have sent to that session. Collation alone has plenty of edge cases; we wrote up &lt;a href="https://readyset.io/blog/collation-improvements-in-readyset" rel="noopener noreferrer"&gt;how Readyset handles them&lt;/a&gt;. &lt;em&gt;Uses: the session's settings, seen on the wire.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  8. The cache doesn't know who's asking
&lt;/h3&gt;

&lt;p&gt;A key-value store hands the value to anyone with the key, including a user whose access was revoked an hour ago. Under row-level security, the same SQL returns different rows for different users, which is why most caches skip it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On a key-value store:&lt;/strong&gt; permission checks in every read path and per-tenant key namespaces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Readyset:&lt;/strong&gt; it checks each user's permissions when the query is first created, keeps it up to date by querying the database every 5 minutes, stores a local copy, and consults it before returning data. When it isn't sure, it forwards the query to the database. For RLS, it folds the security context, such as the user ID or a JWT claim, into the cache key. If it hits a policy it can't interpret, it doesn't cache that query. &lt;em&gt;Uses: the database user and security context behind each query.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What knowing the query makes possible
&lt;/h2&gt;

&lt;p&gt;Every fix above comes from the same place. Readyset sees each query on the wire, so it knows things about a cached result that a key-value store never will.&lt;/p&gt;

&lt;p&gt;The result is a cache that keeps p99 latency flat under load, while background workers keep results fresh at the right interval for each one. That's what a TTL cache was supposed to be. It needs the query to get there, and your application keeps sending SQL exactly as before.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about Redis's own answers?
&lt;/h2&gt;

&lt;p&gt;The strongest evidence for this argument comes from Redis itself. It has shipped two products aimed at exactly this gap: one teaches Redis about SQL queries, and one keeps Redis in sync with the database. A company builds those because its customers keep running into the limits of a plain key-value store.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/redis-field-engineering/redis-smart-cache" rel="noopener noreferrer"&gt;Redis Smart Cache&lt;/a&gt;&lt;/strong&gt; wraps your JDBC driver and caches SQL results in Redis without code changes. It records access frequency and mean query time, and caching is driven by rules you configure per query. Because it's a JDBC wrapper, it covers JVM applications; a Python, Node, Go, or Ruby service needs something else. Freshness is still rule-based TTL on top of Redis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://redis.io/docs/latest/integrate/redis-data-integration/architecture/" rel="noopener noreferrer"&gt;Redis Data Integration&lt;/a&gt;&lt;/strong&gt; (RDI) uses change data capture to copy rows from your database into Redis hashes or JSON documents, typically within seconds. That keeps rows fresh, but it replicates tables, not query results. Your application now reads a Redis data model instead of sending SQL, so joins, filters, and aggregates move into application code or pipeline transforms. Per &lt;a href="https://redis.io/docs/latest/operate/rc/rdi/" rel="noopener noreferrer"&gt;Redis Cloud's docs&lt;/a&gt;, it assumes the app can tolerate eventual consistency.&lt;/p&gt;

&lt;p&gt;Readyset takes the third path: keep the SQL, keep your driver in any language, and put the query-aware work in a proxy between the application and the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a few seconds of staleness is too much
&lt;/h2&gt;

&lt;p&gt;Some workloads can't accept any staleness. For those, Readyset has a second mode: push-based caching. Instead of pulling fresh results from the database on a schedule, it pushes each write into the cached results as it happens. It's built on Noria, a dataflow system developed at MIT and presented at OSDI '18.&lt;/p&gt;

&lt;p&gt;When you cache a query, Readyset turns it into a graph of SQL operators ending in a node that holds the result set. It follows the database's replication stream and turns each &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt; into a delta that flows through the graph. Each operator touches only the affected rows, and the cached results change in place. The graph holds only the keys your application has asked for.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fje9bkaiexmjpd355dctp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fje9bkaiexmjpd355dctp.png" alt="Push-based caching dataflow: the replication stream feeds the orders table through a filter and the users table into a join, ending at a reader node that holds the cached results, which application reads look up; each INSERT, UPDATE, and DELETE becomes a delta that touches only the affected rows" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's no invalidation logic to write and no TTL to tune. Write-heavy tables stay cached too, since each write costs only the rows it touched. And unlike row-level CDC into a key-value store, what stays fresh is the query result itself, joins and aggregates included.&lt;/p&gt;

&lt;p&gt;For more on this mode, see &lt;a href="https://readyset.io/blog/dont-use-kv-stores" rel="noopener noreferrer"&gt;Don't use key-value stores to cache from relational databases&lt;/a&gt; and &lt;a href="https://readyset.io/blog/cache-invalidation-sql-queries" rel="noopener noreferrer"&gt;Cache invalidation for SQL queries&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents make the case stronger
&lt;/h2&gt;

&lt;p&gt;Everything above assumes a person wrote the read path. That assumption is fading. A growing share of the SQL hitting production databases is written by agents at runtime, and that changes caching in four ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache-aside has nothing to wrap.&lt;/strong&gt; Cache-aside works by putting cache code around a read path a developer wrote. An agent's query doesn't exist until the model writes it, so there's no function to wrap and no place to put the check, the miss handling or the key design. Only a cache that sees the SQL itself can cache an agent's reads at all. For agent traffic, the query is the only thing there is, which is the argument of this piece taken to its end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0k889w4vmh5pgg67bl3e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0k889w4vmh5pgg67bl3e.png" alt="In an app written by engineers, cache-aside code wraps each read path like getOrders, getUser, and topProducts, and the query set changes at release speed; with an agent, new SQL is written each prompt so there is no read path to wrap and the cache has to sit where the SQL arrives, between the agent and the database" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody can pick the hot queries in advance.&lt;/strong&gt; In an app written by people, the set of read paths changes at release speed, and someone can keep the cache list roughly current. Agent queries change with every prompt. The workable way to decide what to cache is to watch live traffic and rank queries by the load they put on the database, which is what QueryPilot does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent traffic has the shape that breaks naive caches.&lt;/strong&gt; Agents fan out: parallel sub-agents and concurrent sessions often ask for the same data at the same moment, which is a stampede by another name. They also explore, firing off one-off queries that would flush an LRU cache of the expensive results everyone else depends on. Request coalescing and cost-aware eviction were built for exactly these patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A wrong answer becomes a wrong action.&lt;/strong&gt; A person looking at a slightly stale dashboard usually notices. An agent takes the stale number, acts on it, and hands it to the next step. The session and permission problems get sharper the same way: an agent acting for one user that receives another user's cached rows is a data leak with no person in the loop to catch it. Session-aware keys, RLS-aware keys and permission checks become requirements. For reads that drive actions, push-based caching keeps results current as writes arrive; pull-based caching covers exploratory reads where a few seconds of staleness is fine.&lt;/p&gt;

&lt;p&gt;This is also where database load is heading. Agents don't wait for a page to render or log off at night, and their query volume grows with the number of agents, not the number of users. With hardware scarce, that load has to come off the database somewhere, and a cache that knows the query is the only kind that can take it.&lt;/p&gt;

&lt;p&gt;We'll be honest about what we don't know yet, how cacheable agent SQL is in practice. Our internal tests indicate that it's very repetitive. About 40% of queries are duplicates and cacheable in agentic workloads as per our internal testing. Many agents run the same playbooks at the same time, so they ask for the same data with different parameters. Many don't write free-form SQL at all; they call tools that wrap fixed queries. And agents break big tasks into many small follow-up lookups, which repeat far more often than one-off reports. The open question is free-form text-to-SQL, where two agents can ask the same question in differently shaped SQL. We've started tagging agent traffic in our proxy to measure this, and we'll publish what we find.&lt;/p&gt;

&lt;h2&gt;
  
  
  One connection string
&lt;/h2&gt;

&lt;p&gt;Both modes sit behind the MySQL and PostgreSQL wire protocols (&lt;a href="https://readyset.io/blog/what-is-a-wire-compatible-sql-caching-proxy" rel="noopener noreferrer"&gt;here's how a wire-compatible caching proxy works&lt;/a&gt;). Making an application cache-aware usually means building against a cache API and rewriting every read path; we've seen teams spend six months to a year on that work. With Readyset, it's a connection string change, in any language.&lt;/p&gt;

&lt;p&gt;Keep Redis for sessions, counters, and queues. For query results, use a cache that knows what a query is.&lt;/p&gt;

&lt;p&gt;Try it at &lt;a href="https://readyset.io/downloads" rel="noopener noreferrer"&gt;readyset.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>engineering</category>
    </item>
    <item>
      <title>Black October: What Actually Happened, and What To Patch</title>
      <dc:creator>ReadySet</dc:creator>
      <pubDate>Tue, 27 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/readyset/black-october-what-actually-happened-and-what-to-patch-556k</link>
      <guid>https://dev.to/readyset/black-october-what-actually-happened-and-what-to-patch-556k</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxuznwwjawvmj7f942l4g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxuznwwjawvmj7f942l4g.png" alt="Black October: What Actually Happened, and What To Patch" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Black October is the name social media gave to a wave of security disclosures in October 2026, including PHP2Shell, the Railway and TiDB Cloud breaches, and the LiteLLM supply-chain attack.&lt;/p&gt;

&lt;p&gt;Most of the "AI is hacking the internet" stories are about AI finding bugs, not exploiting them. The breaches that hurt database teams came from long-lived credentials stored in shared places. Here's what we found and what to do this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Most of the AI in this story works for defenders.&lt;/strong&gt; Vulnerability disclosures doubled this year, and AI tools found the most serious bug on the list before it was widely exploited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-driven attacks are real but rare.&lt;/strong&gt; Only two of the 28 incidents involve an AI-driven attack, and the evidence for one of them is weak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stolen credentials did the real damage.&lt;/strong&gt; Railway, Zeabur, TiDB Cloud, and LiteLLM all came down to long-lived keys stored in one place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your next step:&lt;/strong&gt; Patch the software next to your database, and rotate any database credential stored in a platform's environment variables.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What was Black October?
&lt;/h2&gt;

&lt;p&gt;Black October was a burst of alarm in mid-October 2026, when a list of 28 security incidents spread on Reddit and X. One post said AI agent swarms were hacking about half the internet. Another said a new CVE (Common Vulnerabilities and Exposures entry) was dropping every hour.&lt;/p&gt;

&lt;p&gt;The panic follows a familiar plot. In The Hunt for Red October (1990), the Soviet Union launches a new submarine with a drive so quiet that sonar can barely hear it. When it heads for the US coast, both governments assume its captain, Marko Ramius, plans to attack. One analyst, Jack Ryan, reads the same evidence and reaches the opposite conclusion: Ramius is defecting and bringing the submarine to the West.&lt;/p&gt;

&lt;p&gt;Black October has the same shape. A powerful new capability, AI that finds software flaws at scale, showed up all at once, and the first reading was that it was being turned against everyone. The evidence mostly points the other way. Like Red October, the alarming new arrival is largely on the defenders' side. The damage this month came from something older and quieter.&lt;/p&gt;

&lt;p&gt;We took Ryan's approach. We checked each incident against primary sources and sorted it by what AI actually did. Then we searched security news for critical incidents that the list missed. We build &lt;a href="https://readyset.io/downloads" rel="noopener noreferrer"&gt;Readyset&lt;/a&gt;, a SQL proxy that sits between applications and Postgres or MySQL, so we focused on those databases and the credentials that reach them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How many of the 28 incidents actually involved AI?
&lt;/h2&gt;

&lt;p&gt;Nine of the 28 incidents have a documented or possible AI connection, and none was traced to AI-written code. The other 19 have no documented AI link. &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkrgxxm7e7q5ne3o9x3s2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkrgxxm7e7q5ne3o9x3s2.png" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Readyset review of 28 incidents from the viral Reddit list, primary sources, as of October 22, 2026 (full table in the appendix)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The sources show two more patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Some of it is old news.&lt;/strong&gt; The Medi-Cal billing-vendor breach happened in October 2025. The AOMEI update tampering ran from May through July. Palo Alto patched its GlobalProtect bug in May, and the North Korean campaign started in July 2025. &lt;a href="https://www.securitytoday.de/en/2026/10/22/black-october-many-attacks-had-been-running-for-months/" rel="noopener noreferrer"&gt;SecurityToday&lt;/a&gt; found that many of the attacks began weeks or months earlier.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Some numbers grew in the retelling.&lt;/strong&gt; eMudhra's "master keys" were eight intermediate certificates that never issued a website certificate. Railway said an attacker "may have" reached customer secrets. Zeabur's 321,810 figure counts account records read from a database, not accounts taken over. Cairn's "100+ companies" is an estimate based on token spending.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The closest case of AI-written code isn't on the list. &lt;a href="https://www.bleepingcomputer.com/news/security/misconfigured-supabase-apps-expose-data-in-over-16-000-databases/" rel="noopener noreferrer"&gt;UpGuard found&lt;/a&gt; more than 16,000 misconfigured Supabase databases, most with row-level security turned off. UpGuard attributes most of them to apps built with AI coding agents, but says its scan can't prove that for any single site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is AI really finding more bugs?
&lt;/h2&gt;

&lt;p&gt;Yes. AI tools got very good at finding bugs this year, and defenders use them the most.&lt;/p&gt;

&lt;p&gt;One scene in the film captures what that feels like. Red October's quiet drive confuses the US sonar computer, which logs it as "magma displacement," a natural sound. Sonar operator Seaman Jones plays the recording 10 times faster and hears a machine. Better sensors put more contacts on the screen, and the work becomes telling them apart. That's where the security industry is now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disclosures doubled.&lt;/strong&gt; The US National Vulnerability Database (NVD) logged &lt;a href="https://github.com/jgamblin/monthlyCVEStats/releases/tag/cve-update-September-2026" rel="noopener noreferrer"&gt;72,812 CVEs from January through September&lt;/a&gt;, more than in all of 2025. September alone had 14,943, compared to 4,321 a year earlier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI found the most serious bug on the list.&lt;/strong&gt; PHP2Shell lets an unauthenticated attacker run code on any web server that runs PHP, through code that dates back to 2012. Calif and Wiz each found it, both using the same OpenAI model. OpenAI says &lt;a href="https://decrypt.co/381567/who-found-php-bug-governments-unplug-websites-chatgpt" rel="noopener noreferrer"&gt;AI found it before attackers did&lt;/a&gt;. But Amazon's honeypots logged an exploit attempt four days before disclosure, and a leak forced the patch out a day early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI tools appear in vulnerability credits.&lt;/strong&gt; The &lt;a href="https://nodejs.org/en/blog/vulnerability/october-2026-security-releases" rel="noopener noreferrer"&gt;Node.js Brotli fix&lt;/a&gt; credits OpenAI Codex Security. Microsoft's &lt;a href="https://www.bleepingcomputer.com/news/microsoft/microsoft-october-2026-patch-tuesday-fixes-1-411-flaws-3-zero-days/" rel="noopener noreferrer"&gt;October Patch Tuesday&lt;/a&gt; fixed about 1,411 flaws, a record that the press links to Microsoft's own AI bug hunting. Anthropic's &lt;a href="https://red.anthropic.com/2026/cvd/" rel="noopener noreferrer"&gt;disclosure dashboard&lt;/a&gt; lists 219 CVEs and 365 GitHub security advisories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attackers use few of these bugs.&lt;/strong&gt; &lt;a href="https://www.vulncheck.com/blog/anthropic-glasswing-receipts" rel="noopener noreferrer"&gt;VulnCheck counted&lt;/a&gt; 225 CVEs credited to Anthropic's program as of late September. Attackers have exploited one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Maintainers carry the cost. &lt;a href="https://www.theregister.com/databases/2026/10/16/postgres-19-delayed-after-ai-reviewers-find-more-bugs-than-humans-can-fix/5303471" rel="noopener noreferrer"&gt;PostgreSQL 19 moved to November 19&lt;/a&gt; and dropped several major features. The project cites the volume of fixes to older branches, and press coverage ties that volume to AI-assisted code review. &lt;a href="https://www.theregister.com/security/2026/10/20/swamped-curl-maintainers-take-low-level-flaws-public-to-survive-vulnerability-avalanche/5304198" rel="noopener noreferrer"&gt;curl will publish&lt;/a&gt; low- and medium-severity reports openly starting October 26 because its maintainers can't keep up.&lt;/p&gt;

&lt;p&gt;If you run Postgres or MySQL, patch on a tighter schedule than you did a year ago. &lt;a href="https://www.rapid7.com/blog/post/tr-q3-2026-threat-update-attackers-exploiting-vulnerabilities-within-3-days/" rel="noopener noreferrer"&gt;Rapid7 reports&lt;/a&gt; that attackers now exploit high-severity bugs about three days after disclosure, on average.&lt;/p&gt;

&lt;h2&gt;
  
  
  Have AI agents actually run attacks?
&lt;/h2&gt;

&lt;p&gt;Yes, but the confirmed cases are fewer, and stranger, than the X posts suggest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A lab's own agents caused the Hugging Face incident.&lt;/strong&gt; The "swarm" that people cite came from OpenAI's test environment. According to &lt;a href="https://openai.com/index/hugging-face-model-evaluation-security-incident/" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;, agents running a hacking benchmark with reduced safeguards couldn't solve some tasks and went looking for answers. They broke into OpenAI's own artifact server first, then reached Hugging Face in July. About 1,200 agents were involved. In May, agents that OpenAI was testing also &lt;a href="https://www.theregister.com/security/2026/09/14/openais-malicious-bot-swarm-attacked-rubygems/5296356" rel="noopener noreferrer"&gt;pushed malicious packages to RubyGems&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Criminals use agents to scale up.&lt;/strong&gt; In the &lt;a href="https://thenextweb.com/news/ai-agents-hacker-cairn-100-sites-deere-norforge" rel="noopener noreferrer"&gt;Cairn campaign&lt;/a&gt;, attackers aimed an open-source multi-agent platform at more than 1,200 websites. The agents tried to plant web shells, which are scripts that give an attacker remote control of a server. Reuters named Deere, SharkNinja, and Cummins among the targets, and each reported limited impact. The attackers also left their own control panels exposed, leaking hundreds of credentials. Anthropic's &lt;a href="https://www.anthropic.com/threat-intelligence-report-september-2026" rel="noopener noreferrer"&gt;September threat report&lt;/a&gt; describes one group running Claude Code workflows with little human input, and another that scanned 1.8 million Android apps for hard-coded secrets.&lt;/p&gt;

&lt;p&gt;These agents went after known web bugs and exposed secrets. They didn't invent new kinds of attacks. They made old attacks cheap enough to run against everyone at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where did the real damage come from?
&lt;/h2&gt;

&lt;p&gt;The incidents that hurt database teams this month share a pattern: one long-lived credential, stored in one place, that opened many systems. The weak spots were systems nobody watches closely: the platform that stores your environment variables, your CI/CD (continuous integration and delivery) server, a vendor's control plane, or a laptop with a &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Incident&lt;/th&gt;
&lt;th&gt;How attackers got in&lt;/th&gt;
&lt;th&gt;What they could reach&lt;/th&gt;
&lt;th&gt;Time inside&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.pingcap.com/trust-hub/security/security-update-october-2026/" rel="noopener noreferrer"&gt;TiDB Cloud&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A stolen service-account credential&lt;/td&gt;
&lt;td&gt;Account data, SQL editor text, and encrypted management API keys&lt;/td&gt;
&lt;td&gt;About four weeks (Sep 9-Oct 7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://zeabur.com/zh-TW/blogs/october-2026-security-incident" rel="noopener noreferrer"&gt;Zeabur&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Leaked long-lived AWS keys and a cluster admin token that never expired&lt;/td&gt;
&lt;td&gt;3.2 million environment-variable records and possible access to 865 database services&lt;/td&gt;
&lt;td&gt;Oct 14-15, its second breach since August&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://blog.railway.com/p/security-disclosure-wednesday-october-21st" rel="noopener noreferrer"&gt;Railway&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Not disclosed&lt;/td&gt;
&lt;td&gt;Customer environment variables, which can include database credentials; some were decrypted&lt;/td&gt;
&lt;td&gt;Detected Oct 21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://thehackernews.com/2026/10/litellm-compromised-in-supply-chain.html" rel="noopener noreferrer"&gt;LiteLLM&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;GitHub CLI tokens stolen from three engineers&lt;/td&gt;
&lt;td&gt;A stealer that took &lt;code&gt;.env&lt;/code&gt; files, &lt;code&gt;psql&lt;/code&gt; and &lt;code&gt;mysql&lt;/code&gt; shell history, and keys found through &lt;code&gt;DATABASE_URL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;About two hours live on PyPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://patchstack.com/articles/updraftcentral-key-compromise-wordpress-backdoors/" rel="noopener noreferrer"&gt;UpdraftPlus&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A breach of the vendor's own website&lt;/td&gt;
&lt;td&gt;Connection keys to customer WordPress sites that never expire&lt;/td&gt;
&lt;td&gt;Oct 18-19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.securityweek.com/hijacked-developer-account-used-to-plant-stealer-in-ovhcloud-cli/" rel="noopener noreferrer"&gt;OVHcloud CLI&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A contributor's GitHub account, compromised since March&lt;/td&gt;
&lt;td&gt;Release files for five versions of the tool&lt;/td&gt;
&lt;td&gt;About 94 minutes of bad downloads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;The pattern shared by the Railway, Zeabur, TiDB Cloud, and UpdraftPlus incidents&lt;/p&gt;

&lt;p&gt;Zeabur's own report says its admin credentials were long-lived, stored in plain text, and usable from the public internet. Railway says its incident is one of a series aimed at hosting platforms, with AI inference providers as the final target. A researcher traced cryptocurrency thefts that used keys stored on Railway to about two hours before Railway's disclosure.&lt;/p&gt;

&lt;p&gt;TiDB Cloud matters most to us because it's a managed MySQL-compatible database. PingCAP found no sign that the attacker touched data inside customer databases, and it hasn't asked customers to change database passwords. But the attacker did get text that people typed into the web SQL editor. If anyone on your team pasted a connection string or password into a hosted SQL console, assume that someone else has it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should you patch and rotate this week?
&lt;/h2&gt;

&lt;p&gt;Patch PgBouncer, your database, and the app-tier software next to it, then rotate every database credential stored in a platform's environment variables. The details follow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Patch these components
&lt;/h3&gt;

&lt;p&gt;The list starts with the database tier.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Update to&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.postgresql.org/about/news/pgbouncer-1261-released-fixes-three-cves-3403/" rel="noopener noreferrer"&gt;PgBouncer&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.26.1&lt;/td&gt;
&lt;td&gt;A client with one user's password can sign in as any other user that uses SCRAM password authentication, including superusers. Two other bugs let an unauthenticated client freeze it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.oracle.com/security-alerts/cpuoct2026.html" rel="noopener noreferrer"&gt;MySQL&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;October Critical Patch Update (8.4.13 for the 8.4 line)&lt;/td&gt;
&lt;td&gt;Quarterly MySQL fixes; version 8.4 is affected through 8.4.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://mariadb.com/docs/server/security/cve/community-server" rel="noopener noreferrer"&gt;MariaDB&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;10.6.28, 10.11.19, 11.4.13, 11.8.9, or 12.3.3&lt;/td&gt;
&lt;td&gt;MariaDB published 15 CVEs on October 9 and 16, including three rated 9.9. The fixes shipped in the summer releases, so confirm that you have them.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.postgresql.org/about/news/pgvector-087-released-3392/" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;0.8.7&lt;/td&gt;
&lt;td&gt;Buffer overflow when it builds IVFFlat indexes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://jdbc.postgresql.org/security/" rel="noopener noreferrer"&gt;PgJDBC&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;42.7.14&lt;/td&gt;
&lt;td&gt;Two CVEs in the Java driver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.postgresql.org/about/news/postgresql-186-1711-1615-1519-1424-and-19-beta-3-released-3365/" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;18.6, 17.11, 16.15, 15.19, or 14.24&lt;/td&gt;
&lt;td&gt;No October release, but the August release fixed 28 CVEs. If you skipped it, update before the November release.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://cert.europa.eu/publications/security-advisories/2026-018/" rel="noopener noreferrer"&gt;PHP&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;8.5.12, 8.4.27, 8.3.36, or 8.2.35&lt;/td&gt;
&lt;td&gt;PHP2Shell lets attackers run code on the app servers that hold your database credentials.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-5-1-released/" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;19.5.1 or its backports&lt;/td&gt;
&lt;td&gt;Two bugs rated 10 out of 10. CI/CD variables often hold production database credentials.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://nodejs.org/en/blog/vulnerability/october-2026-security-releases" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;22.23.4, 24.22.1, or 26.12.1&lt;/td&gt;
&lt;td&gt;Brotli decoder bug, plus fixes for TLS hostname checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://bleepingcomputer.com/news/security/f5-releases-out-of-band-patches-for-critical-nginx-vulnerabilities" rel="noopener noreferrer"&gt;NGINX&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.31.7 or 1.30.6&lt;/td&gt;
&lt;td&gt;Three memory bugs, each needing a specific configuration (alias, AIO with SSI, or a regex map)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If any computer installed LiteLLM 1.107.0 on October 21, treat it as compromised. Rotate every credential it could read, including database passwords.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reduce credential risk
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Rotate every database credential that you've stored in a hosting platform's environment variables, a CI/CD system, or a hosted SQL console.&lt;/li&gt;
&lt;li&gt;Switch to short-lived credentials where your platform supports them, such as cloud identity and access management (IAM) database authentication or dynamic secrets from a vault.&lt;/li&gt;
&lt;li&gt;Create a separate database user for each service and each AI agent, with only the access it needs. Don't let agents use the application's sign-in. The same goes for any proxy in the path: Readyset, PgBouncer, or ProxySQL should each connect with its own user, not the application's.&lt;/li&gt;
&lt;li&gt;Block public internet access to your database, and require TLS.&lt;/li&gt;
&lt;li&gt;Turn on audit logging for your database and for any control plane that manages it.&lt;/li&gt;
&lt;li&gt;Store backups offsite, and make them immutable. The &lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa26-293a" rel="noopener noreferrer"&gt;SafePay advisory&lt;/a&gt; describes attackers resetting backup appliances and deleting backups before they encrypt data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Per-agent database users get hard to manage once you have more than a few agents. That's the problem we're building Readyset Guard to solve: it applies per-agent access policies at the proxy, so each agent gets only the queries and data it should.&lt;/p&gt;

&lt;h2&gt;
  
  
  One ping only
&lt;/h2&gt;

&lt;p&gt;The film ends well because Ryan read the evidence instead of the noise. Near the end, Ramius asks for "one ping only": a single sonar pulse that shows exactly where he is after hours of guessing on both sides.&lt;/p&gt;

&lt;p&gt;That's the standard we'd hold this month's coverage to. The AI story is mostly good news: AI is finding old bugs faster than attackers, and patches are shipping. The part that cost people data and money was quieter. It came in through keys that had sat in the same place for months.&lt;/p&gt;

&lt;p&gt;Patch what sits next to your database, and spend an afternoon on the systems nobody watches. You'll learn more there than from the news feed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix: All 28 incidents
&lt;/h2&gt;

&lt;p&gt;We checked each incident against primary sources where they exist. Relevance means relevance to teams that run Postgres or MySQL.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Incident&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;What AI did&lt;/th&gt;
&lt;th&gt;Relevance&lt;/th&gt;
&lt;th&gt;What the list got wrong&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://cert.europa.eu/publications/security-advisories/2026-018/" rel="noopener noreferrer"&gt;PHP2Shell&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Fixed Oct 21&lt;/td&gt;
&lt;td&gt;Found the bug&lt;/td&gt;
&lt;td&gt;Medium-high&lt;/td&gt;
&lt;td&gt;Understated it. "70%" is PHP's share of websites, not the share that's vulnerable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://thehackernews.com/2026/10/eight-emudhra-ca-keys-compromised.html" rel="noopener noreferrer"&gt;eMudhra&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 20&lt;/td&gt;
&lt;td&gt;Found it (weak link)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Intermediate keys, not root keys. None issued a website certificate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cde.ca.gov/nr/in/ets-notification.asp" rel="noopener noreferrer"&gt;California students&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Access May-Sep; notice Oct 19&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;The breach was at ETS, the testing vendor.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://blog.railway.com/p/security-disclosure-wednesday-october-21st" rel="noopener noreferrer"&gt;Railway&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 21&lt;/td&gt;
&lt;td&gt;AI keys targeted&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Railway says "may have accessed" and gives no count.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;a href="https://zeabur.com/zh-TW/blogs/october-2026-security-incident" rel="noopener noreferrer"&gt;Zeabur&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 14-17&lt;/td&gt;
&lt;td&gt;AI keys targeted&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Counted 321,810 records read, not accounts taken over.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.stepsecurity.io/blog/litellm-1-107-0-compromised-pypi-backdoored-wheels-credential-stealer" rel="noopener noreferrer"&gt;LiteLLM&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 21&lt;/td&gt;
&lt;td&gt;AI tool compromised&lt;/td&gt;
&lt;td&gt;Medium-high&lt;/td&gt;
&lt;td&gt;The bad release was live for about two hours.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-5-1-released/" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 12 and 21&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Merged two separate bugs rated 10 out of 10 into one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;a href="https://thenextweb.com/news/ai-agents-hacker-cairn-100-sites-deere-norforge" rel="noopener noreferrer"&gt;Cairn&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Sep 25-Oct 19&lt;/td&gt;
&lt;td&gt;Agents ran the attack&lt;/td&gt;
&lt;td&gt;Medium-high&lt;/td&gt;
&lt;td&gt;"100+" is an estimate based on token spending.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;a href="https://bleepingcomputer.com/news/security/retail-giant-ahold-delhaize-takes-systems-offline-after-cyberattack" rel="noopener noreferrer"&gt;Ahold Delhaize&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 11-12&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;The company took systems offline itself, and stores stayed open.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.onamae.com/news/article/11584" rel="noopener noreferrer"&gt;Onamae.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 20-21&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;One VPS product. 26,739 servers were possibly affected.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;&lt;a href="https://securityaffairs.com/201490/security/signal-vulnerability-could-keep-calls-alive-after-users-hang-up.html" rel="noopener noreferrer"&gt;Signal&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Fixed Sep; CVE Oct 20&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;A malicious caller has to trigger it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;&lt;a href="https://bleepingcomputer.com/news/security/f5-releases-out-of-band-patches-for-critical-nginx-vulnerabilities" rel="noopener noreferrer"&gt;NGINX&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 14&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Medium-high&lt;/td&gt;
&lt;td&gt;Rated 8.1 under CVSS 3.1. Each bug needs a non-default configuration.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;&lt;a href="https://thehackernews.com/2026/10/new-qpack-flaws-can-crash-http3-servers.html" rel="noopener noreferrer"&gt;HTTP/3 QPACK&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 22&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Affects HTTP/3 libraries, not mainstream web servers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;&lt;a href="https://thehackernews.com/2026/10/hackers-exploit-plex2shell-flaws-to.html" rel="noopener noreferrer"&gt;Plex&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Exploited from Oct 17&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Patched in August. Only unpatched servers are at risk.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa26-293a" rel="noopener noreferrer"&gt;SafePay&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 20&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;600+ victims, but not all are critical infrastructure.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;&lt;a href="https://unit42.paloaltonetworks.com/active-exploitation-of-pan-os-cve-2026-0257/" rel="noopener noreferrer"&gt;Palo Alto GlobalProtect&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Bug from May; update Oct 19&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Medium-high&lt;/td&gt;
&lt;td&gt;An old bug with a new ransomware attribution.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.bleepingcomputer.com/news/security/bullhorn-takes-staffing-platform-offline-to-contain-cyberattack/" rel="noopener noreferrer"&gt;Bullhorn&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 16-19&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Low-medium&lt;/td&gt;
&lt;td&gt;Accurate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.securityweek.com/970000-medi-cal-members-affected-by-paradigm-healthcare-data-breach/" rel="noopener noreferrer"&gt;Medi-Cal&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Breach Oct 2025&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Low-medium&lt;/td&gt;
&lt;td&gt;A billing vendor, and a year old&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;&lt;a href="https://patchstack.com/articles/updraftcentral-key-compromise-wordpress-backdoors/" rel="noopener noreferrer"&gt;UpdraftPlus (WordPress)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 18-19&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Accurate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;&lt;a href="https://thehackernews.com/2026/10/attackers-tampered-with-aomei.html" rel="noopener noreferrer"&gt;AOMEI&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;May-Jul; disclosed Oct 16&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Months old&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;&lt;a href="https://cybersecuritynews.com/meta-muse-ai-privacy-flaw/" rel="noopener noreferrer"&gt;Meta Muse&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 20-21&lt;/td&gt;
&lt;td&gt;AI product leaked data&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Shared chats only. No misuse seen.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;&lt;a href="https://kaba.cx/posts/20261020_Tor/" rel="noopener noreferrer"&gt;Tor relays&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Aug-Oct 20&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Low-medium&lt;/td&gt;
&lt;td&gt;Hacked exit relays stole SSH passwords, not general sign-ins.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.pingcap.com/trust-hub/security/security-update-october-2026/" rel="noopener noreferrer"&gt;TiDB Cloud&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Sep 9-Oct 19&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;The attacker's 10.6 TB and 180,000-organization claims are unconfirmed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;&lt;a href="https://thehackernews.com/2026/10/state-bank-of-india-confirms-data-leak.html" rel="noopener noreferrer"&gt;State Bank of India&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 21&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;3,897 records confirmed. SBI hasn't confirmed a breach of its systems.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.securityweek.com/hijacked-developer-account-used-to-plant-stealer-in-ovhcloud-cli/" rel="noopener noreferrer"&gt;OVHcloud CLI&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 19&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Accurate, but nobody has analyzed the payload.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.securityweek.com/siemens-software-center-vulnerability-exploited-in-clickfix-malware-attacks/" rel="noopener noreferrer"&gt;Siemens Software Center&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 15-19&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;An installer app, not industrial systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.bleepingcomputer.com/news/security/lazarus-hackers-use-post-quantum-cryptography-for-malware-delivery/" rel="noopener noreferrer"&gt;North Korea (Lazarus)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Since Jul 2025; Oct 21&lt;/td&gt;
&lt;td&gt;Attack (weak link)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Not new. The AI link is one generated photo.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nodejs.org/en/blog/vulnerability/october-2026-security-releases" rel="noopener noreferrer"&gt;Node.js Brotli&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 21&lt;/td&gt;
&lt;td&gt;Found the bug&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Code execution was shown only on a weakened build.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What the list missed
&lt;/h3&gt;

&lt;p&gt;Database and credential items come first.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Incident&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;What AI did&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.postgresql.org/about/news/pgbouncer-1261-released-fixes-three-cves-3403/" rel="noopener noreferrer"&gt;PgBouncer SCRAM bypass&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 14&lt;/td&gt;
&lt;td&gt;None stated&lt;/td&gt;
&lt;td&gt;Lets a client sign in as any SCRAM user, including superusers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.oracle.com/security-alerts/cpuoct2026.html" rel="noopener noreferrer"&gt;MySQL October patch update&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 20&lt;/td&gt;
&lt;td&gt;None stated&lt;/td&gt;
&lt;td&gt;Quarterly MySQL fixes; 8.4 is affected through 8.4.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://mariadb.com/docs/server/security/cve/community-server" rel="noopener noreferrer"&gt;MariaDB&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 9 and 16&lt;/td&gt;
&lt;td&gt;None stated&lt;/td&gt;
&lt;td&gt;15 CVEs published, three rated 9.9; fixes already shipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.postgresql.org/about/news/pgvector-087-released-3392/" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 5&lt;/td&gt;
&lt;td&gt;None stated&lt;/td&gt;
&lt;td&gt;Overflow when it builds indexes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.bleepingcomputer.com/news/security/misconfigured-supabase-apps-expose-data-in-over-16-000-databases/" rel="noopener noreferrer"&gt;Supabase exposure&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Sep 28&lt;/td&gt;
&lt;td&gt;AI-built apps, probably&lt;/td&gt;
&lt;td&gt;16,000+ readable databases, most with row-level security turned off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://thehackernews.com/2026/10/opencode-console-breach-may-have.html" rel="noopener noreferrer"&gt;OpenCode Console&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 16-17&lt;/td&gt;
&lt;td&gt;AI keys targeted&lt;/td&gt;
&lt;td&gt;Stored LLM API keys may have been exposed for about 1.4 percent of users.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://thehackernews.com/2026/10/atlassian-data-center-flaw-draws.html" rel="noopener noreferrer"&gt;Atlassian Data Center&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Patched about Oct 6&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Unauthenticated file read that leads to admin access, exploited within hours of a public write-up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.securityweek.com/chinese-apt-exploited-beyondtrust-vulnerability-as-zero-day/" rel="noopener noreferrer"&gt;BeyondTrust Remote Support&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Fixed Oct 8&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;A zero-day exploited since August&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.bleepingcomputer.com/news/security/ivanti-warns-of-connect-secure-zero-day-exploited-in-attacks/" rel="noopener noreferrer"&gt;Ivanti Connect Secure&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 13&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;A zero-day, with part of the attack chain still unpatched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.abc.net.au/news/2026-09-24/ai-agent-accessed-australian-government-site-pm-says/107189078" rel="noopener noreferrer"&gt;OpenAI agent and Medicare Australia&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Disclosed Sep 24&lt;/td&gt;
&lt;td&gt;Agent ran the access&lt;/td&gt;
&lt;td&gt;A lab's test agent reached a government statistics portal.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>news</category>
    </item>
    <item>
      <title>Readyset HA with ProxySQL and MySQL</title>
      <dc:creator>ReadySet</dc:creator>
      <pubDate>Thu, 22 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/readyset/readyset-ha-with-proxysql-and-mysql-1gbm</link>
      <guid>https://dev.to/readyset/readyset-ha-with-proxysql-and-mysql-1gbm</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwx9px7rm1ln7juutug96.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwx9px7rm1ln7juutug96.png" alt="Readyset HA with ProxySQL and MySQL" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A query cache stores the result of a SQL query and answers later identical queries from that stored copy instead of running them again. Readyset is a query cache for MySQL and PostgreSQL that works as a proxy: it speaks the database's wire protocol, so it can decide per query whether to answer from cache or pass the query through.&lt;/p&gt;

&lt;p&gt;Speaking the wire protocol is what makes Readyset drop-in: anything that can connect to MySQL can connect to Readyset instead, with no driver or query change. Put two Readyset nodes behind ProxySQL and you have a cache tier that survives losing a node.&lt;/p&gt;

&lt;p&gt;We wanted to know how small that change really is when the topology already exists. So we built the boring version of a production setup, a MySQL source with a replica and a pair of ProxySQL nodes in cluster mode in front of both, gave it a report query that costs 106 ms, and counted the steps needed to put a highly available (HA) cache behind that one query. It came to two &lt;code&gt;docker run&lt;/code&gt;commands and two &lt;code&gt;INSERT&lt;/code&gt; statements in ProxySQL's admin interface. Nothing in the application changed, and neither did the schema or MySQL's configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a ProxySQL and MySQL topology looks like before Readyset
&lt;/h2&gt;

&lt;p&gt;Seven machines in one availability zone, all &lt;code&gt;c6i.large&lt;/code&gt;, which is 2 virtual central processing units (vCPUs) and 4 GiB of memory each. MySQL 8.0.46 on the source and on the replica, two ProxySQL 2.6.5 nodes in front of them, and the &lt;a href="https://dev.mysql.com/doc/sakila/en/" rel="noopener noreferrer"&gt;sakila sample database&lt;/a&gt; loaded on the source and replicated down.&lt;/p&gt;

&lt;p&gt;Both ProxySQL nodes are configured the way most people configure one: hostgroup 10 is the writer, hostgroup 20 is the reader, writes go to the source, and a catch-all rule sends anything matching &lt;code&gt;^SELECT&lt;/code&gt; to the replica.&lt;/p&gt;

&lt;p&gt;The two proxies run in ProxySQL's cluster mode, which makes them peers. Each lists the other in &lt;code&gt;proxysql_servers&lt;/code&gt;, they share a cluster credential, and a configuration change applied to either one is pulled by the other within a few seconds. That is why adding Readyset below costs the same two statements it would cost with a single proxy.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hostgroup&lt;/th&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;MySQL source&lt;/td&gt;
&lt;td&gt;all writes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;MySQL replica&lt;/td&gt;
&lt;td&gt;all reads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything in grey below was already running. Everything in green is what this post adds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frx4h5kykapdeakt4a6jf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frx4h5kykapdeakt4a6jf.png" alt="Topology: an application connects to either of two clustered ProxySQL nodes, which route writes to the MySQL source, most SELECT statements to the MySQL replica, and one query digest to a new hostgroup holding two Readyset nodes with the replica as a weight 1 fallback" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The two Readyset nodes and the hostgroup 30 routing are the whole change. Readyset connects to the replica as an ordinary SQL client, so there is no replication stream into it and nothing to configure on MySQL. The purple link is ProxySQL's cluster sync, which is why the routing has to be added on only one of the two proxies.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Which query is worth caching, and how ProxySQL tells you
&lt;/h2&gt;

&lt;p&gt;ProxySQL already knows. It keeps a digest table of every query shape it has seen, with the total time spent on each, and the answer falls out of one query against the admin interface on port 6032.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;hostgroup&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count_star&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sum_time&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sum_time&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stats_mysql_query_digest&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;sum_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="n"&gt;hg&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="n"&gt;total_s&lt;/span&gt; &lt;span class="n"&gt;avg_ms&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
&lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;xAD23E0866F6BDE4B&lt;/span&gt; &lt;span class="mi"&gt;140&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;xDDE78179CF5B1440&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;film&lt;/span&gt;
&lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;xAC447120122130DE&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;
&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;xFE0D5031018F599A&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;NAMES&lt;/span&gt; &lt;span class="n"&gt;utf8mb4&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ProxySQL admin interface is SQLite underneath, so &lt;code&gt;substr()&lt;/code&gt; works and&lt;code&gt;LEFT()&lt;/code&gt; does not. One shape accounts for all of the time. It is a customer leaderboard: the top 25 customers by lifetime value, each annotated with the film category they spend the most on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;lifetime_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
          &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="n"&gt;p2&lt;/span&gt;
          &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;rental&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rental_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rental_id&lt;/span&gt;
          &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt; &lt;span class="n"&gt;i2&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;i2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory_id&lt;/span&gt;
          &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;film_category&lt;/span&gt; &lt;span class="n"&gt;fc&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;film_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;film_id&lt;/span&gt;
          &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category_id&lt;/span&gt;
         &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
         &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
         &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
         &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;top_category&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address_id&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="n"&gt;ci&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;city_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;city_id&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
 &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;lifetime_value&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
 &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query is not a straw man. &lt;code&gt;EXPLAIN&lt;/code&gt; shows every join resolving through an index, and sakila is about 10 MB against a 2 GB buffer pool, so the whole working set is in memory and no read touches a disk. It costs 106 ms anyway, because the correlated subquery is a &lt;code&gt;DEPENDENT SUBQUERY&lt;/code&gt; that MySQL evaluates once per customer. That is 599 executions of a five-table join, each building a temporary table and sorting it, on every single call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just rewrite the slow query
&lt;/h2&gt;

&lt;p&gt;That is the first thing a database engineer asks, so we measured it. We rewrote the correlated subquery as a window function over a common table expression, which is the textbook fix, and ran both forms against the same replica.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Form&lt;/th&gt;
&lt;th&gt;p50 latency&lt;/th&gt;
&lt;th&gt;Queries per second&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original, with correlated subquery&lt;/td&gt;
&lt;td&gt;106.360 ms&lt;/td&gt;
&lt;td&gt;9.44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rewritten with &lt;code&gt;ROW_NUMBER()&lt;/code&gt; over a common table expression&lt;/td&gt;
&lt;td&gt;88.595 ms&lt;/td&gt;
&lt;td&gt;11.32&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Single connection, 8 s warmup and 25 s measured, read directly against the MySQL replica.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The rewrite is worth 1.2x. The work being done is the same either way: aggregate 16,044 payments across a five-table join, then do the whole thing again for the next caller. Routing the query to Readyset is worth 149x on the same hardware, and it gets there by skipping the work rather than by doing it faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  The minimal steps to add Readyset behind ProxySQL
&lt;/h2&gt;

&lt;p&gt;Three commands total, and none of them touch MySQL, the application, or the second proxy.&lt;/p&gt;

&lt;p&gt;First, run Readyset on two hosts. Each one is independent of the other, with no shared state and no clustering to configure. &lt;code&gt;AUTO_CACHE=true&lt;/code&gt; is the whole configuration: it is shorthand for &lt;code&gt;--cache-mode=shallow --query-caching=inrequestpath&lt;/code&gt;, which means every previously unseen &lt;code&gt;SELECT&lt;/code&gt;becomes a candidate for a cache that Readyset creates by itself, on the request path, the first time it sees the query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; readyset &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 3307:3307 &lt;span class="nt"&gt;-p&lt;/span&gt; 6034:6034 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;UPSTREAM_DB_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysql://app:readyset@&amp;lt;replica-ip&amp;gt;:3306/sakila &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;LISTEN_ADDRESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.0.0.0:3307 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;AUTO_CACHE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  public.ecr.aws/readyset/readyset:latest-nightly

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We pointed both nodes at the read replica rather than the source, so the queries Readyset runs upstream land on the machine that already serves reads.&lt;/p&gt;

&lt;p&gt;Docker is here because it makes the deployment one command that a reader can run verbatim, which is what a demonstration needs. It is not the only option: the same adapter ships as &lt;code&gt;.deb&lt;/code&gt; and &lt;code&gt;.rpm&lt;/code&gt; packages for x86_64 and arm64, run under systemd and configured through &lt;code&gt;/etc/readyset/readyset.conf&lt;/code&gt;. Both paths are covered at&lt;a href="https://readyset.io/docs/cache/install-rs" rel="noopener noreferrer"&gt;Install Readyset Cache&lt;/a&gt;. The flags below are the same either way, as environment variables in Docker or as settings in the configuration file.&lt;/p&gt;

&lt;p&gt;Both nodes were accepting connections about 45 seconds after &lt;code&gt;docker run&lt;/code&gt;returned, and that is the whole startup. In shallow mode Readyset does not replicate, which the log states plainly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO readyset_server::controller::inner: Replication disabled; skipping snapshotting and replication

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No replication user, no binary-log position, no snapshot of the dataset, no publication and no slot. Readyset connects to the upstream the same way any client does, runs the query on a miss, and keeps the result.&lt;/p&gt;

&lt;p&gt;Second, tell ProxySQL the nodes exist, and point one query shape at them. This is the only change to the existing topology, and in cluster mode it is applied to one proxy rather than to each of them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Hostgroup 30 is the new cache tier. The replica joins it at weight 1, which&lt;/span&gt;
&lt;span class="c1"&gt;-- makes it an automatic fallback: it takes 1 read in 2001 while Readyset is&lt;/span&gt;
&lt;span class="c1"&gt;-- healthy, and all of them when both Readyset nodes are gone.&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;mysql_servers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hostgroup_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_connections&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;readyset-1-ip&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3307&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'readyset-1'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;readyset-2-ip&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3307&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'readyset-2'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;replica-ip&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3306&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'mysql-replica-fallback'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Route exactly one query shape at the cache, by the digest ProxySQL reported.&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;mysql_query_rules&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rule_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination_hostgroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0xAD23E0866F6BDE4B'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;LOAD&lt;/span&gt; &lt;span class="n"&gt;MYSQL&lt;/span&gt; &lt;span class="n"&gt;SERVERS&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;RUNTIME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;LOAD&lt;/span&gt; &lt;span class="n"&gt;MYSQL&lt;/span&gt; &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;RULES&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;RUNTIME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;SAVE&lt;/span&gt; &lt;span class="n"&gt;MYSQL&lt;/span&gt; &lt;span class="n"&gt;SERVERS&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;DISK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;SAVE&lt;/span&gt; &lt;span class="n"&gt;MYSQL&lt;/span&gt; &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;RULES&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;DISK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Routing by digest rather than by table is what keeps the change small. The same tables keep serving hostgroup 20 for every other query. Only this one shape moves.&lt;/p&gt;

&lt;p&gt;We ran that against &lt;code&gt;proxysql-1&lt;/code&gt; and nothing at all against &lt;code&gt;proxysql-2&lt;/code&gt;. The second proxy picked the change up on its own in 3.7 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;### proxysql-2 BEFORE, nothing applied to it directly
hostgroup 30 servers: 0
rule 50 present: 0

### applying the change to proxysql-1 ONLY
propagated to proxysql-2 in 3.7s, with no command run against it

### proxysql-2 AFTER
30 172.31.38.52 3306 1 mysql-replica-fallback
30 172.31.39.254 3307 1000 readyset-1
30 172.31.44.109 3307 1000 readyset-2
rule 50:
50 1 0xAD23E0866F6BDE4B 30

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is what cluster mode buys. Adding a cache tier to a pair of proxies is the same two statements as adding it to one, so the two proxies cannot drift into routing the same query differently.&lt;/p&gt;

&lt;p&gt;One credential detail is easy to miss. Readyset authenticates incoming clients against its own upstream user, so the user ProxySQL uses for its backend connections has to be the same user that appears in &lt;code&gt;UPSTREAM_DB_URL&lt;/code&gt;. A separate application user gets an access denied error at the Readyset connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed after routing one query to Readyset
&lt;/h2&gt;

&lt;p&gt;Same driver, same ProxySQL endpoint, same query text. The only difference between the two halves of each pair is whether rule 50 is active.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;MySQL p50&lt;/th&gt;
&lt;th&gt;Readyset p50&lt;/th&gt;
&lt;th&gt;MySQL q/s&lt;/th&gt;
&lt;th&gt;Readyset q/s&lt;/th&gt;
&lt;th&gt;Latency ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;106.376 ms&lt;/td&gt;
&lt;td&gt;0.716 ms&lt;/td&gt;
&lt;td&gt;9.43&lt;/td&gt;
&lt;td&gt;1291.93&lt;/td&gt;
&lt;td&gt;149x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;396.784 ms&lt;/td&gt;
&lt;td&gt;2.401 ms&lt;/td&gt;
&lt;td&gt;10.27&lt;/td&gt;
&lt;td&gt;1463.33&lt;/td&gt;
&lt;td&gt;165x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;795.061 ms&lt;/td&gt;
&lt;td&gt;5.398 ms&lt;/td&gt;
&lt;td&gt;10.33&lt;/td&gt;
&lt;td&gt;1301.70&lt;/td&gt;
&lt;td&gt;147x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;10 s warmup, 30 s measured, zero errors in every run. Latency ratio is p50 against p50.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx44l3dnxt69p956gg8qz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx44l3dnxt69p956gg8qz.png" alt="Bar chart of median latency for the same query at one, four and eight clients: the MySQL replica takes 107, 403 and 795 ms while Readyset takes 0.765, 2.385 and 5.311 ms" width="799" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Readyset bars are drawn at a 3 pixel minimum so they are visible at all; at this scale their true widths are half a pixel. These are 2 vCPU demo machines and one query, so read the shape rather than the absolute numbers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The rows come back identical. We compared the full result set and every field of every column definition between the cached path and the replica read directly: 25 rows, 7 columns, byte for byte the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to confirm Readyset served the reads
&lt;/h2&gt;

&lt;p&gt;A cache that exists is not a cache that is being used, and the failure mode looks like success: correct rows at a plausible latency, quietly served by the database. So we checked three separate things.&lt;/p&gt;

&lt;p&gt;ProxySQL answers the first two: is the rule firing, and where did the query actually go. Both come from the admin interface on port 6032.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- is the routing rule matching, and how often&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rule_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;destination_hostgroup&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stats_mysql_query_rules&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;runtime_mysql_query_rules&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rule_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rule_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- and which hostgroup actually served that query shape&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;hostgroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count_star&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stats_mysql_query_digest&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'0xAD23E0866F6BDE4B'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We sent 800 reads at &lt;code&gt;proxysql-1&lt;/code&gt; and 400 at &lt;code&gt;proxysql-2&lt;/code&gt;, and each proxy counted only its own share:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- proxysql-1 (drove 800) ---
hostgroup digest count_star
30 0xAD23E0866F6BDE4B 800

--- proxysql-2 (drove 400) ---
rule_id hits destination_hostgroup
50 400 30
100 1 20
hostgroup digest count_star
30 0xAD23E0866F6BDE4B 400

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both proxies send the digest to hostgroup 30, and rule 100, the pre-existing catch-all, still handles everything else. One caveat if you run this yourself:&lt;code&gt;stats_mysql_query_rules&lt;/code&gt; is flushed asynchronously, so straight after a burst it can lag the digest table by a few seconds before the two agree.&lt;/p&gt;

&lt;p&gt;That proves the query reached Readyset. It does not prove Readyset answered from cache rather than passing the query upstream, which is the failure that looks exactly like success. Readyset's own per-query counters settle it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight prometheus"&gt;&lt;code&gt;&lt;span class="n"&gt;curl&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;localhost:6034&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="n"&gt;grep&lt;/span&gt; &lt;span class="n"&gt;readyset_shallow_shallow_result&lt;/span&gt;


&lt;span class="o"&gt;---&lt;/span&gt; &lt;span class="n"&gt;readyset&lt;/span&gt;&lt;span class="mi"&gt;-1&lt;/span&gt; &lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="n"&gt;readyset_shallow_shallow_result_hit&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;query_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"q_d618b5595f3efeab"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mi"&gt;98347&lt;/span&gt;
&lt;span class="n"&gt;readyset_shallow_shallow_result_miss&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;query_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"q_d618b5595f3efeab"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="o"&gt;---&lt;/span&gt; &lt;span class="n"&gt;readyset&lt;/span&gt;&lt;span class="mi"&gt;-2&lt;/span&gt; &lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="n"&gt;readyset_shallow_shallow_result_hit&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;query_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"q_d618b5595f3efeab"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mi"&gt;93984&lt;/span&gt;
&lt;span class="n"&gt;readyset_shallow_shallow_result_miss&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;query_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"q_d618b5595f3efeab"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hits move with the traffic and misses stay in single digits. Both nodes built the same cache for the same query identifier on their own, and both carried roughly half the reads. A Readyset node whose hit counter is flat while queries flow through it is serving everything from the database, and this is the counter to alert on.&lt;/p&gt;

&lt;p&gt;Then we measured the instrument itself. A bare &lt;code&gt;SELECT 1&lt;/code&gt; over the same path, from the same driver through the same ProxySQL, costs 0.280 ms at p50. That is the client library, two network hops and the proxy, and it is 39% of Readyset's 0.716 ms. Readyset's own contribution to the cached read is closer to 0.44 ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when a Readyset node or a ProxySQL node dies
&lt;/h2&gt;

&lt;p&gt;Two tiers can now lose a node, so we killed one on each.&lt;/p&gt;

&lt;p&gt;We ran the query continuously with four clients and stopped the Readyset nodes underneath it, checking every second whether the application was still getting correct answers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;What the client saw&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docker stop&lt;/code&gt; on Readyset 1&lt;/td&gt;
&lt;td&gt;three errored connections in the second it happened, then cached reads again from the survivor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docker stop&lt;/code&gt; on Readyset 2, both now gone&lt;/td&gt;
&lt;td&gt;reads served by the replica through the weight 1 fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughout&lt;/td&gt;
&lt;td&gt;no second passed without completed queries, and the top row of the report never changed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Losing one Readyset node did not show up in the latency series at all. The survivor absorbed the traffic and the application had no way to tell.&lt;/p&gt;

&lt;p&gt;Then we did the same to a proxy, with the load running against &lt;code&gt;proxysql-2&lt;/code&gt; and&lt;code&gt;proxysql-1&lt;/code&gt; stopped underneath it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Queries per second&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Load on proxysql-2, both proxies up&lt;/td&gt;
&lt;td&gt;1460&lt;/td&gt;
&lt;td&gt;2.47 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load on proxysql-2, proxysql-1 stopped&lt;/td&gt;
&lt;td&gt;1454&lt;/td&gt;
&lt;td&gt;2.40 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Zero errors, and no second without completed queries.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The client talking to the surviving proxy did not notice, which is what a second proxy is for. Cluster mode also covers the rebuild. We brought &lt;code&gt;proxysql-1&lt;/code&gt; back from an empty configuration, with its data volume deleted and &lt;code&gt;--initial&lt;/code&gt; on the command line, and it pulled the whole Readyset routing from its peer without being told any of it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hostgroup 30 on the rebuilt proxysql-1, which was never told about Readyset:
30 172.31.38.52 3306 1 mysql-replica-fallback
30 172.31.39.254 3307 1000 readyset-1
30 172.31.44.109 3307 1000 readyset-2
50 1 0xAD23E0866F6BDE4B 30

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Losing both Readyset nodes fell through to the replica and the application kept working at the speed it had before Readyset existed. That is the useful property of the weighted fallback: the degraded mode is the system you already had, not an error page. The handful of errors across these runs were connections in flight when a container was killed, and a client with retry logic would have swallowed them.&lt;/p&gt;

&lt;p&gt;We did not measure how much load a pair of Readyset nodes can take, so none of these figures is a capacity claim. Two 2 vCPU machines is a demonstration size, not a sizing exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Readyset and ProxySQL pattern applies
&lt;/h2&gt;

&lt;p&gt;The topology has to be a proxy that can route by query shape, in front of at least one read query expensive enough that the round trip to Readyset is worth avoiding. On a query that already runs in 2 ms, none of this pays for itself, because the floor we measured is 0.342 ms of driver and network before Readyset does anything at all.&lt;/p&gt;

&lt;p&gt;The two Readyset nodes are independent caches, not a cluster. There is no shared state between them and nothing to configure to pair them up, which is the opposite of the ProxySQL pair, where cluster mode is the whole point. ProxySQL spreads reads across both Readyset nodes and takes either one out of rotation on its own, and that is what makes the Readyset failover simple.&lt;/p&gt;

&lt;p&gt;What kept the change small is that nothing in the application knew about it. The connection string still points at ProxySQL, and the query text, the schema and MySQL's configuration are all exactly what they were. Cluster mode kept it small in the other direction too: a second proxy did not mean a second copy of the work. One query moved from a hostgroup that costs 106 ms to a Readyset hostgroup that costs 0.716 ms, and the rest of the workload never noticed.&lt;/p&gt;

&lt;p&gt;The documentation for shallow caching, the &lt;code&gt;--auto-cache&lt;/code&gt; flag and the other installation methods is at&lt;a href="https://readyset.io/docs/cache/install-rs" rel="noopener noreferrer"&gt;Install Readyset Cache&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>tutorials</category>
      <category>mysql</category>
      <category>querycaching</category>
      <category>databasescaling</category>
    </item>
    <item>
      <title>Meet the RDST Agent</title>
      <dc:creator>ReadySet</dc:creator>
      <pubDate>Wed, 14 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/readyset/meet-the-rdst-agent-26fb</link>
      <guid>https://dev.to/readyset/meet-the-rdst-agent-26fb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnbw3j7hng82a0f6kmj3r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnbw3j7hng82a0f6kmj3r.png" alt="Meet the RDST Agent" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An always-on agent that catches every slow query on your PostgreSQL or MySQL database and gives you the exact fix. It runs on your computer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Product Updates | Readyset team | October 14, 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When a query gets slow, a customer usually notices first. The RDST Agent is built to catch it before that. It's a free, always-on agent that runs on your computer, watches every query on your Postgres or MySQL database, and hands you the exact fix.&lt;/p&gt;

&lt;p&gt;Teams with a DBA or a monitoring budget usually have tooling that watches for this. A lot of teams running Postgres or MySQL on RDS, Supabase, Neon or their own servers don't, so query performance tends to get checked after something breaks.&lt;/p&gt;

&lt;p&gt;Today we're releasing the &lt;strong&gt;RDST Agent&lt;/strong&gt; , a free, always-on agent that runs on your computer, catches every slow query on your database, and gives you the exact fix when something goes wrong.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wpc13pkwtxekmfz2rql.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wpc13pkwtxekmfz2rql.png" alt="RDST Agent home screen showing two monitored databases, one item that needs attention, and the day's agent activity" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Home: two databases, one thing that needs you, and everything the agent did today.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To show what that looks like, we'll follow one afternoon on &lt;code&gt;shop-prod&lt;/code&gt;, the PostgreSQL database behind an online store.&lt;/p&gt;

&lt;h2&gt;
  
  
  It keeps a history of every query
&lt;/h2&gt;

&lt;p&gt;When you turn on monitoring, RDST reads your database's query statistics every minute, from &lt;code&gt;pg_stat_statements&lt;/code&gt; on PostgreSQL or &lt;code&gt;performance_schema&lt;/code&gt; on MySQL. It builds a history for every query: how often it runs, how long it takes and how much of the database's time it uses. The last 48 hours stay minute by minute, and older readings roll up into hourly and daily summaries that reach back months.&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;shop-prod&lt;/code&gt;, the query behind the store's order history page usually takes about 6 ms. At 2:05 PM a deploy adds a new "My orders" page, and that same query starts taking 184 ms. Within minutes it's using almost a third of the database's time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fijle4rw2lkoffgx79173.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fijle4rw2lkoffgx79173.png" alt="Chart of shop-prod at 2:14 PM showing the order history query spiking from 6 ms to 184 ms after the deploy, with a desktop notification" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;shop-prod at 2:14 PM. Order history jumps after the deploy, and the agent sends a desktop notification.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  It notices, and it knows what's worth your time
&lt;/h2&gt;

&lt;p&gt;Every 5 minutes the agent compares each busy query against its own history. It looks for queries that got slower or busier, changed plans, got stuck or started waiting on locks.&lt;/p&gt;

&lt;p&gt;Most of those changes don't deserve an interruption. Jev, Readyset's attention model, decides which ones do. Order history getting 30x slower clears the bar, so at 2:14 PM it lands in the Inbox under &lt;strong&gt;Act now&lt;/strong&gt; and a notification shows up on the desktop. Smaller wobbles stay on the chart for when you go looking.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqpo1zg1daymrpf5oq0f2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqpo1zg1daymrpf5oq0f2.png" alt="Inbox item explaining what changed in the order history query and what to check, with the SQL for each step" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Inbox item explains what changed and what to check, with the SQL for each step.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  It does the analysis nobody has time for
&lt;/h2&gt;

&lt;p&gt;Two minutes later, the agent has a deep analysis. It runs &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; on the query with a real &lt;code&gt;customer_id&lt;/code&gt; sampled from live traffic, and the plan shows the problem right away. Postgres is reading all 2.4 million rows of &lt;code&gt;orders&lt;/code&gt; to find one customer's 20 most recent orders. The fix is a composite index on &lt;code&gt;customer_id&lt;/code&gt; and &lt;code&gt;created_at&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;New heavy queries get this treatment as they show up, and every night the agent writes a report for each database with what to fix first, along with checks for bloat, unused indexes, settings and replication.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2nnoeavthhpdg61lfgs4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2nnoeavthhpdg61lfgs4.png" alt="Deep analysis scoring the query, showing what the execution plan is doing, and naming the composite index to add on customer_id and created_at" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The analysis scores the query, shows what the plan is doing, and names the index to add.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Actions: one list of changes worth making
&lt;/h2&gt;

&lt;p&gt;Everything the agent learns feeds &lt;strong&gt;Actions&lt;/strong&gt; , a ranked list of the changes worth making across your databases. The new index goes straight to the top of &lt;strong&gt;Do first&lt;/strong&gt; , because it saves about 7 seconds of database time every minute.&lt;/p&gt;

&lt;p&gt;Right below it is an index to drop. &lt;code&gt;orders_status_idx&lt;/code&gt; hasn't been used in 30 days, and every one of the 220 writes a second to &lt;code&gt;orders&lt;/code&gt; still has to maintain it. The agent weighs each index against how often its table gets written to, so it'll recommend removing dead weight as readily as adding what's missing. Each action comes with the SQL to run and the SQL to undo it, and you can copy it as a migration for Alembic, Prisma, Rails or Flyway.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifezi43ezlwc5khqtig6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifezi43ezlwc5khqtig6.png" alt="Actions list for both databases, ranked into Do first, Then, and Later, each with the SQL to run and to undo" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Actions for both databases, ranked into Do first, Then and Later.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;RDST never writes to your database.&lt;/strong&gt; You ship the change the way you ship any other migration, and the agent checks whether it worked.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At 3:02 PM the new index shows up on &lt;code&gt;shop-prod&lt;/code&gt;. The agent measures Order history before and after: 184 ms down to 3.8 ms, and from 31% of the database's time to 2%. The action closes as Verified.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn8spjoz6vkv39dn1i5ag.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn8spjoz6vkv39dn1i5ag.png" alt="Verified action showing the order history query measured before and after the index: 184 ms down to 3.8 ms, 31% of database time down to 2%" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Verified: the agent measured the query before and after the index went in.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it runs on your desktop
&lt;/h2&gt;

&lt;p&gt;We could have started with a hosted service. We started on the desktop because it's the shortest path to the databases people actually care about.&lt;/p&gt;

&lt;p&gt;Your database credentials stay in your operating system's keychain. The connection runs from your machine straight to your database, so there's nothing to install on your servers, and Readyset never needs network access to your database. It reaches whatever your machine can reach, including a local Docker container, a database behind your VPN, or a private instance through an SSH jump host. Query history lives in a local SQLite database on your computer.&lt;/p&gt;

&lt;p&gt;There's a tradeoff. The agent only collects while your computer is awake, and right now it's built for one person. For teams that want an agent running around the clock on their own infrastructure, we're working out a cloud version for enterprise teams. If that's you, tell us what you'd need at &lt;a href="mailto:hello@readyset.io"&gt;hello@readyset.io&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What gets sent to AI
&lt;/h2&gt;

&lt;p&gt;Collecting stats, keeping history, running health checks and doing the arithmetic in reports all happen on your machine with no model involved. Two things go out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deep analyses and reports&lt;/strong&gt; run on Claude Haiku from Anthropic. We don't send anything to foreign models. A request carries the query, its execution plan and the schema of the tables it touches, stripped of your data: no table contents, no result rows and no credentials. Anthropic doesn't train on API data. If you'd rather use your own Anthropic key, analysis goes straight from your machine to Anthropic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jev&lt;/strong&gt; sees normalized query text along with table and index statistics, which is what it needs to decide what's worth your attention.&lt;/p&gt;

&lt;p&gt;Every AI call the agent makes is listed on the Agent activity page, with what it did and when. The privacy docs cover the rest.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4hvrq8zjs21nmmexz19w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4hvrq8zjs21nmmexz19w.png" alt="Agent activity page listing every check, analysis, and verification the agent ran that Tuesday" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Agent activity: every check, analysis and verification from that Tuesday.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;RDST is free and open source under Apache 2.0. It supports PostgreSQL 13+ and MySQL 8.0+ and runs on macOS (Apple Silicon), Windows and Linux. There's a demo database built in if you want to watch the agent work before you point it at your own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://readyset.io/downloads" rel="noopener noreferrer"&gt;Download RDST&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read the docs | &lt;a href="https://github.com/readysettech/rdst" rel="noopener noreferrer"&gt;RDST on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We'd love to hear what it finds on your database, and what it gets wrong. Come tell us in the &lt;a href="https://go.readyset.io/community" rel="noopener noreferrer"&gt;Readyset Slack&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>productupdates</category>
      <category>querycaching</category>
    </item>
    <item>
      <title>What Is a Wire-Compatible SQL Caching Proxy?</title>
      <dc:creator>ReadySet</dc:creator>
      <pubDate>Thu, 08 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/readyset/what-is-a-wire-compatible-sql-caching-proxy-24oi</link>
      <guid>https://dev.to/readyset/what-is-a-wire-compatible-sql-caching-proxy-24oi</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnvqo6ufhdcbtm37fotol.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnvqo6ufhdcbtm37fotol.png" alt="What Is a Wire-Compatible SQL Caching Proxy?" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A wire-compatible SQL caching proxy sits between your application and your database, speaking the same protocol the database speaks. Because of this, your app can connect to the proxy instead of the database without any code changes, just swap the hostname or port.&lt;/p&gt;

&lt;p&gt;The idea is simple. Right now, every query goes straight to the database:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffmwhxrauwz0qjh5vb7n2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffmwhxrauwz0qjh5vb7n2.png" alt="Diagram: application connecting directly to the database" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With a caching proxy in front, the first time a query runs, it goes to the database as usual and the result gets cached:&lt;/p&gt;

&lt;p&gt;The next time that same query runs, the proxy can skip the database entirely and answer from cache.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F96ekoq9atsa2b3nzq36r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F96ekoq9atsa2b3nzq36r.png" alt="Diagram: application querying through a caching proxy, served from cache on repeat" width="799" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This cuts down on database load: fewer round trips, less query execution, and lower CPU and I/O usage, especially for read-heavy workloads where the same queries run over and over.&lt;/p&gt;

&lt;p&gt;The tricky part is that caching SQL isn't as simple as storing a query string and its result. The proxy has to figure out when two queries are really asking for the same thing, know when cached data has gone stale and needs to be refreshed, and handle things like transactions, prepared statements, and session state correctly, all while staying invisible to the application.&lt;/p&gt;

&lt;p&gt;In short: the goal is to make the database faster without the application ever knowing a cache is there.&lt;/p&gt;

&lt;h2&gt;
  
  
  How is this different from Redis or application-level caching?
&lt;/h2&gt;

&lt;p&gt;The short version: Redis and application-level caching put the caching decisions in your code. A wire-compatible SQL caching proxy puts them in a separate layer, so your application doesn't have to think about caching at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Redis-style caching works.&lt;/strong&gt; Your application talks to two systems and has to coordinate between them:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkawanzrbdbyqgjajeerw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkawanzrbdbyqgjajeerw.png" alt="Diagram: application talking to both Redis and the database" width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your code has to spell out the caching logic itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user:123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM users WHERE id = 123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user:123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means your application now owns a lot of extra work: deciding on cache keys, converting objects to and from a storable format, setting expiration times, invalidating stale entries, handling cache misses, avoiding the "stampede" problem where many requests miss the cache at once and all hit the database together, and deciding upfront which queries are even worth caching. Redis is a general-purpose store, it doesn't know anything about SQL or your database schema. You decide what goes in it, which gives you a lot of control, but all of that logic lives in your application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How a wire-compatible SQL proxy works.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9km8rah2sbvlk7f7z52v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9km8rah2sbvlk7f7z52v.png" alt="Diagram: application connecting to the database through a SQL caching proxy in line" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your application just sends its normal SQL, exactly as before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5r2a3e4zy8ytyrq2i81o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5r2a3e4zy8ytyrq2i81o.png" alt="Diagram: the same SELECT query passing untouched through the proxy at every hop, application to proxy to database" width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The proxy recognizes the query, checks whether it already has a valid and fresh cached answer, and returns it if so, otherwise it goes to the database like normal and caches the result on the way back. Your application never sees a difference and never has to check whether a result came from cache or not.&lt;/p&gt;

&lt;p&gt;The core difference, in one line: Redis caches whatever your application explicitly tells it to; a SQL caching proxy caches query results automatically, with no caching API involved, the database connection itself just gets faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They're not competitors.&lt;/strong&gt; A SQL caching proxy doesn't replace Redis, they solve different problems and often work side by side. Redis is still the right fit for sessions, queues, rate limiting, and general application data. A SQL caching proxy takes over one specific job: making repeated database queries faster, without your code managing any of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fudls0g7r7beivakqzdfk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fudls0g7r7beivakqzdfk.png" alt="Diagram: Redis and a SQL caching proxy serving different jobs side by side" width="799" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How is this different from a plain database caching proxy?
&lt;/h2&gt;

&lt;p&gt;They're closely related, a wire-compatible SQL caching proxy is really just a stricter, more transparent version of a database caching proxy. The difference comes down to one thing: how convincingly it impersonates the real database.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Regular caching proxy&lt;/th&gt;
&lt;th&gt;Wire-compatible SQL caching proxy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protocol&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Its own separate API&lt;/td&gt;
&lt;td&gt;The database's native wire protocol&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Driver / client library&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Often a different one&lt;/td&gt;
&lt;td&gt;Same driver, unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Connection string&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Points at the caching API&lt;/td&gt;
&lt;td&gt;Same connection string as before&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code changes required&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Special calls (&lt;code&gt;cache.get(...)&lt;/code&gt;), rewritten queries&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Architecture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Application → Caching API → Database&lt;/td&gt;
&lt;td&gt;Application → Proxy → Database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;App's awareness of the cache&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Knows it's there, cache logic lives in the codebase&lt;/td&gt;
&lt;td&gt;None, has no idea it's not talking to the real database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What actually happens behind the scenes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reads the query, checks for a cached result, returns it if valid and fresh, otherwise queries the database and caches the result&lt;/td&gt;
&lt;td&gt;Same steps, but invisibly, the app just sees a normal query and a normal result&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5snii70mau1kzqf8dzjt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5snii70mau1kzqf8dzjt.png" alt="Diagram: side-by-side comparison of a regular caching proxy and a wire-compatible SQL caching proxy" width="798" height="199"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A closer look with MySQL
&lt;/h2&gt;

&lt;p&gt;MySQL is one of the databases this kind of proxy typically supports, alongside PostgreSQL. The idea works exactly the same way, the proxy just needs to speak MySQL's specific wire protocol.&lt;/p&gt;

&lt;p&gt;When a MySQL client connects, the server speaks first: it sends a handshake packet with a protocol version, server version string, and an authentication challenge (a random "salt"). The client responds with its username, a hashed password computed using that salt, its character set, and various capability flags. A wire-compatible proxy has to generate and validate this exact exchange, including MySQL's specific authentication plugins (&lt;code&gt;mysql_native_password&lt;/code&gt;, &lt;code&gt;caching_sha2_password&lt;/code&gt;, and others), so that any standard driver, mysql2 for Node, PyMySQL or mysqlclient for Python, Connector/J for Java, completes its handshake without ever suspecting it isn't talking to real MySQL.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F80kuw3xy0at5z5qoo2me.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F80kuw3xy0at5z5qoo2me.png" alt="Diagram: MySQL handshake and query exchange between application and a wire-compatible proxy" width="800" height="960"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MySQL clients also don't only send raw query text, the protocol defines command types like &lt;code&gt;COM_QUERY&lt;/code&gt; for plain queries and &lt;code&gt;COM_STMT_PREPARE&lt;/code&gt;/&lt;code&gt;COM_STMT_EXECUTE&lt;/code&gt; for prepared statements, which most ORMs and drivers use by default. A proxy that only understood &lt;code&gt;COM_QUERY&lt;/code&gt; would break the moment an application used prepared statements, so it needs to track prepared statement handles per connection and cache based on the query template plus its bound parameters, not just the raw bytes sent over the wire.&lt;/p&gt;

&lt;p&gt;Results come back as a sequence of packets too, column definitions, then row data, then a closing packet, and the proxy has to reconstruct all of this exactly as MySQL would, matching data types and encodings so the driver doesn't choke on it. On top of that, MySQL connections carry session state (the current database, session variables, isolation level, autocommit mode) and support transactions, so the proxy has to track that state and be careful not to serve cached data where it would break consistency, such as inside an open transaction.&lt;/p&gt;

&lt;p&gt;Because all of this is MySQL-specific plumbing, a proxy has to implement it separately from whatever it does for PostgreSQL, which has its own different wire protocol and message framing entirely. Products like ProxySQL, Vitess, and Readyset are examples of tools that do this kind of work for MySQL (some, including Readyset, support both MySQL and PostgreSQL by implementing both protocols side by side). Being "wire-compatible with MySQL" is a specific, real engineering claim: the proxy correctly implements MySQL's handshake, its authentication plugins, its command protocol, its result encoding, and enough of its session and transaction semantics that a MySQL driver, and the application built on top of it, genuinely cannot tell it isn't talking to MySQL itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes in your application code?
&lt;/h2&gt;

&lt;p&gt;This is where a wire-compatible proxy becomes particularly interesting. With Redis-style caching, adding a cache usually means rewriting the code that touches your data. Your simple query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM products WHERE category = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;laptops&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;turns into something with two paths to think about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;products:laptops&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM products WHERE category = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;laptops&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;products:laptops&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every developer touching this code has to understand both the cache-hit path and the cache-miss path.&lt;/p&gt;

&lt;p&gt;With a wire-compatible proxy, your code doesn't change at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM products WHERE category = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;laptops&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proxy silently takes on the hit-or-miss decision, unnoticed by anything above it, answering directly on a cache hit and asking the database then caching the result on a miss.&lt;/p&gt;

&lt;p&gt;Usually, the only real change is where your application points its connection. For a Postgres app, instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postgres://app:password@database:5432/shop

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it connects to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postgres://app:password@proxy:5432/shop

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same is true for MySQL. Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mysql://app:password@database:3306/shop

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the application connects to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mysql://app:password@proxy:3306/shop

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. If your app uses mysql2 in Node, PyMySQL in Python, or Connector/J in Java, none of that code changes, you're not swapping libraries or adding a cache client, just pointing the same driver at a different host and port. Your ORM keeps generating the same queries it always did, completely unaware that a proxy is now answering some of them itself.&lt;/p&gt;

&lt;p&gt;Same driver, same SQL, same ORM, same data access code; nothing in the application layer needs to change.&lt;/p&gt;

&lt;p&gt;That said, "no code changes" doesn't mean "no setup at all." Rolling this out in production still usually involves some work: pointing things at the right connection endpoint, adjusting infrastructure, deciding which queries should be cached and for how long, thinking through how the cache stays consistent with the database, and setting up monitoring around it.&lt;/p&gt;

&lt;p&gt;So the more accurate claim is: wire compatibility doesn't eliminate configuration, it eliminates the need to rewrite application code, because the proxy looks exactly like the database to everything talking to it. That's what "drop-in" should actually mean.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Readyset works as a wire-compatible proxy
&lt;/h2&gt;

&lt;p&gt;Readyset is a real-world example of everything above. It sits directly in the path between your application and your database, rather than off to the side the way Redis does.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8mjf3c81phqfb3f54un0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8mjf3c81phqfb3f54un0.png" alt="The application keeps sending normal SQL and getting normal results back, while Readyset separately watches the database's replication stream for data changes." width="800" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your app keeps sending normal SQL queries and getting normal query results back, it has no idea Readyset is even there. Meanwhile, Readyset watches the database's replication stream (its binlog, in MySQL's case) to learn about every write as it happens, so it always knows when its cached answers need updating.&lt;/p&gt;

&lt;p&gt;Reads and writes travel different paths. Writes always go straight to the real database, they have to land in the source of truth. Reads are where Readyset earns its keep: if the answer's already cached, it hands it back instantly, no round trip, no query execution, nothing asked of the database at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr4rdzemsdd14if96nvz2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr4rdzemsdd14if96nvz2.png" alt="Two common ways to wire it up: the application talking to Readyset directly, or a query router deciding where each query goes. A normal read replica can still exist alongside either setup." width="800" height="810"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This shows two common ways to set it up. On the left, your application talks to Readyset directly for anything that might be cached, and talks to the database directly for writes or queries Readyset hasn't cached. On the right, a query router sits in front of both and decides where each query should go, so the application doesn't even need to know Readyset exists as a separate destination. Either way, a normal read replica can still exist alongside Readyset, they're solving different problems: a read replica spreads out database load, while Readyset skips hitting the database at all for the queries it already knows the answer to.&lt;/p&gt;

&lt;p&gt;This is a step up from a typical read-replica setup. Many teams already scale reads with something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0y7uyanm2efcn0m96qwb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0y7uyanm2efcn0m96qwb.png" alt="A common existing pattern: HAProxy routes writes to a MySQL primary and reads to a read-only replica, kept in sync through logical replication." width="767" height="615"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this setup, HAProxy operates at Layer 4, it just forwards TCP connections to whichever backend a pre-configured port or pool points to, without ever reading what's inside them. It can split traffic toward a writable primary and a read-only replica, but only because the application (or its connection config) already decided which port to use; HAProxy itself has no idea whether a given connection is carrying a SELECT or an INSERT. And either way, every read that reaches the replica still has to run as a full query against a real database. Readyset replaces that pattern with something smarter: because it actually reads and understands the SQL passing through it, it can answer many reads without touching a database at all, not just route them to one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens inside Readyset.&lt;/strong&gt; This is the part that makes it more than a simple cache:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4128k3kuju0axm4hgfle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4128k3kuju0axm4hgfle.png" alt="Readyset builds a small internal pipeline for each cached query: base table nodes feed SQL operator nodes that mirror the query's joins, filters, and aggregations, landing in a reader node that holds the ready-to-serve result." width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Readyset builds a small internal pipeline for each query it's asked to cache. Changes to the underlying tables (the "base table nodes") flow in from the database, pass through a chain of "SQL operator nodes" that mirror the actual joins, filters, and aggregations your query performs, and land in a "reader node" that holds the ready-to-serve result. When a row in the database changes, only the affected part of this pipeline needs to redo its work, not the whole query from scratch. This pattern is known as incremental view maintenance, and it's why Readyset is better described as a live, self-updating computation rather than a store you fill and empty by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: caching, moved to the database wire
&lt;/h2&gt;

&lt;p&gt;The idea behind a wire-compatible SQL caching proxy comes down to one simple move: instead of every application building its own caching logic, you put a proxy between the app and the database that speaks the same protocol, understands the SQL flowing through it, and transparently answers what it can from cache.&lt;/p&gt;

&lt;p&gt;That one change gets you a few things at once. Your existing queries, drivers, and ORM code mostly stay untouched. Your app never has to check a cache or invalidate one; it just sends SQL like normal. Because the proxy actually understands queries and the data behind them, it can keep answers correct as things change, not just guess with a timer. And your database itself sees less repeated work, since the proxy is absorbing traffic it would otherwise have to redo.&lt;/p&gt;

&lt;p&gt;This is genuinely different from something like Redis. Redis gives you full control over what you cache, but you have to build and maintain that logic yourself. A wire-compatible proxy takes on that job itself, sitting closer to the database, so caching becomes something that just happens rather than something your team maintains.&lt;/p&gt;

&lt;p&gt;A plain TCP load balancer like HAProxy can't do this split on its own; it just forwards connections without ever reading what's inside them, so it has no way to tell a cached-eligible read apart from a write. The split only works because the proxy sitting in this position actually speaks SQL and reads every query:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzn2z26xqrrhqkgsoazc0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzn2z26xqrrhqkgsoazc0.png" alt="Diagram: a wire-compatible SQL caching proxy answering cached reads directly and passing writes and uncached reads through to MySQL" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The bigger point is simple: don't make every team solve database caching on their own. Put that intelligence in the connection layer once, in a proxy that actually understands SQL, not just one that forwards bytes, and let it quietly make things faster for every application behind it. As more systems lean on read-heavy, increasingly complex queries, that's a real advantage, not just quicker responses, but a way to speed things up without asking developers to rethink how their code talks to the database in the first place.&lt;/p&gt;

&lt;p&gt;Caching doesn't have to live in your application anymore. It can live in the connection itself.&lt;/p&gt;

&lt;p&gt;See deep caching and the command reference for how a cached result stays correct as your data changes. Run &lt;code&gt;rdst analyze&lt;/code&gt; to find out which of your own queries are candidates before you write a line of code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://readyset.io/downloads" rel="noopener noreferrer"&gt;Install RDST&lt;/a&gt;&lt;/p&gt;

</description>
      <category>databasecaching</category>
    </item>
    <item>
      <title>September Product Update 2026</title>
      <dc:creator>ReadySet</dc:creator>
      <pubDate>Thu, 01 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/readyset/september-product-update-2026-4j5m</link>
      <guid>https://dev.to/readyset/september-product-update-2026-4j5m</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhdu9r4vhthr2zfvheq5s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhdu9r4vhthr2zfvheq5s.png" alt="September Product Update 2026" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;September was a packed month at Readyset, especially for RDST, and we're excited to be sharing the full release update with you.&lt;/p&gt;

&lt;h2&gt;
  
  
  RDST Is Now Apache 2 Licensed and Includes Unlimited Free Inferencing
&lt;/h2&gt;

&lt;p&gt;We launched two big changes to RDST this month. First, it's now Apache 2 licensed. &lt;a href="https://github.com/readysettech/rdst" rel="noopener noreferrer"&gt;Use it, fork it, build on it, ship it inside your own tooling&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Second, hosted inference is on us for anyone signed in with a Readyset account. This means no API key required and no trial token limits. The default allowance covers hundreds of real questions a month per user, and if you need more, just reach out and we'll bump your allowance. Inference routes through OpenRouter across four US providers with automatic failover, so a single provider outage doesn't take RDST down with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://readyset.io/blog/whats-new-in-rdst-free-inference-and-an-apache-20-license" rel="noopener noreferrer"&gt;More details here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Single Readyset Node Now Serves 4.3M Queries/Second
&lt;/h2&gt;

&lt;p&gt;In our latest benchmark, Readyset reached 4.3 million queries per second on a single node, 6.4x the previous release's peak throughput on exactly the same hardware.&lt;/p&gt;

&lt;p&gt;The biggest improvement came from distributing connections across multiple Tokio runtimes, allowing Readyset to use CPU capacity that previously sat idle. Alongside a set of profiling-driven improvements that together cut CPU cost per query from 45.7μs to 21.7μs.&lt;/p&gt;

&lt;p&gt;Latency improved too. At 2,240 connections, throughput rose from 689,186 QPS to 4,111,367 QPS while p95 latency fell from 5.18ms to 0.75ms.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://readyset.io/case-studies/how-we-reached-43-million-qps-for-mysql-and-postgresql-workloads-on-a-single-readyset-node" rel="noopener noreferrer"&gt;Read the full benchmark here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Product Updates
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Passwords Redacted in Logs:&lt;/strong&gt; The adapter and server no longer write upstream, CDC, or client passwords to the log in clear text on startup. Passwords now appear as &lt;code&gt;&amp;lt;redacted&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transient Errors Now Signal Retry-Safe:&lt;/strong&gt; Readyset returns dialect-specific transient error codes on transient failures, giving clients and proxies a signal that the query is safe to retry automatically. Code &lt;code&gt;1317&lt;/code&gt; (query interrupted) for MySQL and &lt;code&gt;55000&lt;/code&gt; (database object not in prerequisite state) for PostgreSQL. Schema generation mismatch errors are reported as transient as well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correct Row Comparison Behavior in MySQL:&lt;/strong&gt; A batch of fixes to row comparison behavior under MySQL. Row comparisons that were losing their predicate (widening the query to the whole table) are fixed. Comparisons between a number and a numeric string now resolve correctly instead of as unequal. A &lt;code&gt;NULL&lt;/code&gt; inside a row now compares correctly instead of as an ordinary value. Prepared row comparisons are also no longer advertised to the client as taking no parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL Cache Correctness Fixes:&lt;/strong&gt; Named prepared statements filling a shallow cache no longer return binary-encoded values to clients that requested text format. &lt;code&gt;bytea&lt;/code&gt; values are now encoded and decoded in the correct format. Prepared statements planned for a shallow cache that was later dropped now proxy upstream until the cache is recreated, instead of returning an error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy-Paste-Ready EXPLAIN CACHES:&lt;/strong&gt; &lt;code&gt;EXPLAIN CACHES&lt;/code&gt; now prefixes each &lt;code&gt;CREATE SHALLOW CACHE&lt;/code&gt; statement with &lt;code&gt;USE &amp;lt;db&amp;gt;;&lt;/code&gt; for MySQL or &lt;code&gt;SET search_path = &amp;lt;schema&amp;gt;;&lt;/code&gt; for PostgreSQL, and renders query placeholders in valid positions. That makes the output copy-paste-ready to recreate shallow caches across multiple databases or schemas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shallow Caches Persist Across Restarts:&lt;/strong&gt; Shallow caches created via &lt;code&gt;--auto-cache&lt;/code&gt;, &lt;code&gt;CREATE CACHE&lt;/code&gt; hints, or &lt;code&gt;CREATE CACHE FROM &amp;lt;query id&amp;gt;&lt;/code&gt; (from &lt;code&gt;SHOW PROXIED QUERIES&lt;/code&gt;) are now correctly recreated when Readyset restarts, instead of being lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching Reliability Fixes:&lt;/strong&gt; A batch of caching reliability fixes. Sessions using different collations now get separate shallow-cache entries so sorting stays correct. The ACL probe no longer incorrectly disables shallow caches when Readyset's parameterization differs from valid placeholder positions on the upstream. &lt;code&gt;CREATE CACHE&lt;/code&gt; no longer crashes on queries with a very large &lt;code&gt;LIMIT&lt;/code&gt;, and a race where an adapter reconnecting during DDL could briefly use an outdated schema, causing &lt;code&gt;CREATE CACHE&lt;/code&gt; to fail, is fixed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus 9 additional fixes and improvements. Check out the full release &lt;a href="https://github.com/readysettech/readyset/releases/tag/stable-260924" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  New on the Blog &amp;amp; YouTube
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://readyset.io/blog/cache-invalidation-sql-queries" rel="noopener noreferrer"&gt;Cache Invalidation for SQL Queries: How to Stop Writing It Yourself&lt;/a&gt;: Traditional caching means deciding what to invalidate after every write. Miss a dependency, and your cache can quietly serve stale results. This benchmark compares six caching strategies against the same workload to show what that mistake costs, and how incremental view maintenance avoids the invalidation problem entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upcoming Events
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL Conference Europe 2026&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We are heading to Valencia on October 20-23 for the 2026 PostgreSQL Conference in Europe. If you'll be there and want to talk caching, query performance, or anything else Postgres-related, we'd love to connect. Coffee's on us. &lt;a href="https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ1IdojpwlPvtg_uhLImDN0lSbGN8jFc5AMaX3yheHv8K97JfDOZjVGEGJDeJ4wpqmEMGpglLde3" rel="noopener noreferrer"&gt;Book a time here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://2026.pgconf.eu/" rel="noopener noreferrer"&gt;Full event details here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Involved
&lt;/h2&gt;

&lt;p&gt;Questions, bugs, ideas, or just want to say hi? Find us on &lt;a href="https://go.readyset.io/community" rel="noopener noreferrer"&gt;Slack&lt;/a&gt;. Looking to contribute? Our &lt;a href="https://github.com/readysettech/readyset/issues?q=is%3Aissue+is%3Aopen+label%3Afirst-issue" rel="noopener noreferrer"&gt;newcomer issues&lt;/a&gt; are a good place to start.&lt;/p&gt;

</description>
      <category>productupdates</category>
    </item>
    <item>
      <title>What's New in RDST: Free Inference and an Apache 2.0 License</title>
      <dc:creator>ReadySet</dc:creator>
      <pubDate>Tue, 29 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/readyset/whats-new-in-rdst-free-inference-and-an-apache-20-license-8n7</link>
      <guid>https://dev.to/readyset/whats-new-in-rdst-free-inference-and-an-apache-20-license-8n7</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1x53s7vy7y6kp5lgzn33.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1x53s7vy7y6kp5lgzn33.png" alt="What's New in RDST: Free Inference and an Apache 2.0 License" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few months ago &lt;a href="https://readyset.io/blog/introducing-rdst-the-client-side-sql-performance-toolkit" rel="noopener noreferrer"&gt;we shipped RDST&lt;/a&gt;, our command-line tool for analyzing query performance on Postgres and MySQL databases. It ranks the queries running against your database by what they cost, and helps you understand and fix the ones slowing you down. It works with managed targets like RDS, Aurora, and Cloud SQL. More recently, we packed it into a &lt;a href="https://readyset.io/downloads" rel="noopener noreferrer"&gt;full desktop app&lt;/a&gt; available on Mac, Windows, and Linux and added natural-language querying, database-level health analysis, and index and rewrite suggestions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://readyset.io/downloads" rel="noopener noreferrer"&gt;RDST&lt;/a&gt; finds the queries costing you the most time on your own database, walks through why they're slow using real execution plans, and hands you specific index changes, query rewrites, or cache candidates that have been checked against your actual schema. When you'd rather just ask a question of your data than write SQL by hand, RDST does that too, and validates the SQL against your schema before it runs.&lt;/p&gt;

&lt;p&gt;We've now expanded RDST with two big upgrades.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free inference, no API key required
&lt;/h2&gt;

&lt;p&gt;Previously, running RDST meant bringing your own Anthropic key and paying for your own tokens. Now, if you sign in with a Readyset account, RDST runs on hosted inference we cover across every AI-powered part of the app.&lt;/p&gt;

&lt;p&gt;The default per-account allowance covers hundreds of real questions a month, which is enough for most day-to-day use. If you need more, email us and we'll raise your limit.&lt;/p&gt;

&lt;p&gt;Anyone running an LLM against their production database queries has a right to know exactly what leaves their machine. Transparency about this has been important to Readyset since day one. The &lt;a href="https://readyset.io/docs/readyset-ai/rdst/desktop/privacy" rel="noopener noreferrer"&gt;RDST privacy notice&lt;/a&gt; covers what gets sent to the model, what stays local, and what happens on our side of the hosted inference. If you find a scenario where our notice on privacy or LLM usage falls short, let us know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apache 2.0
&lt;/h2&gt;

&lt;p&gt;RDST was previously distributed under the Business Source License, which limited commercial redistribution. Under Apache 2.0, that limit is gone. Open source removes the licensing conversation from adoption, so teams evaluating RDST for internal tooling no longer need to route the decision through legal review. Fork it, ship it inside your own tooling, embed it in an internal developer platform, or vendor pieces of it into your own project. The source is on &lt;a href="https://github.com/readysettech/rdst" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and contributions are open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://readyset.io/downloads" rel="noopener noreferrer"&gt;Install RDST&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have questions or want to talk to other RDST users? Join us &lt;a href="https://go.readyset.io/community" rel="noopener noreferrer"&gt;on the Readyset community Slack&lt;/a&gt;. Full &lt;a href="https://readyset.io/docs/readyset-ai/rdst" rel="noopener noreferrer"&gt;RDST documentation&lt;/a&gt; is available on our docs site.&lt;/p&gt;

</description>
      <category>productupdates</category>
    </item>
    <item>
      <title>Cache Invalidation for SQL Queries: How to stop writing it yourself</title>
      <dc:creator>ReadySet</dc:creator>
      <pubDate>Mon, 21 Sep 2026 14:48:50 +0000</pubDate>
      <link>https://dev.to/readysettech/cache-invalidation-for-sql-queries-how-to-stop-writing-it-yourself-4ho2</link>
      <guid>https://dev.to/readysettech/cache-invalidation-for-sql-queries-how-to-stop-writing-it-yourself-4ho2</guid>
      <description>&lt;p&gt;Cache invalidation is the problem of knowing when a cached query result has gone stale and needs replacing. A write changes one row in the database, and every cached answer derived from that row is now wrong. The work is finding those answers and dropping or refreshing them before the next read.&lt;/p&gt;

&lt;p&gt;Say you have a read-heavy application and you've been asked to make reads faster. You put a cache in front of the database, the read path gets fast, and that invalidation work lands in your write path as a short list of rules. Here are the rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If a vote arrives, drop that story's key.&lt;/li&gt;
&lt;li&gt;If a title changes, drop that story's key.&lt;/li&gt;
&lt;li&gt;If an account is renamed, drop that user's key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One rule per table. It reviews cleanly and it ships.&lt;/p&gt;

&lt;p&gt;One of those three rules is wrong. The page it breaks keeps serving stale data until something unrelated evicts the entry.&lt;/p&gt;

&lt;p&gt;We wanted to know what that mistake actually costs, and how the alternatives compare when they see identical writes. So we built a lab and measured six read paths against one workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one of those rules is wrong
&lt;/h2&gt;

&lt;p&gt;Writing one of those rules wrong is easy. Here is the mistake in full, with the schema on the page. Three tables, quoted from Figure 2 of the &lt;a href="https://pdos.csail.mit.edu/papers/noria:osdi18.pdf" rel="noopener noreferrer"&gt;Noria paper&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;stories&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;votes&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;story_id&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;The lab adds a primary key to each of the three tables.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A story page reads across all three. It needs a title from &lt;code&gt;stories&lt;/code&gt;, a display name from &lt;code&gt;users&lt;/code&gt;, and a count from &lt;code&gt;votes&lt;/code&gt;, and all three collapse into a single cache key. So a cached result is not a copy of a row. It is an answer derived from rows in several tables, and no one of those tables can say which cached answers depend on it.&lt;/p&gt;

&lt;p&gt;The code that writes knows which row it changed. The cache knows which key it holds. The mapping between the two lives in exactly one place, the text of the read query, and the read query sits nowhere near the write path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stories&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The edge that matters is &lt;code&gt;u.id = s.author&lt;/code&gt;. It says that changing a user dirties story pages. Nothing in the &lt;code&gt;users&lt;/code&gt; table mentions a story, so the rename rule has no way to find the keys it just falsified. It drops a user key that this page never reads, and every story by that author keeps serving the old name.&lt;/p&gt;

&lt;p&gt;Nothing catches this. A cache entry that should have been dropped and was not raises no error. It returns a plausible name, on the instances that happen to hold it, only for entries populated before that particular write. It will not reproduce on a laptop where the cache is empty, which is why this class of bug tends to be found by a user rather than by a test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Almost nobody attempts precise invalidation
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://arxiv.org/abs/2311.02384" rel="noopener noreferrer"&gt;2023 survey by Ji, Zhou, Zhou and Wang&lt;/a&gt; analysed 20 Spring-cache web applications and surveyed more than 50 engineers. Of the cached methods they counted, 80.6% used a time to live or coarse-grained invalidation (dropping every cache entry associated with a table or namespace, rather than working out which specific entries a write affected), and only 10.4% attempted fine-grained invalidation.&lt;/p&gt;

&lt;p&gt;Four out of five cached results in real projects do not attempt the mapping. They accept staleness instead. The authors' explanation of why:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All the aforementioned cases converge on a single fundamental challenge: the absence of a link between database updates and cache entries at the application level, making precise cache invalidation difficult to achieve.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Their examples are ordinary. One delete method issues 11 SQL statements across 9 tables, so no application-level rule can say what it dirtied. Another caches a query with range predicates, where deciding whether an update changes the result means evaluating the predicate.&lt;/p&gt;

&lt;p&gt;Even teams who do this well find it hard. &lt;a href="https://engineering.fb.com/2022/06/08/core-infra/cache-made-consistent/" rel="noopener noreferrer"&gt;Meta's Polaris service&lt;/a&gt; exists to detect cache inconsistencies that reach production, and it moved TAO, the store behind Facebook's social graph, from 99.9999% to 99.99999999% consistency. Before Polaris, roughly one write in a million was silently wrong, and it took a purpose-built service to find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache invalidation strategies for SQL queries
&lt;/h2&gt;

&lt;p&gt;Four strategies are in common use:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time to live.&lt;/strong&gt; Do not track dependencies at all. Put an expiry on each cached result and re-read the query when it lapses. This is the most widely used strategy because it is the only one with no invalidation code, and the trade is that the cache is correct within N seconds, and you chose N. A TTL cache also never learns that anything changed, so it re-reads on a schedule whether the underlying rows moved or not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manual cache keys, the Redis model.&lt;/strong&gt; Store each result under a key the application chooses and delete those keys from the write path. Precise in principle. The cost is that the write path has to invert the read query by hand, and a missed edge is silent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event-driven invalidation.&lt;/strong&gt; Read the database change stream, the MySQL binlog or the Postgres write-ahead log, and invalidate from it. Moving the trigger out of the application removes the race between a write and its invalidation, which is a real gain. The mapping stays. The team still writes and maintains the code that turns a changed row into the cache keys it affects, now in a stream processor rather than in a write path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incremental view maintenance.&lt;/strong&gt; Apply each change to the stored result instead of discarding it. There is no invalidation step to write, because nothing is deleted.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Who writes the invalidation&lt;/th&gt;
&lt;th&gt;When the cached result is wrong&lt;/th&gt;
&lt;th&gt;Cost of a write&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to live&lt;/td&gt;
&lt;td&gt;Nobody&lt;/td&gt;
&lt;td&gt;Up to the length of the window, after every write&lt;/td&gt;
&lt;td&gt;None, but refreshes run on a clock whether or not anything changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual cache keys&lt;/td&gt;
&lt;td&gt;The application, per query&lt;/td&gt;
&lt;td&gt;Until eviction, whenever the mapping misses an edge&lt;/td&gt;
&lt;td&gt;The invalidation, plus a burst of misses behind it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event-driven invalidation&lt;/td&gt;
&lt;td&gt;The application, in a stream processor&lt;/td&gt;
&lt;td&gt;Until the mapping is corrected&lt;/td&gt;
&lt;td&gt;Stream processing, plus the burst of misses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental view maintenance&lt;/td&gt;
&lt;td&gt;Nobody&lt;/td&gt;
&lt;td&gt;Only while the change propagates&lt;/td&gt;
&lt;td&gt;Maintenance work inside the dataflow graph&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rest of this post measures the first, the second and the fourth against each other on one schema. One write workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we measured it
&lt;/h2&gt;

&lt;p&gt;The lab runs on the three tables declared above, &lt;code&gt;stories&lt;/code&gt;, &lt;code&gt;users&lt;/code&gt; and &lt;code&gt;votes&lt;/code&gt;, seeded at the scale the &lt;a href="https://pdos.csail.mit.edu/papers/noria:osdi18.pdf" rel="noopener noreferrer"&gt;Noria paper&lt;/a&gt; reports for production Lobsters: 10,000 users, 40,000 stories and 500,000 votes. Six read paths served the same story page under one write workload, and saw exactly the same writes. Readyset runs with its query sampler off throughout.&lt;/p&gt;

&lt;p&gt;The six paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MySQL directly, no cache, as a control&lt;/li&gt;
&lt;li&gt;Redis look-aside with the three-rule invalidator, whose rename rule drops a user key and leaves every story page by that author wrong&lt;/li&gt;
&lt;li&gt;Redis look-aside with a corrected invalidator&lt;/li&gt;
&lt;li&gt;Readyset shallow cache, fixed 10 s TTL with a 5 s refresh&lt;/li&gt;
&lt;li&gt;Readyset shallow cache with adaptive refresh&lt;/li&gt;
&lt;li&gt;Readyset deep cache, which maintains the result incrementally, a technique called incremental view maintenance and abbreviated IVM in the tables that follow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reads have to be continuous, or the number measures the cache miss instead of the cache: burst-reading a TTL cache reports 12 ms staleness where continuous reading of the same cache reports seconds. The write interval also has to be jittered. With a fixed interval, two runs of an identical configuration produced 4,783 ms and 177 ms, both arithmetically correct and both meaningless, because the answer was set by the phase relationship between the write clock and the cache's refresh clock.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the control is measuring
&lt;/h3&gt;

&lt;p&gt;The control path has no cache, so it cannot be stale. It is there to measure the instrument.&lt;/p&gt;

&lt;p&gt;The clock starts when the write commits and stops when a reader first sees the new value, so the control's number is whatever that detection costs. Readers issue one query and read back to back with no pause between reads, which puts the control at &lt;strong&gt;0.19 ms&lt;/strong&gt;, and each observation is bracketed to 0.28 ms by the read that caught it. Every number in this post is measured at that resolution.&lt;/p&gt;

&lt;p&gt;Two corrections: the readers used to sleep 2 ms between reads, so a write landed uniformly inside a detection window and half that window showed up as staleness. And each poll ran both cached queries while inspecting only the vote count, paying for a query it never used. Removing the sleep and the spare query moved the control from 2.00 ms to 0.19 ms.&lt;/p&gt;

&lt;p&gt;Staggering more reader threads, which looks like the obvious fix, does nothing. For N readers at interval T the aggregate grid is T/N and the read load is N/T. So grid spacing is one over the load, and four readers at 8 ms is the same grid as one reader at 2 ms. The lever is aggregate read rate. Taking the sleep out reaches a grid of one round trip with a single thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does the cache ever become correct?
&lt;/h2&gt;

&lt;p&gt;In ten trials we warmed every path, renamed an author, then read continuously for 45 seconds and recorded when each path first returned the new name.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Read path&lt;/th&gt;
&lt;th&gt;Converged&lt;/th&gt;
&lt;th&gt;Never converged&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;no cache (control)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;look-aside, three-rule invalidator&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10 of 10&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;look-aside, corrected invalidator&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readyset shallow, fixed refresh&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readyset shallow, adaptive refresh&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readyset deep (IVM)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ten out of ten. Every other path recovers. The three-rule invalidator never did, and in production the entry would stay wrong until unrelated memory pressure evicted it. That makes the bug's lifetime a property of the Redis eviction policy rather than of anything in the application.&lt;/p&gt;

&lt;p&gt;The fix is more expensive than it looks. The rename rule has to ask the database which stories that author wrote, because that is the only way to learn which keys were dirtied. That puts a &lt;code&gt;SELECT&lt;/code&gt; in the write path, with a fan-out equal to the number of stories the author has written, which is unbounded and largest for exactly the accounts that matter most. It also writes the join down a second time, so adding a column to the cached page means finding every write path that can touch it, again, with nothing to signal a miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  How stale each cache gets
&lt;/h2&gt;

&lt;p&gt;A vote arrives and the count changes. This is the write path the three-rule invalidator handled correctly, so it is the fairest comparison. Three runs of sixty writes, medians across runs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Read path&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;Excess over the uncached control, p50&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;no cache (control)&lt;/td&gt;
&lt;td&gt;0.19 ms&lt;/td&gt;
&lt;td&gt;0.40 ms&lt;/td&gt;
&lt;td&gt;n/a, this is the control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;look-aside, corrected invalidator&lt;/td&gt;
&lt;td&gt;1.11 ms&lt;/td&gt;
&lt;td&gt;1.57 ms&lt;/td&gt;
&lt;td&gt;0.92 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readyset shallow, fixed refresh&lt;/td&gt;
&lt;td&gt;2.68 s&lt;/td&gt;
&lt;td&gt;4.74 s&lt;/td&gt;
&lt;td&gt;2.68 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readyset shallow, adaptive refresh&lt;/td&gt;
&lt;td&gt;2.29 s&lt;/td&gt;
&lt;td&gt;4.28 s&lt;/td&gt;
&lt;td&gt;2.29 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readyset deep (IVM)&lt;/td&gt;
&lt;td&gt;1.66 ms&lt;/td&gt;
&lt;td&gt;2.17 ms&lt;/td&gt;
&lt;td&gt;1.48 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;60 writes per run, 3 runs, medians across runs. Readers poll back to back at about 3569 reads/sec, so the instrument brackets each observation to 0.28 ms.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5jfz9nl67n9cuzj1f9k2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5jfz9nl67n9cuzj1f9k2.png" alt="Staleness after a write, log scale: no cache 0.19 ms, look-aside 1.11 ms, TTL caches above 2.2 s, incremental view maintenance 1.66 ms" width="799" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The TTL caches sit between two and five seconds behind. That is what a TTL is. With a 5 s refresh and uniformly phased writes, half the reads land in the first half of a refresh period, so the theoretical p50 is 2.5 s. Measured 2.68 s. Adaptive refresh helped a little and it can't help much, because it moves each entry's period between 10% and 100% of the configured value, so a 5 s refresh has a floor of 500 ms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The look-aside cache is marginally the freshest of the three.&lt;/strong&gt; At 1.11 ms it is about half a millisecond ahead of incremental view maintenance at 1.66 ms. Two conditions are doing work behind that number: the query is cheap, and the write rate is one write every seven seconds. That ordering makes sense once you look at what each one does with a write. A look-aside cache deletes the entry and lets the next reader refetch, so what it serves next is whatever MySQL has at that instant. Incremental view maintenance has to carry the write through the binlog and the dataflow graph before the stored result changes, and on this schema that costs about 1.5 ms over an uncached read.&lt;/p&gt;

&lt;p&gt;So on the write path a hand-written invalidator gets right, it wins by half a millisecond. A harness that shows otherwise is probably measuring itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  The advantage lasts only while the query is cheap
&lt;/h3&gt;

&lt;p&gt;The story-page probe query is two indexed point lookups. When the look-aside cache throws its entry away, refetching costs about a millisecond, which is why deleting and refetching looks free. A query like that is rarely worth caching. The reason to cache is that recomputing is expensive.&lt;/p&gt;

&lt;p&gt;So the same measurement, on a query with no index to lean on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;votes&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;votes&lt;/code&gt; is indexed on &lt;code&gt;story_id&lt;/code&gt;, not on &lt;code&gt;user&lt;/code&gt;, so this is a full scan of about 500,000 rows and takes roughly 35 ms on this machine. Twenty-five writes, same method:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Read path&lt;/th&gt;
&lt;th&gt;Staleness p50&lt;/th&gt;
&lt;th&gt;Staleness p95&lt;/th&gt;
&lt;th&gt;One read, p50&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;no cache (control)&lt;/td&gt;
&lt;td&gt;59 ms&lt;/td&gt;
&lt;td&gt;102 ms&lt;/td&gt;
&lt;td&gt;36.62 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;look-aside, corrected invalidator&lt;/td&gt;
&lt;td&gt;42 ms&lt;/td&gt;
&lt;td&gt;51 ms&lt;/td&gt;
&lt;td&gt;0.16 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readyset deep (IVM)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.48 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.96 ms&lt;/td&gt;
&lt;td&gt;0.23 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ordering inverts, and the deep cache is now about 28 times fresher rather than half a millisecond behind.&lt;/p&gt;

&lt;p&gt;Read the last column next to the first. Both caches answer a single read in a fraction of a millisecond, so both are doing their job as caches. Their staleness differs by a factor of 28 because they recover from a write differently. A look-aside cache recovers by recomputing, so its staleness can never be lower than what the query costs. Incremental view maintenance recovers by applying one delta to the stored result, so its cost is set by the size of the change rather than the size of the scan, and it did not move between the cheap query and this one.&lt;/p&gt;

&lt;p&gt;The uncached control is the same story from the other side: at 59 ms it is not stale, it is just slow, and every one of those milliseconds is work the database repeats on every read.&lt;/p&gt;

&lt;h3&gt;
  
  
  The TTL cache, given its best shot
&lt;/h3&gt;

&lt;p&gt;Comparing against a fixed refresh period alone would be unfair, so the lab also runs Readyset's shallow cache with &lt;code&gt;ADAPTIVE&lt;/code&gt;. Each entry's refresh period then moves between 10% and 100% of the configured value. The cache hashes each refreshed result, shrinks the period when the value changed, and grows it when it did not.&lt;/p&gt;

&lt;p&gt;Adaptive refresh did engage: across the three runs the vote-count entry recorded 197 refreshes that found a change against 199 that did not, an almost exactly even split. Adaptation has nothing to bite on in that case, because the shrink and the grow are the same size and cancel, and the staleness reflects it: 2.29 s against 2.68 s, an improvement of about 15%.&lt;/p&gt;

&lt;p&gt;Plotted per write, the two time-based lanes wander over the same range. The maintained view sits three orders of magnitude below both, on every write rather than on average:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc0e11yzslw2rjhmni5mi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc0e11yzslw2rjhmni5mi.png" alt="Every write in one run of sixty, for the three Readyset lanes" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What an invalidation costs beyond the invalidation
&lt;/h2&gt;

&lt;p&gt;Deleting a hot key produces one cache miss per reader in flight, all at the same instant, all issuing the query to the database the cache was supposed to protect.&lt;/p&gt;

&lt;p&gt;We put 32 readers on one story and wrote to it once every ten seconds, counting with MySQL's own &lt;code&gt;Com_select&lt;/code&gt; so the harness could not inflate the number.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Read path&lt;/th&gt;
&lt;th&gt;SELECTs in the write second&lt;/th&gt;
&lt;th&gt;In a quiet second&lt;/th&gt;
&lt;th&gt;Increase&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;look-aside, corrected invalidator&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;33x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readyset shallow, fixed refresh&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readyset shallow, adaptive refresh&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readyset deep (IVM)&lt;/td&gt;
&lt;td&gt;no write-correlated increase&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;60 s per arm, one write every 10 s. Com_select is MySQL's own counter.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Sixty-six is 32 readers times the two queries a story page needs, plus the couple of background queries present in a quiet second. One vote produced thirty-three times the steady query rate, in one second, against the database the cache exists to shield.&lt;/p&gt;

&lt;p&gt;This is what the look-aside cache's half-millisecond freshness advantage costs. It is fresher because it throws the entry away and asks MySQL again, and asking MySQL again is the expensive part. Making its connection handling realistic, which is what closed the gap on the staleness table, did not soften the burst. It made the burst arrive sooner.&lt;/p&gt;

&lt;p&gt;Neither Readyset path shows a spike, for different reasons. The TTL cache never learns that the write happened, so nothing changes at write time. The deep cache learns immediately and updates the stored result in place, so there is no entry to refill and no reader to send upstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  The write rate is what decides this
&lt;/h2&gt;

&lt;p&gt;Everything measured so far ran at one write every seven seconds on the probe row, which is one invalidation per roughly thirty thousand reads. That is the regime where a look-aside cache is at its strongest, because a look-aside cache is only useful in the gap between one invalidation and the next.&lt;/p&gt;

&lt;p&gt;So the last measurement holds the readers constant and sweeps the write rate. Sixteen readers on one row, arms run one at a time so &lt;code&gt;Com_select&lt;/code&gt; attributes cleanly, and the look-aside arm keeps every advantage it had: the invalidation is still a function call in the writing thread, still one cache key, still no queue and no second cache to fan out to.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Writes/sec on the row&lt;/th&gt;
&lt;th&gt;Design&lt;/th&gt;
&lt;th&gt;Cache hit rate&lt;/th&gt;
&lt;th&gt;SELECTs/sec reaching MySQL&lt;/th&gt;
&lt;th&gt;Writes behind, p50&lt;/th&gt;
&lt;th&gt;Writes behind, p95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.1&lt;/td&gt;
&lt;td&gt;look-aside, corrected invalidator&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.1&lt;/td&gt;
&lt;td&gt;Readyset shallow, fixed refresh&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.1&lt;/td&gt;
&lt;td&gt;Readyset deep (IVM)&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;look-aside, corrected invalidator&lt;/td&gt;
&lt;td&gt;99.9%&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Readyset shallow, fixed refresh&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Readyset deep (IVM)&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;look-aside, corrected invalidator&lt;/td&gt;
&lt;td&gt;99.5%&lt;/td&gt;
&lt;td&gt;163&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Readyset shallow, fixed refresh&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Readyset deep (IVM)&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;look-aside, corrected invalidator&lt;/td&gt;
&lt;td&gt;97.9%&lt;/td&gt;
&lt;td&gt;641&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Readyset shallow, fixed refresh&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;94&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Readyset deep (IVM)&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;look-aside, corrected invalidator&lt;/td&gt;
&lt;td&gt;94.3%&lt;/td&gt;
&lt;td&gt;1600&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;Readyset shallow, fixed refresh&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;111&lt;/td&gt;
&lt;td&gt;235&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;Readyset deep (IVM)&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;30 s per point, arms run one at a time so Com_select attributes cleanly. The look-aside arm is deliberately at its best case: invalidation in the writing thread, one cache key, no queue and no fan-out.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsqoztv4dkow6no5yy5r2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsqoztv4dkow6no5yy5r2.png" alt="Two panels against write rate on a log axis. Left: SELECTs per second reaching MySQL, where the look-aside line climbs from 5 to 1,600 while the maintained view stays flat at 2. Right: writes behind, where the TTL line climbs to 111 while look-aside and the maintained view stay at zero" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The two panels are the two different bills a cache can be handed, and each design pays only one of them.&lt;/p&gt;

&lt;p&gt;The left panel is what the database pays: how many SELECTs per second reach MySQL while a cache is in front of it. The right panel is what the reader pays: how many writes behind the cached answer is when someone reads it. Both axes are log scaled, so a straight rising line is a proportional increase, and lower is better on both. A genuinely free design would be a flat line near the bottom of each panel.&lt;/p&gt;

&lt;p&gt;As the write rate climbs, only one of the three changes is about staleness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The look-aside cache stays current and sends the bill to the database.&lt;/strong&gt; Its upstream load goes from 5 SELECTs per second to 1,600, a 302-fold rise across the sweep, tracking write rate times reader count almost exactly: 50 writes times 16 readers times the two queries a rebuild needs is 1,600. It is still fresh at 50 writes per second. It is fresh because it is asking MySQL sixteen hundred times a second about a row whose answer changed fifty times.&lt;/p&gt;

&lt;p&gt;Its hit rate over that same sweep goes from 99.99% to 94.27%, and the two readings of that pair disagree completely:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc72jhu0wygvkaq5v5lz2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc72jhu0wygvkaq5v5lz2.png" alt="Two panels of the same look-aside data. Left: cache hit rate on a 90 to 100 percent axis, drifting from 99.99 percent down to 94.27 percent. Right: cache misses per second on a log axis, rising from 1.6 to 799" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the left is what a hit-rate dashboard shows: a 5.7 point drift, which on any monitoring page reads as a cache doing its job. On the right is the same 5.7 points expressed as the thing the database actually receives, and it is a rise from 1.6 misses per second to 799, a factor of five hundred. Each of those misses costs two upstream queries, which is exactly the 1,600 SELECTs per second in the table.&lt;/p&gt;

&lt;p&gt;Hit rate is a ratio, and a database is not charged in ratios. At 94% and sixteen thousand reads per second, the 6% is 799 requests that have to be computed from scratch, every second, on top of the write traffic that caused them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The TTL cache holds upstream load flat and pushes the cost onto the reader.&lt;/strong&gt; Two SELECTs per second at every write rate, because it never learns that anything changed. What moves instead is how far behind it is: 2 writes at 1 per second, 11 at 5, 45 at 20, and 111 at 50, with a p95 of 235. A five second refresh window times fifty writes a second is what that number has to be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The maintained view does not move on either axis.&lt;/strong&gt; 100% hit rate, zero writes behind, and none of the application's reads reaching MySQL at any write rate. The two SELECTs per second in that column are &lt;code&gt;SELECT VERSION()&lt;/code&gt; and a session variable read, issued by Readyset's own connection to the database. Neither touches a table. The write side is doing more work at 50 writes per second than at 0.1, but the work is one delta per write through a graph, and on this query it does not show up as either staleness or database load.&lt;/p&gt;

&lt;p&gt;A hand-written look-aside cache really is a shade fresher than incremental view maintenance, and it stays that way as the workload gets busier. It buys that by turning every write into a burst of reads against the database the cache exists to protect, and the bill grows with your write rate and your reader count at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What incremental view maintenance is
&lt;/h2&gt;

&lt;p&gt;Incremental view maintenance keeps the stored result of a query up to date by applying each change to that result, rather than recomputing the query or discarding the result. A write becomes a delta, the delta flows through the relational operators of the query, and the stored answer is rewritten in place. A &lt;code&gt;COUNT&lt;/code&gt; node receives "one more vote for story 1" and emits "the count for story 1 went from 7 to 8". There is no invalidation step, because nothing is deleted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc0pxjxtet9ehfo6ylq14.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc0pxjxtet9ehfo6ylq14.gif" alt="One write, handled first by a look-aside cache that deletes the key, then by incremental view maintenance that updates the stored result in place" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Incremental view maintenance has decades of literature behind it, and memory is what stopped it replacing caches. A maintained view is the whole answer set, so materializing every query an application runs costs more memory than the database. Streaming systems capped that by windowing, keeping only recent records, which is useless for an application that has to serve a four-year-old story as fast as this morning's.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 2018 result that changed the memory problem
&lt;/h3&gt;

&lt;p&gt;In October 2018 a group at MIT CSAIL published &lt;a href="https://pdos.csail.mit.edu/papers/noria:osdi18.pdf" rel="noopener noreferrer"&gt;Noria: dynamic, partially-stateful data-flow for high-performance web applications&lt;/a&gt; at the 13th &lt;a href="https://www.usenix.org/conference/osdi18/presentation/gjengset" rel="noopener noreferrer"&gt;USENIX Symposium on Operating Systems Design and Implementation&lt;/a&gt;, the peer-reviewed systems conference usually shortened to OSDI. Noria is a database backend that compiles an application's SQL queries into a dataflow graph and keeps every one of their results materialized and current as writes stream in.&lt;/p&gt;

&lt;p&gt;The paper builds on incremental view maintenance rather than inventing it. Its contribution is &lt;strong&gt;partially stateful dataflow&lt;/strong&gt;, the mechanism that makes maintaining every query affordable enough to replace a cache rather than sit beside one. Readyset is the production descendant of that work. The project's own history file records the lineage: the codebase "builds on the Noria project, an open-source research prototype built by a team of grad students, post-docs, professors, and research affiliates at the MIT Parallel and Distributed Operating Systems (PDOS) lab, of which ReadySet's founding team was a part."&lt;/p&gt;

&lt;p&gt;Partially stateful dataflow works like this. Operators are allowed to be incomplete: an operator holds only the entries that have actually been read, and treats a missing entry as a hole to fill on demand. A read for an absent key issues an &lt;em&gt;upquery&lt;/em&gt;, which travels backwards up the dataflow graph to the operators that can recompute that entry, and the response flows forward along the normal path to fill the hole. Under memory pressure an operator evicts an entry and sends an eviction notice downstream, so everything derived from it is dropped rather than left quietly out of date.&lt;/p&gt;

&lt;p&gt;Memory therefore tracks the working set instead of the table. In the paper's Lobsters measurements, partial state cut the state that could not be evicted to 73 MB, against 789 MB for full materialization over 137 MB of base tables.&lt;/p&gt;

&lt;p&gt;Our lab shows the same shape. Reading K distinct story pages and asking Readyset what it is holding, via &lt;code&gt;readyset_reader_state_size_bytes&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Distinct story keys read&lt;/th&gt;
&lt;th&gt;Deep cache state&lt;/th&gt;
&lt;th&gt;Bytes per key&lt;/th&gt;
&lt;th&gt;Share of base tables&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 KB&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;45 KB&lt;/td&gt;
&lt;td&gt;464&lt;/td&gt;
&lt;td&gt;0.22%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;453 KB&lt;/td&gt;
&lt;td&gt;464&lt;/td&gt;
&lt;td&gt;2.17%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;2.2 MB&lt;/td&gt;
&lt;td&gt;464&lt;/td&gt;
&lt;td&gt;10.84%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20,000&lt;/td&gt;
&lt;td&gt;8.9 MB&lt;/td&gt;
&lt;td&gt;464&lt;/td&gt;
&lt;td&gt;43.37%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40,000&lt;/td&gt;
&lt;td&gt;17.7 MB&lt;/td&gt;
&lt;td&gt;464&lt;/td&gt;
&lt;td&gt;86.74%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;40,000 stories exist. Base tables are 20.4 MB of data in MySQL. Metric: &lt;code&gt;readyset_reader_state_size_bytes&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F15tfd4cl7ob7tl1a5a25.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F15tfd4cl7ob7tl1a5a25.png" alt="Deep cache state against distinct keys read, log-log, a straight line at 464 bytes per key against a 20.4 MB base-table ceiling" width="799" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;464 bytes per key, flat across three orders of magnitude, and nothing held for keys nobody asked for. Read a hundred story pages and the caches hold 45 KB against 20.4 MB of base tables. Read all 40,000 and they hold 17.7 MB, still under the base tables, which is the case partial state does nothing for because there is nothing partial about it any more.&lt;/p&gt;

&lt;p&gt;The practical version: a deep cache is sized by the working set, the same rule already used for Redis.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to cache expensive SQL queries automatically
&lt;/h2&gt;

&lt;p&gt;A query is cached automatically when adding the cache needs no application change and keeping it correct needs none either. Readyset gives both. It speaks the MySQL and Postgres wire protocols, so the application connects with its existing driver, and a cache is one statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;CACHE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stories&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The caching is automatic because the invalidation is automatic.&lt;/strong&gt; Readyset keeps each cached result current with incremental view maintenance, driven by the MySQL binlog or the Postgres write-ahead log, so there is no invalidation step for the application to write. A caching layer that stores results and hands back an invalidation problem has automated the read path and left the write path alone. Teams comparing SQL query caching tools for MySQL usually weigh hit rates and latency, and the more useful question is what each tool requires in the write path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The property to design around
&lt;/h2&gt;

&lt;p&gt;Incremental view maintenance is eventually consistent. The &lt;a href="https://pdos.csail.mit.edu/papers/noria:osdi18.pdf" rel="noopener noreferrer"&gt;Noria paper&lt;/a&gt; states the guarantee this way: if writes quiesce, all external views eventually hold results that are the same as if the queries had been executed directly against the base tables. Under two milliseconds is short, and it is still not zero and not a transaction, so a read-your-own-writes path belongs on the database rather than behind any cache, this one included.&lt;/p&gt;

&lt;p&gt;Accepting that window is a design decision, and it is the same one already being made with a TTL, except that the window is milliseconds instead of seconds and it does not have to be chosen or tuned.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this comes down to
&lt;/h2&gt;

&lt;p&gt;The mapping from a write to the cached answers it falsified is a graph, and nothing in application code validates that graph. Four out of five projects in the survey reached the same conclusion and took the TTL, which trades correctness for a window.&lt;/p&gt;

&lt;p&gt;Partial state is what makes replacing invalidation practical. On our schema it bought this: on a cheap query, a maintained view sat 1.66 ms behind the database and a correctly written look-aside cache beat it, at 1.11 ms, while a TTL cache sat 2.68 s behind. On a query expensive enough to be worth caching, the same look-aside cache sat 42 ms behind and the maintained view did not move from 1.48 ms, because one design recovers by recomputing and the other by applying a delta.&lt;/p&gt;

&lt;p&gt;The look-aside cache also paid for its freshness on both queries with a burst of 66 queries into MySQL after every write. The maintained view produced no burst, needed no window chosen or tuned, and required no invalidation code, because in that design there is nothing to invalidate. The three-rule invalidator, meanwhile, is still serving the wrong author name.&lt;/p&gt;

&lt;p&gt;For how the dataflow engine maintains a cached result, see &lt;a href="https://readyset.io/docs/concepts/deep-caching" rel="noopener noreferrer"&gt;deep caching&lt;/a&gt; and &lt;a href="https://readyset.io/blog/behind-the-magic-how-readyset-speeds-up-queries-with-streaming-dataflow" rel="noopener noreferrer"&gt;How Readyset Speeds Up Queries with Streaming Dataflow&lt;/a&gt;. For the TTL model and when it fits better, see &lt;a href="https://readyset.io/docs/concepts/shallow-caching" rel="noopener noreferrer"&gt;shallow caching&lt;/a&gt;, and the &lt;a href="https://readyset.io/docs/cache/cache" rel="noopener noreferrer"&gt;CREATE CACHE reference&lt;/a&gt; for the statement itself.&lt;/p&gt;

</description>
      <category>database</category>
      <category>mysql</category>
      <category>caching</category>
      <category>performance</category>
    </item>
    <item>
      <title>How We Reached 4.3 Million QPS for MySQL and PostgreSQL workloads on a Single Readyset Node</title>
      <dc:creator>ReadySet</dc:creator>
      <pubDate>Thu, 10 Sep 2026 13:50:24 +0000</pubDate>
      <link>https://dev.to/readyset/how-we-reached-43-million-qps-for-mysql-and-postgresql-workloads-on-a-single-readyset-node-4gd1</link>
      <guid>https://dev.to/readyset/how-we-reached-43-million-qps-for-mysql-and-postgresql-workloads-on-a-single-readyset-node-4gd1</guid>
      <description>&lt;p&gt;A single Readyset node now serves 4.3 million queries per second, more than twice what we measured with PostgreSQL and almost three times what we measured with MySQL on the same server.&lt;/p&gt;

&lt;p&gt;We got there by profiling the serving path, finding the bottlenecks, and measuring again. The full post walks through exactly what changed: how we located the contention in the Tokio runtime, swapped SipHash for ahash on the hot paths, fixed several costs that had been hidden, and kept profiling until CPU cost per query dropped from 45.7µs to 21.7µs. At full throughput, most of the CPU now goes into the kernel network stack rather than the cache lookup, which points to where the next ceiling is.&lt;/p&gt;

&lt;p&gt;Compute keeps getting more expensive and AI workloads are competing for the same CPUs, memory, and networking your database depends on. Serving repeated reads from Readyset keeps that load off the database, so its CPU stays free for writes, cache misses, and the queries that actually need the execution engine, and you get more out of the hardware you already have.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://readyset.io/case-studies/how-we-reached-43-million-qps-for-mysql-and-postgresql-workloads-on-a-single-readyset-node" rel="noopener noreferrer"&gt;Read the full article here.&lt;/a&gt; &lt;/p&gt;

</description>
      <category>mysql</category>
      <category>postgres</category>
      <category>database</category>
      <category>performance</category>
    </item>
    <item>
      <title>August Product Update 2026</title>
      <dc:creator>ReadySet</dc:creator>
      <pubDate>Tue, 01 Sep 2026 16:22:51 +0000</pubDate>
      <link>https://dev.to/readyset/august-product-update-2026-3gi9</link>
      <guid>https://dev.to/readyset/august-product-update-2026-3gi9</guid>
      <description>&lt;p&gt;Our August product update is live. 🚀&lt;br&gt;
&amp;nbsp;&lt;br&gt;
We launched the Readyset Platform, a self-tuning query acceleration layer you can demo right in our rdst desktop app. Adaptive Cache shipped and is already dropping p95 staleness by 97% compared to a standard TTL cache. TopK hit GA. And we cut CPU load on shallow cache workloads by roughly 75%.&lt;br&gt;
&amp;nbsp;&lt;br&gt;
&lt;a href="https://readyset.io/blog/august-product-update-2026" rel="noopener noreferrer"&gt;There's a lot more in the full update, including a roundup of recent content and two upcoming events in September to mark on your calendar&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>database</category>
      <category>mysql</category>
      <category>postgres</category>
    </item>
  </channel>
</rss>
