Inside Hyperdrive: How We Solved A Rare Hyperdrive MySQL Caching Bug
If you’ve been following the self-hosting scene for a while, you know that the dream is simple: run your own services, own your data, and keep latency low. Cloudflare’s Hyperdrive has been a game-changer for a lot of us in that regard. It pools database connections and caches query results at the edge, which means your self-hosted MySQL database sitting in a closet at home can suddenly feel like it’s running in a data center across the street.
But a few weeks ago, we hit a bug that nearly made us rip Hyperdrive out of our stack entirely. It was subtle, intermittent, and — as it turns out — rare enough that almost nobody was talking about it online. So we’re writing this up in case you ever run into the same wall.
Let’s walk through what happened, how we debugged it, and the fix that got everything running smoothly again.
The Setup: What We Were Running
Before we get into the bug, here’s the context. Our homelab stack looked like this:
- Database server: A Dell PowerEdge R720 running Debian 12, with MySQL 8.0.36
- App layer: A Node.js API (Express) deployed on Cloudflare Workers
- Connection layer: Cloudflare Hyperdrive, configured to connect to our home MySQL instance over a Cloudflare Tunnel
- Cache: Hyperdrive’s built-in query caching, enabled with a 60-second TTL
The app was a personal finance dashboard. It pulled transaction data, category breakdowns, and budget summaries from MySQL. Most queries were read-heavy aggregations — exactly the kind of workload Hyperdrive caching was designed for.
Everything worked great for about three weeks. Then the weird stuff started.
The Symptoms: Ghost Data And Phantom Updates
It began with a user report (okay, it was my spouse, but she counts). She logged a new transaction in the app, refreshed the dashboard, and… it wasn’t there. She refreshed again. Still nothing. Then, about two minutes later, it appeared.
At first, I assumed it was a frontend rendering issue. Maybe the API response was stale on the client side. But when I checked the API directly with curl, the response was missing the new transaction too.
Here’s what made this genuinely confusing:
- The transaction was in MySQL. I could see it with a direct
SELECTfrom the database server. - The Node.js API, when run locally against the database directly (bypassing Hyperdrive), returned the correct data immediately.
- The same API running on Workers through Hyperdrive returned stale data for roughly 60–120 seconds.
- It didn’t happen on every insert. Only about 30% of the time.
That last point is what made this a nightmare. It was intermittent.
Step 1: Ruling Out The Obvious
The first thing we did was check whether this was just normal cache behavior. Hyperdrive caches SELECT queries, and the default TTL is 60 seconds. If you write data and then immediately read it, you might get a cached result that’s up to 60 seconds old.
But we had already accounted for that. Our write-then-read flow looked like this:
| |
Hyperdrive is supposed to invalidate cached results when it detects a write operation (INSERT, UPDATE, DELETE) on the same connection. The cache key is based on the query text and parameters, and a write should bust any cached SELECT results that might be affected.
So in theory, step 2 should never return stale data after step 1. But it was.
Step 2: Enabling Hyperdrive Debug Logging
We turned on verbose logging in our Worker to see exactly what Hyperdrive was doing. Cloudflare Workers lets you add structured logs, so we instrumented the code:
| |
We also enabled Hyperdrive’s cache logging by setting the HYPERDRIVE_DEBUG environment variable in our wrangler.toml:
| |
After redeploying and reproducing the issue a few times, the logs revealed something interesting. On the requests where stale data was returned, the log showed:
| |
A cache hit with an age of 73 seconds — right after an INSERT. The cache invalidation wasn’t firing.
Step 3: Digging Into Cache Invalidation
This is where we had to understand how Hyperdrive’s cache invalidation actually works under the hood.
Hyperdrive tracks write operations per connection session. When you use withSession() on your database binding, Hyperdrive creates a session that tracks all reads and writes together. If a write happens in the same session as a subsequent read, Hyperdrive knows to skip the cache for that read.
But here’s the catch: session tracking is per-isolate. Cloudflare Workers run on V8 isolates, and each isolate has its own session state. If your write and read happen on the same request (and therefore the same isolate), invalidation works. But if they happen on different requests that land on different isolates, the session context doesn’t carry over.
In our case, the intermittent nature of the bug was because of load balancing across isolates. About 70% of the time, the insert and the subsequent dashboard refresh landed on the same isolate, and invalidation worked. The other 30% of the time, the refresh landed on a different isolate that still had the old cached result.
This wasn’t a bug in our code. It was a limitation of how Hyperdrive’s session-based cache invalidation interacts with Cloudflare’s distributed isolate model.
Step 4: The Fix
We explored several approaches. Here’s what we tried and what actually worked.
Attempt 1: Disable Caching Entirely
We set maxAge: 0 in our Hyperdrive config:
| |
This fixed the stale data problem immediately. Every query went straight to MySQL. But it also killed the performance benefit. Our dashboard latency went from ~40ms to ~180ms because every request had to traverse the Cloudflare Tunnel to our home server.
Not ideal.
Attempt 2: Manual Cache Busting With Query Parameters
Hyperdrive’s cache key includes the query text and parameters. If you change the query text even slightly, it’s a cache miss. So we tried appending a timestamp parameter to force a cache miss after writes:
| |
This worked, but it felt hacky. It also meant we were busting the cache on every read-after-write, even when the write didn’t affect the data being read. And the comment-based approach felt fragile — what if Hyperdrive changed how it normalized query text?
Attempt 3: Use withSession() Correctly (The Real Fix)
After more digging through Cloudflare’s documentation and some GitHub issues, we realized we weren’t using session mode correctly across our full request lifecycle.
The key insight: you need to use withSession() for the entire request flow, not just individual queries. And you need to pass the session through consistently.
Here’s the corrected pattern:
| |
But this still didn’t fully solve the cross-isolate problem. If the POST and the GET happened on different isolates (which they usually do, since they’re separate HTTP requests), the GET could still hit a stale cache.
The Final Solution: Targeted Cache Invalidation With cacheTtl
The real fix was a combination of two things:
- Use
withSession()for write-then-read flows within a single request (as shown above). - For cross-request reads after writes, use a shorter, smarter
cacheTtland accept brief staleness for non-critical reads.
We restructured our caching strategy:
| |
By setting cacheTtl: 0 on reads that happen immediately after writes, we guaranteed fresh data. For general dashboard browsing, a 15-second cache TTL was acceptable — if someone’s just poking around, a 15-second delay in seeing a new transaction is fine.
We also added a client-side cache-busting mechanism for the critical flow:
| |
This ensured the browser never served a cached HTTP response for the immediate post-write refresh.
Step 5: Verifying The Fix
After deploying the changes, we ran a test script that simulated the write-then-read flow 100 times:
| |
Results: 0 failures out of 100. The fix held.
Lessons Learned
This bug taught us several things about Hyperdrive and caching in general:
- Cache invalidation is hard. This is a well-known computer science problem, and Hyperdrive doesn’t magically solve it. It gives you tools (
withSession(),cacheTtl), but you have to use them correctly. - Intermittent bugs are usually concurrency or distribution issues. The fact that it only happened 30% of the time was the clue that pointed to isolate-level load balancing.
- Read the docs carefully. The
withSession()behavior is documented, but the implications for cross-isolate requests aren’t obvious until you hit them. - Test your cache behavior explicitly. Don’t assume that “it worked in dev” means caching will behave correctly in production with real traffic patterns.
Summary
The rare Hyperdrive MySQL caching bug we encountered was caused by session-based cache invalidation not carrying across Cloudflare Workers isolates. When a write and a subsequent read landed on different isolates, the read could return stale cached data.
The fix was a combination of:
- Using
withSession()consistently for write-then-read flows within a single request - Setting
cacheTtl: 0for critical reads that must reflect recent writes - Using shorter cache TTLs (15 seconds) for non-critical dashboard reads
- Adding client-side cache-busting for immediate post-write refreshes
If you’re using Hyperdrive with MySQL and seeing intermittent stale data after writes, check your session usage and cache TTL configuration. It’s probably not a bug in your database — it’s the cache layer doing exactly what it was configured to do, even if that’s not what you wanted.
Have you run into caching weirdness with Hyperdrive or any other database proxy? We’d love to hear about it. Drop a comment on the video, join our Discord, or hit us up on Mastodon. And if this article saved you a few hours of debugging, consider subscribing to the channel — we’re always digging into the weird corners of self-hosting so you don’t have to.
Until next time, keep your containers running and your caches fresh.