Key-Value Databases Explained with Examples
Chat2DB TeamA key-value database is the simplest data model that is still useful: a dictionary that survives a process restart. You store a value under a key, and you get it back by supplying that key. There are no columns, no schema, and no query language — which is exactly the point, because removing those things is what lets a key-value store answer in microseconds and scale horizontally without much thought.
The interesting part is not the model. It is what you give up, and what you have to design around.
The data model
Three operations cover the core of every key-value store:
PUT(key, value) store a value under a key
GET(key) -> value retrieve it
DELETE(key) remove itThe key is almost always a string or byte array. The value is opaque to the database — it might be a number, a JSON document, a serialised protobuf, or a JPEG. The store does not parse it, index it or validate it. That opacity is the fundamental trade: the database can be extremely fast because it never has to understand what you stored.
The direct consequence: you cannot query by value. There is no WHERE country = 'DE'. If you need that access path, you must build it yourself as another key.
What a key actually represents
In a key-value database the key carries all the structure your data model has. So key design is schema design. The universal convention is a delimited, hierarchical namespace:
user:1042 -> {"name":"Ana","email":"ana@example.com"}
user:1042:sessions -> ["s_88f1","s_2b30"]
session:s_88f1 -> {"user_id":1042,"expires":1788480000}
order:9931 -> {"user_id":1042,"total":"41.20"}
user:1042:orders -> [9931, 9944]
cart:1042 -> {"items":[{"sku":"A1","qty":2}]}
ratelimit:api:1042:2026-09-03T14 -> 87Three rules that hold up in practice:
- Prefix by entity type, so you can scan or expire a class of keys together.
- Encode the access path, not the data.
user:1042:ordersexists because "list a user's orders" is a query you need; the key is the index. - Never build a key from a value you might change. A key derived from an email address becomes a migration when someone changes their email.
Redis: key-value with useful value types
Redis is the most widely used key-value store, and it stretches the model by giving values real types with server-side operations. That avoids the read-modify-write round trip that a pure opaque-value store would force.
# strings and counters
SET user:1042:name "Ana"
INCR page:home:views
SET session:s_88f1 '{"user_id":1042}' EX 3600 # expires in an hour
# hashes — field-level access to a record
HSET user:1042 name "Ana" email "ana@example.com" tier "pro"
HGET user:1042 email
HINCRBY user:1042 login_count 1
# sorted sets — a leaderboard
ZADD leaderboard 4820 "player:17" 5130 "player:42"
ZREVRANGE leaderboard 0 9 WITHSCORES
# lists — a simple queue
LPUSH jobs:email '{"to":"ana@example.com"}'
BRPOP jobs:email 0
# sets — membership
SADD user:1042:roles admin billing
SISMEMBER user:1042:roles adminEX on SET matters more than it looks. Automatic expiry is a feature relational databases do not have, and it is why Redis is the default for sessions, caches and rate limiting — the cleanup problem disappears.
An atomic rate limiter, the canonical example:
MULTI
INCR ratelimit:api:1042:2026-09-03T14
EXPIRE ratelimit:api:1042:2026-09-03T14 3600
EXECNote that Valkey is now the actively developed open-source fork of Redis after the 2024 licence change, and is API-compatible.
DynamoDB: key-value at scale, with a twist
Amazon DynamoDB is a managed key-value store whose primary key can be either a single partition key or a composite of partition key + sort key. The composite form is what makes it more than a dictionary: items sharing a partition key are stored together, ordered by sort key, and can be range-queried.
PK SK Attributes
USER#1042 PROFILE name=Ana, tier=pro
USER#1042 ORDER#2026-09-01#9931 total=41.20, status=shipped
USER#1042 ORDER#2026-09-03#9944 total=12.50, status=pending# every order for a user, newest first — one request, no scan
resp = table.query(
KeyConditionExpression=Key('PK').eq('USER#1042')
& Key('SK').begins_with('ORDER#'),
ScanIndexForward=False,
)This is the single-table design pattern: multiple entity types in one table, distinguished by key prefix, arranged so that each access pattern is a single query. It is powerful and genuinely hard — you must enumerate every access pattern before you create the table, because changing the key schema later means rewriting the data.
The failure mode to know about is the hot partition: if one partition key takes a disproportionate share of traffic, you throttle even though the table as a whole is far under capacity. The fix is to spread the key, for example by appending a shard suffix ORDER#2026-09-03#7 and querying all shards in parallel.
RocksDB and LevelDB: embedded engines
RocksDB is a key-value library, not a server. It runs inside your process and writes to local disk, and it is the storage engine underneath a surprising number of systems — Kafka Streams state stores, CockroachDB, TiKV, MySQL's MyRocks, Flink's checkpoints.
Its design is the log-structured merge tree: writes land in an in-memory memtable and a write-ahead log, get flushed to sorted immutable SST files, and are compacted in the background into larger levels. That gives excellent write throughput at the cost of read amplification and periodic compaction load.
import rocksdb
db = rocksdb.DB("state.db", rocksdb.Options(create_if_missing=True))
db.put(b"user:1042", b'{"name":"Ana"}')
value = db.get(b"user:1042")
it = db.iteritems() # keys are sorted, so prefix scans work
it.seek(b"user:1042:")Sorted keys are the important property: unlike a hash-based store, an LSM tree supports range scans, which is what makes prefix conventions such as user:1042: genuinely useful.
etcd: key-value for coordination
etcd stores small amounts of critical data — it is where Kubernetes keeps all cluster state. It trades throughput for strong consistency via Raft consensus, and adds watches and leases.
etcdctl put /services/api/node-1 '{"addr":"10.0.1.7:8080"}' --lease=694d...
etcdctl get /services/ --prefix
etcdctl watch /services/ --prefix # stream changes as they happenLeases give you service discovery almost for free: a node registers with a lease it must keep renewing, and its key disappears automatically when it stops.
Where key-value stores break down
Anything you need to query by value. "Which users are on the pro tier?" requires either a full scan or a secondary index you maintain yourself:
# maintained by hand — and now you own the consistency problem
SADD tier:pro user:1042
SREM tier:free user:1042Those two commands must both succeed. If your process dies between them, the indexes disagree, and nothing in the database will tell you.
Joins. Fetching a user and their orders is N+1 round trips, or a denormalised copy you keep in sync.
Multi-key transactions. Redis MULTI/EXEC is not a rollback mechanism — it batches commands atomically but does not undo on logical failure. DynamoDB offers TransactWriteItems with real limits on item count and size.
Aggregations. Counting, summing or grouping means reading everything, or maintaining counters that can drift.
Schema drift. Since values are opaque, nothing stops one process writing {"tier":"pro"} and another writing {"tier":2}. Every reader must handle both, forever.
Choosing
Reach for a key-value store when:
- the access pattern really is lookup by a known key;
- you need sub-millisecond latency at high request rates;
- data has a natural lifetime (sessions, caches, rate limits, tokens);
- you need horizontal scale without coordination.
Stay with a relational database when:
- you query by attributes that are not the key;
- entities have relationships you need to traverse;
- you need real transactions across multiple records;
- your access patterns will change — because in SQL that is a new index, and in a key-value store it is a data migration.
The most common production answer is both: PostgreSQL as the system of record, Redis in front for caching and sessions. That means two very different tools open all day. Chat2DB (opens in a new tab) connects to Redis alongside PostgreSQL, MySQL, MongoDB and 20+ other engines in a single workspace, so browsing keys and querying tables happen in one place — there is a browser version at app.chat2db.ai (opens in a new tab) as well.
Summary
A key-value database is a persistent dictionary with opaque values. Its speed comes from refusing to understand your data; its cost is that every access path you need must be encoded in a key you design and maintain yourself. Redis adds typed values and expiry, DynamoDB adds sort keys and managed scale, RocksDB embeds an LSM engine in your process, and etcd trades throughput for consensus. Pick by access pattern first — if you cannot list every query up front, that is a strong argument for SQL.
