Two different conversations. Coming from Redis, the protocol is the same and the question is what behaves differently. Coming from a relational database, nothing is the same and the question is which part of the workload should move at all — the answer is some of it, and we will say which.
Your client does not change. kevy speaks RESP2 and RESP3 and answers 206 commands. Point your existing library at it, keep your code, keep your redis-cli. There is no SDK to adopt and no new protocol to learn.
So the only real question is what you gain. Four things, and if none of them is worth anything to you, then stay on Redis — it is a superb piece of software and switching for its own sake is a waste of your week.
Embed it in a binary, ship it to a browser tab, boot it on a Cortex-M with no allocator. Today each of those needs its own storage layer with its own API; here it is the same engine and the same commands. If you have ever written a second cache for the client side, this is the reason to look.
Secondary indexes, materialised views, vector KNN and BM25 full-text are in the engine — not a module, not a sidecar, not a second copy of the data drifting out of sync with the first. Teams running Redis plus a search cluster can often run one thing.
# look up by a field, not just by the key
IDX.CREATE idx:city ON PREFIX user: FIELD city TYPE str KIND range
IDX.QUERY idx:city EQ osaka
# vectors, in the same engine, over the same keys
IDX.CREATE idx:sem ON PREFIX doc: FIELD vec TYPE vector KIND ann DIM 768 DISTANCE cosine
IDX.QUERY idx:sem KNN "<vector>" LIMIT 101.33× on GET, 2.66× on SET, 2.05× on INCR against Redis 8.10.1 on the same machine. Read the whole table before you count on it, though — LPUSH and ZADD are only 10% and 15% ahead, and if lists or sorted sets are your hot path this is not the reason to move.
Give the store a RAM budget and the coldest values spill to a disposable value log on disk, paging back on access — every command unchanged on a cold key, the append-only-log durability contract untouched. RAM bounds how many keys you hold; disk bounds how much data. That replaces the Redis-plus-separate-disk-store split for big-value and long-tail workloads. The honest limits: it is off by default, v1 spills strings and hashes (lists, sets and streams stay hot), and values under 64 bytes never spill — a stub would be as big as the value.
# kevy.toml
[tiering]
budget = "70%" # or "4gb", or "auto"No cluster. Replicas are copies, not shards. No AUTH, no TLS. And a handful of commands behave differently — the one to know: a cross-shard RENAME is not atomic (multi-key writes are atomic per shard, not globally). None of these is a bug and all of them are documented per command. Read the list before you commit, not after: every command's real cost and real deviation.
# 1. dump what you want to move. it is a RESP file — readable,
# diffable, and it streams rather than loading into memory.
kevy-cli export -p 6379 --prefix user: dump.resp
-> exported 41023 keys -> dump.resp
# 2. load it. --strict stops on the first error rather than
# limping onward with a half-migrated keyspace.
kevy-cli import -p 6380 --strict dump.resp
-> imported 82046 ok, 0 errors, offset 4108331
# 3. prove they agree, rather than hoping.
kevy-cli digest -p 6379 user:
kevy-cli digest -p 6380 user:
-> 41023 keys 3bca92aa52269300 # the same hash, or you did not migrate
# an interrupted import resumes where it stopped:
kevy-cli import -p 6380 --resume dump.respExport from Redis, import into kevy, and check the two agree. Every command below was run.
Do not move your database. Move the part of it that was never a database problem.
Sessions. Rate limits. Feature flags. Job queues. The hot row every request reads and nobody ever joins against. These live in Postgres in most applications, and they are the rows getting hammered — not because a relational database is bad at them, but because they were never questions. They are lookups. You already know the key.
Keep Postgres for what it is unmatched at — joins, ad-hoc queries, analytics, transactions with real isolation across unrelated rows. kevy takes the serving path and gives the database its evenings back.
And single-table serving reads can move too. Declare typed columns, secondary indexes and composite ORDER BY paths once with TABLE.DECLARE — or compile the PG/MySQL schema file you already have with kevy-sql — and the read path of one table (indexed WHERE, residual filters, ORDER BY, pagination, COUNT) compiles onto kevy indexes, with no planner at query time. kevy-sql is a build-time compiler, not a SQL engine: joins and ad-hoc SQL are refused by name, and they stay in Postgres. That is the part of an ORM most applications actually use.
Per workload. The three rows in red are the ones people get wrong.
| Workload | Move it? | Why |
|---|---|---|
| Sessions, tokens | *Yes | A lookup by key with a TTL. The database was doing you a favour, not a job. |
| Rate limits, counters | *Yes | INCR with an expiry is atomic and O(1). In SQL this is a row lock on your hottest row. |
| Job queues | *Yes | Lists and streams, with consumer groups and per-message acknowledgement. A queue table is a lock convention with extra steps. |
| Feature flags, config | *Yes | Read constantly, written rarely, joined never. |
| Single-table reads (filtered, ordered, paged) | *Yes | Declare the table's access paths once — or compile your schema file with kevy-sql — and the indexed WHERE + ORDER BY + LIMIT read stays a lookup. See serving reads. |
| Aggregates (counts, totals) | *Often | A materialised view keeps it current on the write path instead of recomputing it on every read. |
| Joins across several tables | !No | kevy has no joins and will not grow them. This is what Postgres is for. |
| Analytics, ad-hoc queries | !No | There is no query planner and no optimiser. Do not try. |
| Transactions across unrelated rows | !No | MULTI is per shard, not global. If you need serialisable isolation across the keyspace, you need a database. |
The three red rows are not a to-do list. They are refusals — kevy will not grow joins or an optimiser, because doing either badly is worse than not doing it. Every relational workload, with what it actually costs here, including the ones where the honest answer is "keep it in Postgres".
# 1. pick ONE workload. sessions are the usual first, because
# nothing joins against them and losing one is survivable.
# 2. write to both for a week. reads still come from Postgres.
# you are checking that the shapes match, not that it is fast.
# 3. flip reads to kevy. keep the dual write.
redis-cli SET session:$SID "$JSON" EX 3600
# 4. when it has been boring for a fortnight, drop the table.
# then do the next workload. rate limits, then queues, then
# whichever of your read paths a secondary index can answer.Nothing is cut over at once. Move one workload, keep the database as the source of truth, and measure.
The same three commands run in the other direction. kevy-cli export writes a plain RESP file that any Redis-compatible server will import, and digest proves the copy is faithful. The migration guide covers moving out as carefully as moving in — we would much rather you leave cleanly than stay because you are stuck.