# Redis to Postgres: the three patterns that actually get used

"Redis to Postgres" is three questions wearing one search box. Sometimes it means putting a cache in front of a database. Sometimes it means a queue that hands work to a database. And sometimes it is a literal move: data sitting in Redis that should live in Postgres, or the other way around. Each has a right answer, and they are not the same answer. This guide walks through all three with the real commands, then asks the question underneath: whether you need both engines at all.

## First: nothing connects Redis to Postgres directly

The common picture is a wire between the two: Redis plugged into Postgres, syncing away. There is no such wire in a normal stack. Redis does not read Postgres and Postgres does not read Redis. Your application holds both connection strings and is the only thing that talks to both. Every pattern below is a rule about what your application does, not a feature you switch on.

That is also the good news: any two engines from any two providers combine, because the combination lives in your code.

## Pattern one: Redis as the cache in front of Postgres

Cache-aside covers most "Redis and Postgres" stacks:

1. The app needs a value: `GET cache:user:42` from Redis.
2. On a miss, read Postgres: `SELECT ... WHERE id = 42`.
3. Store it: `SET cache:user:42 <value> EX 300` - always with a TTL.
4. On every write: `UPDATE ...` in Postgres, then `DEL cache:user:42`.

Two rules keep it correct. Every key gets a TTL, because a cache without expiry is a bug that has not happened yet. And the write path deletes the key rather than updating it, because writing two stores in the right order under concurrency is a problem you do not need.

Before adding Redis at all, know that Postgres caches aggressively on its own and answers hot reads from shared buffers in microseconds. Redis earns its place when the same expensive read repeats - a rendered page, a joined report, a per-user dashboard - or when what you store is not a database row: a rate-limit counter (`INCR` plus `EXPIRE`), a leaderboard (a sorted set), a pub/sub feed.

## Pattern two: Redis as the queue, Postgres as the record

The second job: work arrives (a sign-up, an upload, a webhook), the app pushes a job onto Redis with `LPUSH jobs {"id": 123}` or a stream, and a worker pulls it with `BRPOP jobs 5` and does the slow part. Postgres holds what must survive: the user, the charge, the job's result.

The rule that keeps this honest: Postgres is the record, the queue is disposable. If Redis vanished tonight, the worst case should be re-queueing work, never losing it. So the worker writes outcomes to Postgres, and the consumer is idempotent - any job can arrive twice, and processing it twice must be safe.

For small volumes Postgres alone queues fine: a jobs table with `SELECT ... FOR UPDATE SKIP LOCKED` is a respectable queue into the thousands of jobs a minute. Add Redis when you need the throughput or the blocking pop, not before.

## How do I move data from Redis to Postgres?

The literal reading of the search, and the one with the fewest good answers online. There is no built-in sync: you export from one side and load into the other.

Redis to Postgres. Walk the keyspace with `SCAN` (never `KEYS *` on a live box), read each value, and load with `COPY`. `redis-cli --scan --pattern 'session:*'` lists the keys; `GET` each one; on the Postgres side `COPY sessions FROM STDIN` takes the rows at full speed. For a few thousand keys a shell pipeline is enough. Beyond that, write the ten-line script in your app's language and pipeline the reads.

Postgres to Redis is the same shape in reverse: `COPY (SELECT ...) TO STDOUT` out of psql, transform each row into a `SET` command, and pipe the commands into redis-cli, which reads them from standard input. Watch the quoting: values with spaces or binary bytes need escaping, which is the other reason the ten-line script beats the one-liner for real data.

Both moves share one warning: Redis types and Postgres types do not map one-to-one. Decide the target shape first - a sorted set is usually a table with an index, a hash is usually a row or a JSONB document - and only then write the small script.

## When Postgres alone is enough

If what you want from Redis is derived data, Postgres has native answers: an `UNLOGGED` table for ephemeral high-write data (no write-ahead-log overhead, gone on a crash - which is what a cache is anyway), `JSONB` for document-shaped values, materialized views for expensive reads that may lag. Many "we need Redis" stacks are one Postgres feature away from running one engine less.

## When Redis alone is enough

The honest other side: if there is no durable record - a pure cache, rate limiting, short-lived sessions, pub/sub - Postgres adds nothing. Redis 8 persists with its append-only file, and a daily machine image backs it up here, but treat single-node Redis as durable within the limits of one machine, not as the system of record.

## The decision, as a table

| You need | Postgres alone | Redis alone | Both |
| --- | --- | --- | --- |
| The durable record of what happened | Yes | No | Postgres holds it |
| The same expensive read, served hot | Index or materialized view | Yes | Redis in front |
| Rate limits, counters, leaderboards | Possible | Yes | Redis |
| A job queue at small volume | `SKIP LOCKED` | Yes | Either |
| Sessions and pub/sub | Possible | Yes | Redis |
| A one-time move of data | - | - | Export, then `COPY` |

## Running both without two providers

The usual end state of "Redis and Postgres" is two providers, two bills and two metering models. We built Lathe so it is one: Redis 8 and PostgreSQL 17 run on the same dedicated machine, each with a memory budget you set, TLS and a password on both, for one flat price. Commands and queries are not metered, and the cache answers from the box the database is on, so no cross-provider latency sits on every read. The move from another Postgres is [three commands and a connection string](https://docs.lathe.live/guides/migrate-postgres).

The honest limits: one machine means no automatic failover, and a daily backup image means a restore can lose up to a day of writes. If you need a cluster, you need more than one instance - or a different architecture.

### Sources and further reading

- [Redis eviction policies](https://redis.io/docs/latest/develop/reference/eviction/)
- [Redis persistence](https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/)
- [PostgreSQL: CREATE TABLE, including UNLOGGED](https://www.postgresql.org/docs/current/sql-createtable.html)
- [PostgreSQL: COPY](https://www.postgresql.org/docs/current/sql-copy.html)
- [Lathe's managed Postgres](https://lathe.live/postgres) and [managed Redis](https://lathe.live/redis)
- [One machine, the whole stack](https://docs.lathe.live/guides/one-machine)
