ReplDB to Valkey: your Replit key-value store, grown up
ReplDB is the smallest database people actually build on. Not a scratch file, not a demo: apps with real users keep their whole state in a key-value store that tops out at 5,000 keys and answers a single HTTP endpoint. I have a lot of time for that. It is one environment variable, no schema, no connection to manage, and await db.set('score', 42) works about four seconds after you decide you need persistence. As on-ramps go it is close to ideal.
The trouble is that it is an on-ramp with a wall at the end of it, and the wall is invisible until you are already leaning on it.
What ReplDB actually is
The legacy Replit Database lives behind a secret environment variable, REPLIT_DB_URL, pointing at something like https://kv.replit.com/v0/<token>. That URL is the address and the credential at the same time, which is why it is in Secrets and not in your code. Talk to it over plain HTTP and it stores strings.
The ceilings are published and they are firm:
| Limit | Value |
|---|---|
| Store size | 50 MiB |
| Keys | 5,000 |
| Key length | 1,000 bytes |
| Value size | 5 MiB |
Values are plain strings. There are no TTLs, and there are no hashes, lists, or sets. If you have been storing JSON, you have been storing JSON.stringify output and parsing it back yourself, which works fine and is worth remembering when you read the migration section below.
The four ways apps outgrow it
Five thousand keys is a lot until keys are per-something. A key per user, per session, per uploaded file, per day of history: any of those turns 5,000 into a number you can see from where you are standing. The failure is not gradual either. You are fine, and then writes start failing, and the app that was working on Friday is not working on Monday because somebody shared it.
Nothing expires. This is the one I would fix first. Sessions, caches, rate-limit counters, password-reset tokens and email confirmations all want to disappear on their own. Without expiry you write a sweeper, and a sweeper against a 5,000-key store burns its budget listing keys just to find the dead ones. Every one of those cases is a single argument in Redis.
Everything is a string, so nothing is atomic. A view counter becomes read, parse, add one, stringify, write. Two requests land at the same time and one of the increments vanishes. A queue becomes a JSON array you rewrite in full. A leaderboard becomes a sort in application code over every key you can find. Redis has INCR, LPUSH, SADD and sorted sets precisely because these are the operations that go wrong when you build them out of strings.
The URL is the only door, and it rotates. There is no client, no CLI, no dashboard to open when something looks wrong. Debugging means echoing a secret in the Repl shell and reading raw HTTP responses. Nothing outside that Repl can reach the data conveniently: no cron on a VPS, no local script, no colleague with a GUI. That is a reasonable design for a store meant to serve one Repl, and it is a wall the day the data matters to anything else.
The move is one paste
Your app stays on Replit for this. Nothing about the deployment changes except which URL your code reads.
- Create a Valkey database on Layerbase Cloud. Redis works identically if you would rather have that name in your stack. Valkey sits on the free tier, so the landing spot is $0.
- Open the new database and go to its Migrate tab. Pick Replit.
- In the Repl shell, run
echo $REPLIT_DB_URLand copy what it prints. Do this now rather than reusing a value you saved earlier: the token rotates, and a stale URL is the single most common way this fails. - Paste it into the form, tick the acknowledgement, and click Import.
Every key is copied as a plain string SET. There are no TTLs on the source, so nothing has to be reconstructed, and the copy is exact. The source is read once and never modified, and the URL is used for the copy rather than stored.
Two rules the importer enforces that are worth knowing before you start:
- The target must be empty. If the Valkey database already holds keys, the import is refused rather than merged into. Create a fresh database for it.
- The copy is one directional. Nothing is written back to Replit, so your old store is still intact afterwards, which is your rollback.
I ran this end to end against a real Repl before writing it up. Eight keys, byte for byte identical on the other side, including a key literally named key with spaces (ReplDB URL-encodes those, and they come out with the spaces intact), keys with colons in them, unicode values, and stringified JSON. A store sitting at ReplDB's caps copies in seconds, because 50 MiB is not enough data for the copy to be slow.
If you also have a Postgres on Replit, that one moves separately and the whole flow is in Migrating from Replit to Layerbase. Both importers live at layerbase.com/migrate/replit.
What changes in your code
Not much, and that is the point. @replit/database is a get/set client, and those are GET/SET:
// Before
import Database from '@replit/database'
const db = new Database()
await db.set('user:42:prefs', JSON.stringify(prefs))// After
import { createClient } from 'redis'
const redis = createClient({ url: process.env.REDIS_URL })
await redis.connect()
await redis.set('user:42:prefs', JSON.stringify(prefs))
const prefs = JSON.parse((await redis.get('user:42:prefs')) ?? '{}')Set REDIS_URL in the Repl's Secrets to the connection string from the database's connection panel and redeploy. Check what your version of @replit/database returns from get as you go: older releases hand back the value directly and newer ones wrap it, and if your code already unwraps it, the Redis version needs one fewer step.
The reason to do this is not the copy, it is what is available afterwards. The expiry you could not have:
SET session:abc '{"user":42}' EX 3600The counter that stops losing increments under concurrency:
INCR views:post:17And a leaderboard that is a data structure rather than a sort you wrote:
ZADD leaderboard 4820 ada
ZREVRANGE leaderboard 0 9 WITHSCORESPlus pub/sub, hashes for objects you want to update one field at a time, and lists for the job queue you were about to build out of a JSON array. None of that is required on day one. It is there when you need it, which is the difference between a key-value store you grow into and one you grow out of.
When ReplDB is still fine
I would rather you kept it than moved for the sake of moving. Stay where you are if the store serves exactly one Repl and will die with it, if you are holding a few hundred keys and the count does not grow with your users, or if nothing in the app needs something to expire on a schedule. Zero configuration is a genuine feature, and a connection string plus a client is a real cost against it, small as it is.
The specific signals that the wall is close: you have written code to delete old keys, you have hit a write that failed for a reason you had to look up, you have simulated a counter or a queue on top of a string, or something outside the Repl now needs the same data. Any one of those and the move pays for itself the same afternoon.
After the copy
Leave the ReplDB store alone for a week. The import does not touch it, so it is still sitting there complete, and pointing your environment variable back is the cheapest rollback available to you. Once the app has run through a real weekend against Valkey, clear it out if you want to.
If you are still working out which of Replit's databases you are even holding, Replit database alternatives lays out all three and makes the honest case for staying on each. And if the store you care about is Postgres rather than keys, the migration guide covers the production and development paths, plus the driver question that catches Agent-built apps.
Keep reading
- Migrating from Replit to LayerbaseMove your Replit Postgres to Layerbase without moving your app. Production databases are a connection-string paste; development (Helium) databases need one command from the Repl shell. Includes the driver question and how to verify the copy landed.
- Migrating from Vercel KV to LayerbaseVercel KV was sunset and its stores moved to Upstash Redis, so the move is a data copy and a one-line client swap. Paste the rediss:// string behind your project Storage tab and copy every key, type, and TTL into flat-priced managed Redis or Valkey.
- Replit database alternatives: what you are actually runningThere are three different things called "the Replit database", they behave differently, and only one of them is reachable from outside your Repl. Here is what each one is, why usage billing is an awkward shape for a side project, and when the built-in database is the right call anyway.
- Vercel KV is gone: where your data went and how to move itVercel KV was sunset and every existing store was moved into Upstash Redis in December 2024. The @vercel/kv client on npm is formally deprecated and has not shipped a release since 2024. Here is how to find out what you are actually running, and how to move it onto managed Valkey.