A read replica scales reads by keeping a full copy of your MySQL or Postgres database and executing every query against it. Readyset scales reads by caching query results and keeping them fresh from the same replication stream, so a repeated query is looked up instead of executed. Replicas add capacity. Readyset removes work.
A read replica is a second database server that holds a full copy of the primary and answers reads. The primary records every change in its replication log, the binary log (binlog) in MySQL and the write-ahead log (WAL) in Postgres, and the read replica replays that log so its tables follow the primary's.
A read replica executes every query it receives. Prepared statements can skip parsing, and Postgres can reuse a cached plan, but the execution itself (the index lookups, joins, sorts and aggregation) runs again against the replica's own tables and buffer pool, even when the identical query finished a millisecond earlier. A replica adds a server's worth of capacity for that work without shortening it, so an aggregate that is slow on the primary still pays the same execution cost on a replica of the same size.
A read replica also repeats every write. A MySQL replica applies the same row changes the primary made, secondary indexes included, and a Postgres standby replays the WAL for every changed table and index page, so either one needs enough I/O to keep pace with the primary's write rate before it serves a single read.
A read replica needs storage for the full dataset, and usually as much memory as the primary, because it serves the same working set. Infrastructure cost grows linearly with every replica added, and we lay out that arithmetic in why query caching is the most cost-effective way to scale databases.
Readyset is a wire-compatible SQL caching proxy for PostgreSQL and MySQL. The application keeps its driver and its SQL, and the only required change is the connection string, which points at Readyset instead of the database. Readyset answers the queries that have a cache and proxies everything else to the database, including every write and every locking read such as SELECT ... FOR UPDATE.
Caching in Readyset is chosen per query: CREATE DEEP CACHE FROM followed by the query creates a push-based cache for it, and CREATE SHALLOW CACHE FROM creates a pull-based one. Readyset turns the literal values in a query into parameters, so customer_id = 7 and customer_id = 9 share one cache and become two keys in it.
A Readyset push-based cache stays fresh from the same change log a read replica uses. Readyset first snapshots the upstream tables, full rows, into RocksDB on its own local disk. It then follows the MySQL binlog in row format, or a logical replication slot it creates on the Postgres database.
Push-based caching needs Readyset started with CACHE_MODE=deep or deep-then-shallow, because the default mode, shallow, runs no replication at all.
Readyset keeps each push-based cache current with incremental view maintenance: when a row changes upstream, Readyset applies only the resulting delta to the cached results that row affects, instead of re-running the query. Readyset's dataflow engine derives from Noria, a research project at MIT's Computer Science and Artificial Intelligence Laboratory (CSAIL), published at the 2018 USENIX Symposium on Operating Systems Design and Implementation (OSDI).
Readyset uses partial materialization, so a push-based cache holds results only for the parameter values the application has asked for. The first read of a new value is a miss: Readyset computes that one key from its own copy of the tables, in what the documentation calls an upquery, then stores and returns it.
Later reads of that key are in-memory lookups in Readyset, typically under a millisecond, served by lock-free readers that never wait on replication. A change to a customer nobody has read updates Readyset's copy of the table and stops at the first step of the graph that holds no state for that customer.
Readyset caps its memory with --memory-limit. When memory reaches that limit, Readyset evicts the least recently used keys, and an evicted key is computed again on its next read.

The same query, four times. The read replica executes all four. Readyset computes customer 7 and customer 9 once each and answers the two repeats from memory.
Readyset also offers pull-based caches. A pull-based cache stores a query's result for a time-to-live (TTL) and refreshes it from the database in the background, for example CREATE SHALLOW CACHE WITH (POLICY TTL 60 SECONDS REFRESH 10 SECONDS) FROM followed by the query. With REFRESH, a read that finds an entry older than 10 seconds gets the cached result and starts a refresh; REFRESH EVERY refreshes on a fixed schedule instead.
Pull-based caches need no replication stream and accept almost any read query. Their freshness comes from the refresh settings and the TTL rather than from writes: an entry read continuously stays roughly one refresh interval behind, and an entry nobody reads for a full TTL is dropped.
Where a plain TTL cache refreshes every key on one schedule, Readyset can adapt the refresh interval to each result. With the ADAPTIVE option, Readyset shortens the interval for a result that changed at its last refresh and lengthens it for one that didn't, between 10% and 100% of the configured interval, and caps the extra load those refreshes send to the database. Caches that Readyset creates automatically are always adaptive.
In our benchmark, adaptive refresh cut p95 staleness by 86% compared with a fixed refresh schedule.
A read replica stores and queries a full copy of the data, while Readyset serves precomputed query results from memory.
| Read replica | Readyset |
|---|
| What gets copied | The whole database, from the primary's binlog (MySQL) or WAL (Postgres) | Push-based caches: the replicated tables on local disk, from the same binlog or WAL, plus cached results in memory. Pull-based caches copy no tables |
| What runs per read | A full execution against the base tables, plus parse and plan unless a prepared statement reuses them | Push-based caches: a key lookup, or a computation from Readyset's own copy on a miss. Pull-based caches: a lookup, with misses and refreshes run on the database. Uncached queries go to the primary |
| What runs per write | The same row change (MySQL) or WAL page changes (Postgres), indexes included | The change as a delta through each push-based cache that reads the table |
| How freshness is maintained | Asynchronous replay of the binlog or WAL | Push-based caches: incremental view maintenance from the binlog or WAL. Pull-based caches: background refresh from the database |
| Application changes | Reads routed to the replica endpoint, writes to the primary | Connection string pointed at Readyset; you choose which queries to cache |
| How cost scales | Each replica adds a full copy's storage, memory and compute | Memory follows the cached results and the operator state behind them, with eviction at a configured limit |
| Failover | A replica can be promoted to primary | None: the primary and its replicas remain the availability layer |
| SQL coverage | Every read query the engine supports | Push-based caches: a defined subset of SELECT. Pull-based caches: almost any read. Anything else is proxied |
| Consistency | Asynchronous lag, with the primary's engine semantics | Push-based caches: eventually consistent. Pull-based caches: as stale as the refresh interval and TTL allow |
For read scaling, Readyset can replace read replicas for the queries it caches, because Readyset answers a cached query from memory and that read never reaches the primary or any read replica.
Readyset does not replace the other jobs of a read replica. Readyset proxies uncached and unsupported queries to the primary and has no failover role, so the primary still needs read replicas or another high-availability mechanism.
Readyset's two cache types replace different things. Readyset push-based caching, which maintains results from the binlog or WAL, is the alternative to adding read replicas. Readyset pull-based caching is the alternative to an application-managed key-value cache such as Redis, without the invalidation code that goes with it.
Teams use Readyset to delay adding read replicas or to run a smaller database tier, while keeping the replicas that availability requires. The cost argument is in The Real Cost of Read Replicas and why query caching is the most cost-effective way to scale databases.
A read replica is the better choice when the requirement is availability, complete SQL coverage, tighter read consistency, or reads from another region.
Availability. A read replica can be promoted to primary when the primary fails, and Readyset cannot. After a failover, Readyset is repointed to the new primary: on MySQL with GTIDs it resumes from the last transaction it applied, without a new snapshot, and on Postgres it creates a new replication slot and takes a full snapshot again.
SQL coverage. A replica runs any query the engine supports. Readyset push-based caches support a defined subset of SELECT: a query that combines results with UNION, INTERSECT or EXCEPT, or walks a hierarchy with a recursive common table expression (CTE), cannot have a push-based cache on MySQL or Postgres. A pull-based cache accepts those queries, and anything without a cache is proxied to the primary. The query support page lists each shape for both engines and both cache types.
Consistency. A read replica can tighten read-after-write behavior at the cost of write latency, and the two engines do it differently. Postgres can make a commit wait until a synchronous standby has applied it, with synchronous_commit = remote_apply, so a read that starts on that standby afterwards sees the write. MySQL semi-synchronous replication waits only until a replica has received the transaction, so it protects durability without making the replica's reads current.
Location and reporting. A read replica can sit in another region near users, and suits long-running reporting and extract, transform, load (ETL) queries that need no cache.
Workload shape. Readyset saves little on queries that rarely repeat with the same parameters, or on primary-key lookups the database already answers in about a millisecond. Our GitLab field test on Azure Postgres, which used pull-based caches, showed the same pattern: its heaviest queries ran 5.7x to 26.3x faster at the median, while per-query latency on primary-key lookups came out roughly even.
Read replicas and Readyset both follow the primary asynchronously, so either can return data slightly older than the primary.
Under heavy write load, a read replica's lag can grow from milliseconds to seconds, and every read from that replica sees it.
A Readyset push-based cache is eventually consistent. Its lag is the time for a change to arrive through the binlog or WAL and for its delta to reach the reader, and it applies only to cached queries, because Readyset proxies uncached queries to the primary.

One INSERT reaching both. The replica writes the row and recomputes on the next read; Readyset updates the one stored count the row affects.
Readyset provides two controls for reading your own writes (RYW). Inside a transaction, each cache has a transaction policy: by default Readyset proxies the cached query to the primary for the whole transaction, UNTIL WRITE serves the cache until the transaction writes, and ALWAYS always serves the cache. Outside a transaction, an opportunistic window, off by default, sends a session's reads to the primary for a set number of milliseconds after the session writes. The window narrows the race and is not a guarantee.
Yes. Readyset and read replicas run together because they do different jobs.
The application sends the queries it wants cached through Readyset, and Readyset connects to the primary. Readyset answers cached queries from memory and proxies every other query to the primary, while read replicas keep the roles Readyset does not fill: failover, reporting and analytics, and reads from another region.
By default, Readyset reads the binlog in MySQL or the WAL in Postgres from the primary. On MySQL, Readyset can instead follow a read replica that writes its own binlog (log_replica_updates=ON), which adds that replica's lag to Readyset's. On Postgres, Readyset follows the primary, where it creates its publication and replication slot. When a read replica is promoted after a primary failure, Readyset is repointed to the new primary.

Readyset handles the repeated reads and the replicas handle failover and reports. After a failover, Readyset follows the promoted replica.
Readyset shows which of your queries it can cache before you cache anything. Point a staging copy of the application at Readyset, run the usual traffic, and SHOW PROXIED QUERIES lists every query Readyset has passed through, with a readyset_supported column saying whether each one can be cached.
SHOW PROXIED DEEP QUERIES and SHOW PROXIED SHALLOW QUERIES show the same list for push-based and for pull-based caches, so you can see which queries each cache type would take before creating any.
We see Readyset and read replicas as tools for different parts of read scaling, and what decides between them is how often your reads repeat. Our recommendation is to keep the replicas your failover plan needs, then look at your read traffic before adding more. Put it through Readyset, cache the expensive queries that repeat most, and leave the rest to the replicas: one-off reports, SQL that Readyset doesn't cache, and reads that must see the latest write.
To go through your own PostgreSQL or MySQL workload with the Readyset team, book a Readyset demo