Build a Rate Limiter and Leaderboard with Redis
Short version: install the Layerbase CLI and run lbase create redis-features -e redis --start for a local Redis, then point REDIS_URL at what lbase url redis-features prints. The script below builds both a fixed-window rate limiter and a sorted-set leaderboard against it. When you want a hosted one, Redis is available on the Layerbase Cloud free plan.
A rate limiter and a leaderboard both need frequent, atomic updates. They can be built in a relational database, but Redis has primitives designed for exactly these access patterns: counters with expiration and sorted sets.
The implementation details still matter. A counter and its expiration must be created atomically, and an in-memory data store needs an explicit durability and failure policy.
Who this is for: TypeScript developers evaluating Redis for request-path counters and frequently updated rankings.
Outcome: One runnable script with an atomic fixed-window rate limiter and a sorted-set leaderboard.
Time: About 15 minutes, plus the first Redis binary download.
Prerequisites: Node.js 20 or newer and pnpm. No Docker or Cloud account is required.
Start Redis locally
Install the Layerbase CLI, then create a Redis instance:
npm i -g layerbase
lbase create redis-features -e redis --start
lbase url redis-featuresThe last command prints the actual connection string:
redis://127.0.0.1:6379/0If that port is occupied, use the URL printed by the CLI.
Create the TypeScript project:
mkdir redis-features
cd redis-features
pnpm init
pnpm add redis
pnpm add -D tsx typescriptBuild both features
Create redis-features.ts:
import { createClient } from 'redis'
function createRedisClient() {
const rawUrl = process.env.REDIS_URL ?? 'redis://127.0.0.1:6379'
const parsed = new URL(rawUrl)
if (parsed.protocol !== 'rediss:') {
return createClient({ url: rawUrl })
}
return createClient({
username: decodeURIComponent(parsed.username || 'default'),
password: decodeURIComponent(parsed.password),
socket: {
host: parsed.hostname,
port: Number(parsed.port || 6379),
tls: true,
servername: parsed.hostname,
},
})
}
const client = createRedisClient()
client.on('error', (error) => console.error('Redis error:', error))
await client.connect()
const RATE_KEY = 'tutorial:rate:user-42'
const LEADERBOARD = 'tutorial:leaderboard'
await client.del(RATE_KEY)
await client.del(LEADERBOARD)
const fixedWindowScript = `
local count = redis.call('INCR', KEYS[1])
if count == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return count
`
async function checkRateLimit(
key: string,
limit: number,
windowSeconds: number,
): Promise<{ allowed: boolean; remaining: number }> {
const reply = await client.eval(fixedWindowScript, {
keys: [key],
arguments: [String(windowSeconds)],
})
const count = Number(reply)
return {
allowed: count <= limit,
remaining: Math.max(0, limit - count),
}
}
console.log('Rate limiter:')
for (let request = 1; request <= 7; request += 1) {
const result = await checkRateLimit(RATE_KEY, 5, 60)
const status = result.allowed ? 'ALLOWED' : 'BLOCKED'
console.log(
` Request ${request}: ${status} (${result.remaining} remaining)`,
)
}
await client.zAdd(LEADERBOARD, [
{ value: 'alice', score: 2500 },
{ value: 'bob', score: 1800 },
{ value: 'charlie', score: 3200 },
{ value: 'grace', score: 3400 },
{ value: 'eve', score: 2700 },
])
await client.zIncrBy(LEADERBOARD, 500, 'alice')
await client.zIncrBy(LEADERBOARD, 1100, 'bob')
const leaders = await client.zRangeWithScores(
LEADERBOARD,
0,
4,
{ REV: true },
)
console.log('\nLeaderboard:')
leaders.forEach((entry, index) => {
console.log(` #${index + 1} ${entry.value}: ${entry.score}`)
})
await client.quit()The Lua script makes INCR and the first EXPIRE one atomic operation. A process failure cannot leave the counter without a TTL between those two commands.
Run the script with the connection string returned by the CLI:
REDIS_URL="$(lbase url redis-features)" pnpm tsx redis-features.tsExpected output:
Rate limiter:
Request 1: ALLOWED (4 remaining)
Request 2: ALLOWED (3 remaining)
Request 3: ALLOWED (2 remaining)
Request 4: ALLOWED (1 remaining)
Request 5: ALLOWED (0 remaining)
Request 6: BLOCKED (0 remaining)
Request 7: BLOCKED (0 remaining)
Leaderboard:
#1 grace: 3400
#2 charlie: 3200
#3 alice: 3000
#4 bob: 2900
#5 eve: 2700Both key names start with tutorial: and are deleted at the beginning of the run. The script does not flush the database or touch unrelated keys.
Know what this limiter guarantees
This is a fixed window that starts with the first request. It is not a sliding-window limiter. A client can send five requests just before the key expires and another five immediately after it expires.
That boundary burst is acceptable for many product limits and abuse controls. For stricter traffic shaping, use a token bucket or a sliding log built with a sorted set and an atomic script.
Decide what happens when Redis is unavailable:
- Fail open if serving the request is safer than blocking a legitimate user.
- Fail closed if exceeding the limit creates a security or financial risk.
- Use a small local fallback if temporary inaccuracy is acceptable.
The correct choice depends on what the limit protects. Hiding the failure behind a default true or false is not a reliability strategy.
Understand leaderboard durability
Sorted sets keep members ordered by score, which makes rank and top-N reads straightforward. They do not make Redis a permanent system of record by themselves.
Review the server's persistence, replication, and eviction configuration before storing irreplaceable scores. A common design writes durable game events or final scores to a primary database and uses Redis as the fast ranking view. If the Redis data is lost, the leaderboard can be rebuilt.
Redis lists can also implement a simple destructive work queue, but LPUSH plus BRPOP does not provide acknowledgments. Use Redis Streams or a queue system with explicit retry and dead-letter behavior when jobs must survive worker failure.
Move the proven workflow to Layerbase Cloud
After the local behavior is correct, create Redis on Layerbase Cloud and copy its rediss:// connection string from Quick Connect:
REDIS_URL="rediss://default:password@your-host.cloud.layerbase.dev:6379" \
pnpm tsx redis-features.tsThe script parses the TLS URL and sends the database hostname as the TLS server name. That is required because Redis traffic on Layerbase Cloud is routed by hostname over the shared TLS port.
Redis is available on the Free plan. Free includes up to two databases and sleeps databases after 15 minutes of inactivity. It can wake on connect, which is useful for evaluation and low-traffic projects. A rate limiter in a latency-sensitive request path should use an always-on paid configuration instead of depending on a sleeping database. Check current pricing before making a purchasing decision.
Keep the password in server-side environment variables. Do not expose a write-capable Redis connection to browser code.
When Redis is the wrong fit
Redis is useful when the access pattern matches its data structures and the team has a clear recovery policy. It is usually the wrong first choice when:
- An indexed query in the primary database already meets the latency target.
- The data must survive without a separate durable source or tested backup.
- The team does not want to operate another stateful service.
- The workload needs relational joins or ad-hoc reporting.
If licensing, migration, or long-term project governance is part of the decision, read Redis versus Valkey before choosing. The next tutorial, building a cache and session store with Valkey, covers a different outcome without repeating this rate limiter.
The Layerbase CLI is the fastest local path. Layerbase Desktop provides the same workflow in a GUI on macOS, Windows, and Linux. Move to Cloud after the failure and durability policies are explicit.
FAQ
Is Redis free on Layerbase Cloud?
Yes, Redis is one of the free-plan engines. Free covers up to two databases, and they sleep after 15 idle minutes and wake on the next connection. That is fine for evaluation and low-traffic work, but a rate limiter sitting in a latency-sensitive request path wants an always-on paid configuration instead.
What connection string does a cloud Redis database use?
A rediss:// URL from Quick Connect, TLS included. The script parses that URL and sends the database hostname as the TLS server name, which is required because Redis traffic on Layerbase Cloud is routed by hostname over a shared TLS port. Keep the password in a server-side environment variable.
Can I run Redis locally with the CLI?
Yes, and it is the fastest way to work through this post. lbase create redis-features -e redis --start downloads the Redis binary for your platform and starts it, with no Docker and no Cloud account.
Is a Redis leaderboard durable enough to be the system of record?
Not on its own. Sorted sets give you ranks and top-N reads cheaply, but durability depends on the persistence, replication, and eviction settings of the server you run. The usual design writes the authoritative events or final scores to a primary database and treats Redis as a fast ranking view that can be rebuilt.
Keep reading
- Add Redis or Valkey caching to a Lovable appLovable ships a browser SPA with no server, so a cache lives in a Supabase Edge Function, not your React code. Here is how to add a Redis-compatible Valkey the right way.
- Northflank alternatives: a preview environment is not a database branchNorthflank gives every pull request its own stack, and the database in that stack starts empty unless you seed it or restore it from a backup you already had. Here is exactly how their forks work, what a copy-on-write branch does differently, what each one costs while a PR sits open, and the cases where Northflank is the right answer.
- Redis Cloud alternatives: pay for the modules, or stop paying for themRedis Cloud prices itself around the module story: search, JSON, time series, vector sets, Active-Active. If you use those, the bill is buying something real. If you use Redis as a cache, a session store, or a queue, you are paying the module premium for a key-value store. Here is what each tier buys, where a flat plan fits, and when to stay.
- Build the Redis backend for a WhatsApp botConversation state with TTLs, BullMQ queues for scheduled reminders, idempotency keys for retried webhooks, and per-recipient rate limits, all on one Redis-compatible Valkey.