MySQL's query cache existed for years and was removed in MySQL 8.0 — why was a feature meant to speed up reads considered harmful enough to remove entirely, and what should engineers use instead?
Quick Answer
MySQL's query cache stored the exact result set of a SELECT keyed on its literal SQL text, but it used a single global mutex, so every single write to any table invalidated every cached query touching that table and briefly serialized all query cache access cluster-wide — under real write-heavy concurrency, that mutex contention cost more than the cache ever saved, which is why it was deprecated in 5.7 and removed outright in 8.0. Modern MySQL relies instead on the InnoDB buffer pool caching underlying data and indexes, and application-level or proxy-level caching (Redis, ProxySQL query result caching) for repeated identical query results.
Detailed Answer
Imagine a single shared whiteboard in an office where anyone can write down the answer to a common question so others can read it instead of recalculating it themselves. This works fine if the office rarely changes any of the underlying facts. But if even one person updating any related fact anywhere in the building requires erasing the entire whiteboard and someone standing in line to physically access it one at a time to either read or erase it, then in a busy, constantly-changing office, people spend more time queued up at the whiteboard than they would have spent just recalculating the answer themselves.
MySQL's query cache stored the complete result set of a SELECT statement, keyed by the literal text of the query, so that an identical subsequent query could skip execution entirely and return the cached rows. This looked attractive for read-heavy, rarely-changing tables, and early MySQL deployments in the 2000s, often serving mostly-static content, saw real benefit. The design's fatal flaw was invalidation granularity: the query cache tracked dependencies at the table level, not the row or even the specific rows a query actually touched, so a single INSERT, UPDATE, or DELETE on a table invalidated every cached query that referenced that table anywhere in its query text, regardless of whether the write actually affected the rows those cached queries cared about.
Internally, both reading from and invalidating the query cache required acquiring a single global mutex (a lock that only one thread can hold at a time) protecting the entire cache structure, because the cache was a single shared, global resource rather than something sharded by table or connection. On a busy server with many concurrent connections doing a mix of reads and writes, every query — cached or not — had to briefly contend for this one global lock just to check or update cache state, meaning the query cache added serialization overhead to every single query on the server, not just the ones benefiting from a cache hit.
In production, this played out as query cache being genuinely useful on read-only or read-mostly workloads with low connection concurrency, and actively harmful — sometimes reducing throughput by more than half — on typical modern OLTP workloads with frequent writes and high concurrency, which describes most production web applications. MySQL's own documentation eventually recommended disabling it (query_cache_type=0) for exactly this reason well before it was formally removed, and the team ultimately removed it entirely in 8.0 rather than trying to fix the architecture, because a single global mutex is fundamentally incompatible with high-concurrency scaling regardless of how the invalidation logic is tuned.
The modern replacement isn't one single feature but a layered approach: the InnoDB buffer pool already caches actual data pages and indexes in memory (which is a much more granular, engine-level cache that doesn't suffer the same table-wide invalidation problem), and for caching complete query results, teams use an external cache like Redis or Memcached with explicit application-level cache keys and TTLs, or a proxy layer like ProxySQL that can cache specific query result sets with fine-grained rules. The gotcha for teams migrating off an old MySQL version that still relied on the query cache: removing it doesn't just require flipping a config flag, because any application that was silently depending on the query cache masking a genuinely slow, unindexed query will suddenly see that query's real latency for the first time in years, and teams have been caught off guard discovering years-old unindexed queries that 'always ran fine' purely because the query cache had been quietly absorbing the cost.