Web Analytics Made Easy - Statcounter
BaseKV
Sign InSign Up
Back to Articles

"Just Use Postgres"? When a Key-Value Store Still Wins (2026)

The "just use Postgres for everything" take is mostly right - a JSONB table is a fine key-value store. Here is the honest counter-case: the four workload shapes where a dedicated, durable KV store still beats a Postgres table, and a clear rule for deciding.

BaseKV Team9 min read
postgreskey-value-storedatabase-architectureperformancesystem-design

"Just use Postgres." It is the loudest database take of 2026, and the people saying it are mostly right. Postgres is the safest default a backend engineer can pick. It gives you ACID transactions, a battle-tested query planner, foreign keys, rich indexing, and three decades of operational knowledge baked into every monitoring tool and managed-hosting tier you will ever touch. If you want a key-value store, Postgres has one built in: a JSONB column or the hstore type turns a single table into a perfectly serviceable KV map, with the bonus that you can still JOIN against it and query inside the value. Reaching for a separate datastore before you have hit a wall is how teams end up running five databases they cannot all reason about. So when someone says "just use Postgres for everything," the honest first response is: yes, usually, do that.

This article is not a rebuttal of that advice. It is the careful footnote it deserves. Because "usually" is not "always," and the cases where a dedicated key-value store still wins are specific, measurable, and worth naming precisely so you can tell when you are in one. The point is not to talk you out of Postgres. It is to give you a clear rule for the moment your hot path stops looking like a Postgres workload and starts looking like a key-value one.

The steelman: Postgres really is a fine key-value store

Before the counter-case, it is worth being concrete about how good Postgres-as-KV actually is, because the contrarian crowd often understates it.

A KV table in Postgres is genuinely simple:

CREATE TABLE kv (
  key   TEXT PRIMARY KEY,
  value JSONB NOT NULL,
  ttl   TIMESTAMPTZ
);

That gives you point lookups by primary key (a B-tree index, so O(log n) and fast), structured values you can index into with GIN indexes, and the full weight of transactions around every write. You can do a compare-and-set in a transaction. You can update ten keys atomically. You can ask questions the value was never designed to answer ("every key whose JSON status field is expired") without a schema migration. For the overwhelming majority of applications that store sessions, settings, or per-user blobs, this is not a compromise. It is the correct, boring, durable choice, and it is the same engine handling your relational data, so there is nothing new to operate.

If your "key-value need" is a few thousand reads per second against data that also benefits from occasional ad-hoc querying, stop reading and use a Postgres table. The rest of this is about the workloads where that stops being true.

Where Postgres-as-KV starts to strain

Postgres pays for its generality in two currencies on the hot path: per-query overhead and contention. A single point read in Postgres travels through connection handling, the parser, the planner, MVCC visibility checks, and the executor. That machinery is cheap in absolute terms but not free, and it is the same machinery whether you asked for a 12-way join or SELECT value FROM kv WHERE key = $1. A purpose-built key-value store skips almost all of it: parse a tiny binary command, descend a tree, return bytes.

The numbers bear this out. A well-tuned Postgres instance doing simple primary-key lookups typically tops out somewhere in the 10,000 to 20,000 operations per second range per node before connection and CPU overhead dominate, and you usually need PgBouncer in front of it to get there without exhausting backends. A dedicated key-value store on the same hardware comfortably does 100,000+ operations per second, often several times that, because the entire code path was built for exactly one shape of request. This is not Postgres being slow. It is Postgres being general, and generality has a fixed cost per request that a single-purpose engine does not pay.

The second strain is write contention on hot rows. The classic example is a counter. If you want to increment a single views row a few thousand times a second, every one of those UPDATEs in Postgres takes a row lock and creates a new MVCC row version, and they serialize on that one row while VACUUM chases the dead tuples behind them. A key-value store with a native atomic INCR does the increment in the storage engine without any of that bookkeeping. The difference is not subtle when the row is hot.

The four shapes where a KV store still wins

Strip away the marketing and there are four concrete workload shapes where a dedicated key-value store beats a Postgres table, and they tend to show up together.

1. Sub-millisecond point reads at high throughput. Session lookups, feature-flag checks, permission caches, anything on the request critical path that is read far more than it is written. When you are doing this 50,000+ times a second and every microsecond of p99 is visible to a user, the per-query overhead of a general engine is the bottleneck, and a KV store's single tree descent wins. We dig into why a tree-shaped engine is fast at this in what is a key-value database.

2. Atomic counters and windows without table contention. Rate-limit windows, idempotency keys, distributed locks, quota counters. These are tiny, hot, write-heavy, and want atomic read-modify-write semantics on a single key. A KV store's INCR, SETNX, and similar primitives do these without locking a table row or generating dead tuples. This is one of the canonical patterns in key-value database use cases.

3. Built-in TTL semantics. Expiry is a first-class operation in a key-value store: SET key value EX 300 and the engine forgets the key on its own. In Postgres you either store a ttl column and filter on it in every query (and run a periodic DELETE to reclaim space) or you build a partitioned-table-plus-cron contraption. For ephemeral data such as sessions, OTP codes, and short-lived caches, native TTL removes a whole category of housekeeping.

4. Edge and embedded deployment. Sometimes the right place for the data is next to the code, in a process or a tiny sidecar, where standing up a full Postgres server is overkill. A single-file, embeddable KV engine fits in a container or an edge node with no server to operate. This is the same tradeoff that makes SQLite competitive with a key-value store for embedded workloads worth thinking through.

The honest tradeoff: what you give up

A key-value store is faster on its narrow path precisely because it does less, and "less" is not free. Be clear-eyed about what you surrender when you move data out of Postgres into a KV store.

You give up ad-hoc queries. A KV store finds data by key. If you cannot construct the key, you cannot find the value without scanning everything, which is exactly the kind of question Postgres answers in milliseconds with an index. You give up joins. There is no relating one key's value to another's inside the store; that logic moves into your application, which now does multiple round trips and reassembles results itself. And you give up multi-key transactions in the general case. Many KV stores offer atomic operations on a single key and some offer limited multi-key transactions, but none give you Postgres's arbitrary-statement ACID transaction across your whole dataset.

In practice this means a KV store is almost never a replacement for your primary relational database. It is a specialized layer for the hot, simple paths, sitting alongside Postgres, not instead of it. The teams that get burned are the ones who move relational data into a KV store and then spend the next year reimplementing joins and consistency by hand in application code.

"But the KV store loses data on restart"

The strongest objection to adding a key-value store is durability, and it is usually aimed at Redis: it lives in RAM, and unless you have carefully tuned RDB snapshots or AOF, a process restart or a pulled power cord can lose recent writes. That objection is real, and it is the single best argument for keeping everything in Postgres, where durability is the default and a committed transaction is on disk.

The objection only holds against cache-only KV stores. A disk-backed key-value store does not have this property. BaseKV stores every key on disk in a B+tree file, the same family of storage engine Postgres itself uses for its indexes, so a committed write survives a restart the way a Postgres COMMIT does. The durability argument that rules out Redis for your system of record does not rule out a durable KV store, because the data is not in volatile memory in the first place. If durability is why you were going to keep counters and sessions in Postgres, a disk-first KV store removes the reason without removing the durability. The full comparison between in-memory and disk-first KV is in key-value store vs Redis in 2026.

Postgres-as-KV vs a dedicated KV store

| Dimension | Postgres (JSONB / hstore table) | Dedicated KV store (disk-backed) | |---|---|---| | Point-read latency | Good, with per-query overhead | Excellent, single tree descent | | Throughput (point ops) | ~10K to 20K ops/sec per node | 100K+ ops/sec per node | | Ad-hoc queries | Full SQL, joins, secondary indexes | By key only; no joins | | Atomic counters | Row lock + MVCC churn on hot rows | Native INCR, no table contention | | TTL / expiry | Manual column + cron cleanup | Built-in per-key TTL | | Cross-key transactions | Full ACID across the dataset | Single-key atomic; limited multi-key | | Operational model | One engine you already run | Tiny, single-purpose, simple to operate | | Durability | Default, on commit | Default for disk-backed; tunable risk for cache-only |

The table is the whole argument in one place: Postgres wins every column that involves querying, relating, or transacting across data; the KV store wins every column that is about doing one simple thing very fast and very often.

A clear rule for deciding

Default to Postgres. Genuinely. Start there, put your KV-shaped data in a JSONB table, and ship. Reach for a dedicated key-value store only when your hot path is clearly KV-shaped and one of these is true:

  • The path is read-dominated point lookups at a throughput where Postgres's per-query overhead is your measured bottleneck (you have profiled it, not guessed).
  • You have hot atomic counters or rate-limit or idempotency windows causing row contention or VACUUM pressure on a single table.
  • You need per-key TTL and find yourself building cron-driven expiry machinery to fake it.
  • Operational simplicity or edge/embedded deployment matters more than ad-hoc query power for that specific data.

If none of those is true, the contrarian crowd is right and you should not add a second datastore. If one or more is true, you are in the narrow band where a KV store earns its place, and the right move is to add it for that path while Postgres keeps owning everything relational. Use a disk-backed KV store rather than a cache-only one so the "but it loses data" objection never applies, and you keep durability on both sides.

Common questions

Is using a JSONB column as a key-value store an anti-pattern?

No. It is a legitimate and common pattern, and for most KV-shaped needs it is the right one. It only becomes a problem when the access pattern is high-throughput point reads or hot atomic counters, where Postgres's per-query overhead and row-level MVCC contention start to dominate. Below that threshold, a JSONB table is simpler and more durable than running a second system, and you keep the ability to query inside the value.

Can a key-value store replace Postgres entirely?

Almost never, and you should be suspicious of any pitch that says it can. A KV store finds data by key and offers little or no joining or cross-key transactional querying. Anything relational, anything you query by attribute rather than by key, belongs in Postgres. The realistic architecture is Postgres as the system of record with a KV store layered in front of or beside it for the hot, simple paths.

Does the choice change with NVMe and modern hardware?

It narrows the gap rather than closing it. Faster storage helps every engine, and it particularly helps disk-backed KV stores reach memory-class read latency without memory-class cost. But the per-query overhead of a general SQL engine and the contention cost of MVCC on hot rows are CPU-and-design problems, not disk problems, so they do not disappear when the disk gets faster. The shapes where a KV store wins are the same on NVMe; the KV store just gets cheaper to run durably.


Related: Key-Value Store vs Redis in 2026, SQLite vs Key-Value Store Performance, Key-Value Database Use Cases, What Is a Key-Value Database?, Persistent Key-Value Storage.