GET use-cases:sessions-and-rate-limiting
Store each session under its own key with a TTL, rate-limit with INCR plus EXPIRE ... NX per client and window, and fill caches under a SET ... NX PX lock. On KVNest's free Starter plan, set the eviction policy to noeviction first, or the cache policy can silently drop sessions.
On this page
Give every session its own key and a TTL, count requests with INCR and EXPIRE ... NX, and guard cache refills with SET ... NX PX. Each is one or two commands and works the same on Valkey and on Redis. Everything below is Node.js 18 or later and reads VALKEY_URL, the rediss:// URL from your instance page.
Before you store sessions on Starter: its eviction policy is allkeys-lru, which deletes the least recently used keys under memory pressure, sessions included. Set it to noeviction on the instance's Settings tab or with the configure_instance tool; changing it restarts the instance. With noeviction, writes fail once the 192 MB of data is full, so catch the out-of-memory error. Starter also restarts empty, because nothing is written to disk.
Connect once
The Node.js guide has the full client setup. Open one client per process, not per request. KVNest closes idle connections after five minutes, and iovalkey and redis both reconnect on their own.
Sessions
Nuxt and Nitro
Nitro's storage layer can mount the unstorage Redis driver, which needs ioredis installed (npm install ioredis). The driver's ttl option (seconds) expires each key, and writing an item again resets it, which gives you a sliding expiry.
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
storage: {
sessions: {
driver: "redis",
url: process.env.VALKEY_URL,
base: "sessions",
ttl: 60 * 60,
},
},
},
});
// server/utils/session.ts
import { randomUUID } from "node:crypto";
import type { H3Event } from "h3";
export async function useAppSession(event: H3Event) {
const store = useStorage("sessions");
let id = getCookie(event, "sid");
let data = id ? await store.getItem<Record<string, unknown>>(id) : null;
if (!id || !data) {
id = randomUUID();
data = {};
}
setCookie(event, "sid", id, { httpOnly: true, secure: true, sameSite: "lax", maxAge: 60 * 60 });
return {
data,
save: () => store.setItem(id!, data),
destroy: () => store.removeItem(id!),
};
}
Express
connect-redis stores each session as a JSON string under sess:<id>. Pass ttl so the store expires keys, and rolling: true to renew the cookie on each response.
import express from "express";
import session from "express-session";
import { RedisStore } from "connect-redis";
import { createClient } from "redis";
const client = createClient({ url: process.env.VALKEY_URL });
client.on("error", (err) => console.error("Redis client error", err));
await client.connect();
const app = express();
app.set("trust proxy", 1); // behind a proxy that terminates TLS
app.use(session({
store: new RedisStore({ client, prefix: "sess:", ttl: 3600 }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
rolling: true,
cookie: { secure: true, httpOnly: true, sameSite: "lax", maxAge: 3600 * 1000 },
}));
app.get("/", (req, res) => {
req.session.views = (req.session.views ?? 0) + 1;
res.send("Views: " + req.session.views);
});
app.listen(3000);
Rate limiting
Fixed window
INCR creates the key at 1 on the first request. EXPIRE ... NX sets the TTL only when none exists, so later requests do not extend the window. Both run in one transaction.
import Valkey from "iovalkey";
const client = new Valkey(process.env.VALKEY_URL);
export async function fixedWindow(id, limit, windowSec) {
const key = `rl:${id}`;
const results = await client.multi().incr(key).expire(key, windowSec, "NX").exec();
const count = results[0][1];
return { allowed: count <= limit, remaining: Math.max(0, limit - count) };
}
// await fixedWindow(`login:${ip}`, 5, 60)
A client can send limit requests at the end of one window and limit more at the start of the next.
Sliding window
A sorted set holds one member per request, scored by timestamp. Trim old entries, add this request, count. Rejected requests still count, so a client that keeps hammering stays blocked.
import { randomUUID } from "node:crypto";
export async function slidingWindow(id, limit, windowMs) {
const key = `rl:sw:${id}`;
const now = Date.now();
const results = await client
.multi()
.zremrangebyscore(key, 0, now - windowMs)
.zadd(key, now, `${now}-${randomUUID()}`)
.zcard(key)
.pexpire(key, windowMs)
.exec();
return { allowed: results[2][1] <= limit };
}
Memory is one entry per request in the window, so use it for low limits such as login attempts, not for 10,000 requests a minute.
Stampede-safe caching
When a hot key expires, many requests recompute it at once. Let one caller take a lock with SET ... NX PX; the others poll. The lock expires by itself if its owner crashes, and a token keeps one caller from releasing another's lock.
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const unlock = `if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) end return 0`;
export async function cached(key, ttlMs, compute) {
for (let attempt = 0; attempt < 50; attempt++) {
const hit = await client.get(key);
if (hit !== null) return JSON.parse(hit);
const token = randomUUID();
if (await client.set(`lock:${key}`, token, "PX", 10_000, "NX") === "OK") {
try {
const value = await compute();
await client.set(key, JSON.stringify(value), "PX", ttlMs);
return value;
} finally {
await client.eval(unlock, 1, `lock:${key}`, token);
}
}
await sleep(100);
}
throw new Error("cache fill timed out");
}
A cache can use Starter's default allkeys-lru. A shared instance for sessions and caches cannot have both policies, so use one instance for each.
Lock it down
Create a read-write user limited to the key patterns sess:*, sessions:* and rl:*, and give the app that user's URL instead of the default one. If your host has fixed egress IPs, add them as an IP allowlist. Both are on the instance page or in the MCP tools, where an agent such as Claude Code can create the instance for you.
Where KVNest falls short
Starter is free during the preview, with 256 MiB of memory, and 192 MB of that is usable. It runs on Hetzner in Germany. See pricing and the EU managed Redis and Valkey comparison for the alternatives.
FAQ
Can I store sessions on a free managed Redis or Valkey instance?
Yes, with two caveats on KVNest Starter. Switch the eviction policy from allkeys-lru to noeviction so sessions are not evicted under memory pressure, and accept that Starter writes nothing to disk, so a restart logs everyone out.
Fixed window or sliding window?
A fixed window is two commands and fine for login and signup limits, but a client can send up to twice the limit across a window boundary. A sorted-set sliding window is fairer and costs one entry per request.
Does this work on Vercel or Netlify?
On serverless functions that run on the Node.js runtime, yes: open the connection at module scope and use a rediss:// URL. Edge runtimes cannot open raw TCP connections, so these clients do not run there.
How many sessions fit in 256 MiB?
Starter allows 192 MB of data. At about 1 KB per session that is roughly 190,000 sessions before other keys; measure your own average with MEMORY USAGE on a real session key.
Sources
All 3 sources
- [1]EXPIRE command (NX option), Valkey documentation (valkey.io), checked 5 Nov 2026.
- [2]SET command (NX, PX), Valkey documentation (valkey.io), checked 5 Nov 2026.
- [3]unstorage Redis driver (unstorage.unjs.io), checked 5 Nov 2026.
Read next
Memory and a semantic cache for AI agents on Valkey or Redis. One Valkey or Redis instance can hold an agent's chat history as JSON, its long-term memory as an HNSW vector index, and a semantic cache that skips the LLM call for a question already answered. On KVNest this needs the JSON, Bloom and Search modules, which you turn on per instance.
EU managed Redis & Valkey: 11 providers compared (Nov 2026). Seven of the 11 providers keep both the company and the data in the EU: Aiven, OVHcloud, Scaleway, Clever Cloud, UpCloud, Stackhero and KVNest. UpCloud is the cheapest with data on disk, at €8 a month for 1 GB. As of 4 November 2026, KVNest is the only one of the 11 that combines a free tier, a hosted MCP server and a choice of Redis or Valkey.
Try KVNest: free managed Redis or Valkey
A free Starter instance in Germany during the public preview, no card needed. Redis 8.10 or Valkey 9.1, your pick.