There are excellent database proxies out there. pgpool-II, MaxScale, and ProxySQL are battle-tested, feature-rich, and used in production by thousands of companies. I’ve built TQDBProxy as a learning project to explore a different approach: what if caching decisions lived in the application code itself?
Custom client libraries for PHP, TypeScript, and Go allow you to (optionally) specify a cache time-to-live (TTL) as an extra argument to a query method. Queries with a TTL are cached for the specified duration and are served stale while revalidating in the background.
See: https://github.com/mevdschee/tqdbproxy
What is TQDBProxy?
TQDBProxy is a caching database proxy implemented in Go, and can be used as both as an embedded library and as a standalone server. It speaks the MariaDB (MySQL) as well as the PostgreSQL protocol, including authentication, meaning that in server mode it works out-of-the-box with existing clients.
TQDBProxy allows you to cache SELECT queries with a configurable TTL and route them to read replicas. It ensures that only a single query is sent to healthy replicas during warmup and refresh. Queries are automatically tracked by source file and line number for observability and Prometheus-style query metrics are exposed labeled by this source location. It also supports hot-reloading of the configuration.
I’ve learned that these features may be useful in high performance environments.
Demo
Interactive clients work without restrictions (easy for debugging) as you will see in the demo below:
mariadb -u tqdbproxy -p -P 3307 tqdbproxy --comments
Note that the “–comments” argument is essential (to allow sending comments).
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 19905
Server version: 10.11.14-MariaDB-0ubuntu0.24.04.1 Ubuntu 24.04
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [tqdbproxy]> /* ttl:60 */ SELECT SLEEP(1);
+----------+
| SLEEP(1) |
+----------+
| 0 |
+----------+
1 row in set (1,001 sec)
MariaDB [tqdbproxy]> SHOW TQDB STATUS;
+---------------+---------+
| Variable_name | Value |
+---------------+---------+
| Backend | primary |
| Cache_hit | 0 |
+---------------+---------+
2 rows in set (0,000 sec)
MariaDB [tqdbproxy]> /* ttl:60 */ SELECT SLEEP(1);
+----------+
| SLEEP(1) |
+----------+
| 0 |
+----------+
1 row in set (0,000 sec)
MariaDB [tqdbproxy]> SHOW TQDB STATUS;
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Backend | cache |
| Cache_hit | 1 |
+---------------+-------+
2 rows in set (0,000 sec)
MariaDB [tqdbproxy]>
Performance
The proxy adds significant overhead for trivial queries. It is more than 50% slower for sub-millisecond operations. MariaDB drops from 398K to 174K RPS, and PostgreSQL from 267K to 121K RPS when running through the proxy instead of direct connections.
However, this overhead becomes negligible for real-world queries. At 1ms query latency, the difference shrinks to just 11% (91K vs 81K RPS for MariaDB). For most applications where database queries take several milliseconds, the proxy overhead is barely measurable.
The tradeoff pays off in three ways: each application server can run its own proxy with local cache, reducing network hops to the database. SELECT queries with TTL hints are routed to read replicas, offloading the primary. And cache hits bypass the database entirely, achieving high RPS regardless of original query complexity.
In real world scenarios, you can handle 10x-100x more load by adding cache hints. The metrics help you to find the expensive queries that need cache hints. If that is not enough, you can add some light-weight read replicas to offload the primary even more.
How it Works
The approach relies on custom client libraries for PHP, TypeScript, and Go.
These clients add a ttl parameter to the already familiar query methods on
these platforms. Next to this TTL, the caller’s source file and line number are
automatically added to SQL comments. The proxy extracts this metadata and
exposes Prometheus-style query cost metrics labeled by source location.
TQDBProxy speaks native MariaDB and PostgreSQL wire protocols. Clients connect to the proxy exactly as they would to the database server. Even the authentication is forwarded properly by the proxy. The proxy parses incoming queries, classifies them as cacheable or not, and extracts TTL hints from SQL comments.
SELECT queries with a TTL hint are cacheable. For these queries, the proxy first checks its cache. On a hit, it returns the cached response immediately. On a miss, it routes the query to an available replica (or primary if no replicas are configured). The proxy then caches the response before returning it to the client.
When a cache entry does not exist (cold cache), the proxy ensures only a single query is sent to the database. This prevents concurrent queries for the same key during warmup.
When a cache entry is expired, the proxy returns a stale response to the client while calculating the new response in the background. This prevents concurrent queries for the same key during refresh.
Non-cacheable queries, such as INSERT, UPDATE, DELETE, SELECT queries without a TTL hint, or queries that are part of a transaction always go to the primary.
The proxy registers metrics for cache hits/misses, query latency, and more.
Conclusion
TQDBProxy is not (yet) a replacement for established proxies like pgpool-II,
MaxScale, or ProxySQL, but it is trying to get there. It provides a lot of
features that have proven useful in high performance environments. It is easy to
use: install a client library, call queryWithTTL() instead of query(), and
you get caching with stale reads, replica routing, and metrics of expensive
queries by source file and line number.
Disclaimer: I built this as a learning project. Test thoroughly before production use.
See: https://github.com/mevdschee/tqdbproxy