§ 037 · MySQL

Using PHP and Readyset for the First Time with MySQL

Speed up your PHP application without changing a single query.

A few days ago I set out to answer a simple question: how much faster can a PHP app go if you drop a SQL cache in front of MySQL — without touching application code? The answer is below. On a four-table join aggregating revenue by category and region, Readyset served the same result 60x faster than MySQL. And all I did was change a port number.

This post walks through exactly what I did, step by step, so you can reproduce it yourself.

What Is Readyset?

Readyset is a SQL-aware caching engine that speaks the native MySQL wire protocol. Your PHP application connects to it the same way it connects to MySQL — same driver, same queries, same credentials. Underneath, Readyset proxies queries to your upstream database and, for the ones you choose to cache, serves results straight from memory.

Unlike Redis or Memcached, you don’t serialize objects or manage keys. You just tell Readyset “cache this query” and it handles the rest. It supports two caching modes:

  • Shallow caching — stores results with a TTL, works with any SELECT, refreshes automatically in the background. This is what we’ll use here.
  • Deep caching — maintains materialized views updated incrementally via the MySQL binlog. Always-fresh, but limited to supported query patterns.

Shallow caching is the fastest way to get going. Let’s start.

Prerequisites

  • PHP 8.1+ with the pdo_mysql extension
  • Docker and Docker Compose

Tested with PHP 8.1.34, 8.2.30, 8.3.30, and 8.4.19. All versions produced consistent results.

Step 1: Start Readyset with Docker

Create a docker-compose.yml:

Bring it up:

Give Readyset a moment to connect to MySQL and initialize. You can check when it’s ready:

Expected output:

Step 2: Connect Your PHP Application

The only change: point your DSN at port 3307 instead of 3306.

Set PDO::ATTR_EMULATE_PREPARES to false so PDO uses the binary protocol for prepared statements — Readyset handles these natively.

One thing to know: Readyset admin commands (SHOW PROXIED QUERIESSHOW CACHESCREATE CACHE) need the text protocol. If you want to run those from PHP, use a separate connection with emulated prepares enabled:

Step 3: Set Up Some Test Data

I wanted something realistic enough to show a meaningful difference — not just a single-table lookup. So I created a small e-commerce schema: customers, products, orders, and order items.

Then I seeded it: 500 customers across 5 regions, 200 products in 5 categories, and 2,000 orders each with 1–5 line items. Nothing huge, but enough to make a join query actually work for its result.

Step 4: The Slow Query

Here’s the kind of query you’d find behind a dashboard — revenue broken down by product category and customer region, filtered by order status:

Four tables, a GROUP BY, a DISTINCT count, and a SUM. Against MySQL directly, this ran in about 4ms on average. Not terrible, but if you’re calling it hundreds of times a second on a dashboard or an API, it adds up.

Step 5: Cache It

First, run the query through Readyset so it can observe the shape. Then tell it to cache:

Expected output:

Verify:

Expected output:

The properties column tells you exactly how this cache behaves. The ttl 10000 ms is the default time-to-live — cached results are considered stale after 10 seconds. The refresh 5000 ms means Readyset starts refreshing the cache in the background 5 seconds before it expires, so your users almost never hit a cold cache. The coalesce 5000 ms groups concurrent cache misses together to avoid hammering the upstream database. These are all Readyset defaults (configurable via DEFAULT_TTL_MSDEFAULT_COALESCE_MS environment variables), independent of the QUERY_CACHING setting — which only controls whether caches are created explicitly (you run CREATE CACHE) or implicitly (Readyset caches queries automatically).

Or do it from PHP:

Step 6: The Benchmark

I ran the same query 500 times against MySQL directly, then 500 times through Readyset cache. Here’s what I got on PHP 8.4.19:

The results were consistent across every PHP version I tested:

PHP Version MySQL direct Readyset cached Speedup
8.1.34 3.996 ms 0.067 ms 59.7x
8.2.30 4.008 ms 0.051 ms 78.0x
8.3.30 4.173 ms 0.067 ms 62.0x
8.4.19 4.125 ms 0.065 ms 63.3x

That’s a four-table join with aggregation going from ~4ms to ~0.06ms. For a dashboard hitting this query on every page load, that’s the difference between your database sweating and barely noticing.

Here’s what the query actually returns:

Here’s the complete script that sets up the data, creates the cache, and runs the benchmark. Save the code below as benchmark.php and run it with php benchmark.php. Make sure the file starts with a tag (some browsers strip it when you copy from a web page):

What About Writes?

Writes pass straight through Readyset to MySQL. No configuration needed — INSERTs, UPDATEs, and DELETEs go upstream automatically. The shallow cache refreshes on its TTL cycle, so new data shows up within seconds:

I verified this in every test run — after the TTL window, New Widget consistently appeared in the cached results with no manual invalidation.

If you need writes to appear instantly in cached results, switch to deep caching (CACHE_MODE=deep), which uses the MySQL binlog to update caches incrementally. The trade-off is that deep caching only supports a subset of query patterns, whereas shallow caching works with any SELECT. However, the DEEP caching is a topic for another blog post.

Using Readyset with Laravel

Update your .env and you’re done:

Eloquent, the query builder, raw queries — they all work through Readyset without changes. Cache the expensive ones through SHOW PROXIED QUERIES and move on.

A Few Things I Learned Along the Way

Start with the expensive queries. The simple single-table lookup (SELECT name, price FROM products WHERE category = ?) only showed a 1.5–2x improvement because MySQL already handles it quickly (~0.1ms). The big wins come from joins, aggregations, and anything that makes the database do real work.

Readyset exposes Prometheus metrics. Hit http://localhost:6034/metrics to get cache hit rates, latency distributions, and replication lag. Wire that into Grafana and you have full observability.

Shallow caching works with everything. I didn’t hit a single query that shallow caching couldn’t handle. Deep caching has restrictions around certain functions and subqueries, but shallow just works.

Readyset is source-available under the Business Source License 1.1 (BSL 1.1) — you can use it freely in production at no cost. The code is available at github.com/readysettech/readyset.

We’d love to see what you build with Readyset. If you run a proof of concept or benchmark with your own workload, share your results with us — real-world feedback is how we make this better. Reach out at readyset.io or open a discussion on GitHub.

Written by

Vinicius Grippa

Writes this blog. Mostly about databases. Boring on purpose.

More about me →

The floor is yours.

0 comments · Moderated · civil & on-topic

First comment appears here once approved. Questions, corrections, and counterpoints welcome — just no self-promotion.

Add a comment

Your email address is never published. * required

Subscribe · Posted when ready

A quiet, technical email about databases.

One post per send, corrections when I’m wrong, nothing else. No social-media cross-posts. No “what we learned.”

Unsubscribe with any reply