Databases·8 min read·

Redis Cache in Front of Postgres or MySQL

Put Redis in front of Postgres or MySQL with cache-aside reads, write-time invalidation, and TTLs so the cache stays a speed layer, not a second database.

NB

Netbay Engineering

Netbay Engineering

On this page

Postgres and MySQL are the source of truth. Redis is the speed layer you put in front when the same row is read far more often than it is written. The pattern that stays honest is cache-aside: the application reads Redis first, hits the database on a miss, then writes the Redis key with a TTL. On write, the application updates the database and deletes the cache key. Anything more clever — write-behind, dual writes, Redis as a second store of record — turns a cache miss into a consistency incident.

This post stays on a single VPS. Redis binds 127.0.0.1. The app, Redis, and the SQL server share the host. That layout is enough for a surprising amount of traffic if the cache key design is boring and the TTLs are real.

Cache-aside is GET, then SQL, then SETEX

A read path has three steps. GET the cache key. If it exists, return it. If it does not, query SQL, then SETEX the payload. SETEX is SET plus EXPIRE in one command, so you never insert a key without a TTL.

javascript
const key = 'product:' + productId;
function getProduct(productId, cb) {
  redis.get(key, function (err, hit) {
    if (err) return cb(err);
    if (hit) return cb(null, JSON.parse(hit));
    db.query('SELECT id, name, price FROM products WHERE id = ?', [productId], function (err2, rows) {
      if (err2) return cb(err2);
      const row = rows[0] || null;
      if (row) redis.setex(key, 60, JSON.stringify(row));
      cb(null, row);
    });
  });
}

JSON in a string value is fine at this size. Hash fields are better when you later want HGET of one column, but they make invalidation slightly noisier. Start with a string per row and a key that names the table and the id.

The write path is SQL first, then DEL. Do not SET the new value from the write path unless you enjoy race conditions between a concurrent reader that still sees the old SQL row.

javascript
function updatePrice(productId, price, cb) {
  db.query('UPDATE products SET price = ? WHERE id = ?', [price, productId], function (err) {
    if (err) return cb(err);
    redis.del('product:' + productId, cb);
  });
}

A short TTL is the safety net when a DEL is missed. Sixty seconds is a good default for product pages. Five seconds is right for a rapidly changing inventory counter. One hour is right only if you truly invalidate on every write and you accept stale reads when a worker forgets.

Stampede, nulls, and what not to cache

When a hot key expires, many requests miss together and all hit SQL. For a small VPS that storm is the moment Postgres wait events spike. Mitigations that actually work: longer TTLs on the hottest keys, a single-flight lock (SET key:lock NX EX 5 around the SQL fetch), or a stale-while-revalidate read that returns the old value while one caller refreshes. Do not start with a lock. Start with a TTL that matches how often the row changes.

Negative caching is optional. If a missing product id is requested in a loop, SETEX a short sentinel so SQL is not hammered. Expire it in 10 seconds so a newly inserted row appears quickly.

Do not cache rows that are user-specific and huge. Do not cache query results that take a LIMIT from the client without putting the LIMIT in the key. Do not cache across tenants without the tenant id in the key. The key is the contract.

bash
redis-cli INFO stats | grep keyspace
redis-cli INFO stats | grep evicted
redis-cli --scan --pattern 'product:*' | head

A healthy cache shows a high keyspace_hits ratio, some evictions if maxmemory is set, and keys that all have TTLs. TTL 0 or a missing expire on a cache key is a leak. Run a sample: redis-cli TTL product:42 should return a positive integer.

Redis config that matches a cache

The instance should evict. Persistence can be off. maxmemory must sit well under the VPS RAM so Postgres still has a buffer pool.

bash
bind 127.0.0.1
maxmemory 512mb
maxmemory-policy allkeys-lru
save ""
appendonly no

If Redis restarts, the cache is cold and SQL takes the load until keys refill. That is acceptable. It is not acceptable for sessions. Keep those on a second instance or a second database index only if you are disciplined; two processes with two maxmemory values are safer.

Netbay VPS nodes in Lucknow run Intel Xeon Platinum with High-Speed SSD. The SSD helps Postgres; Redis is still RAM. Size the plan for the working set of hot keys plus SQL, not for a dump of the entire products table.

Cache-aside read and write App GET Redis SQL miss SETEX 60s Write: SQL first, then DEL never dual-write the new JSON TTL is the safety net when a DEL is missed

Takeaway

Read Redis, fall back to SQL, SETEX with a TTL, and DEL on write. Redis is a speed layer, not a second Postgres. Spin up an Ubuntu 24.04 box on Netbay and put both on loopback — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance