unstorage vs keyv vs cache-manager: Universal Key-Value Storage in Node.js (2026)
TL;DR
unstorage (by UnJS) is the modern universal storage layer — driver-based API supporting memory, filesystem, Redis, Cloudflare KV, Vercel KV, HTTP, and more. keyv is the simple key-value store with TTL — adapters for Redis, Postgres, SQLite, MongoDB, and other backends. cache-manager is the caching-focused library — multi-level caches (memory → Redis), TTL, tag-based invalidation, and wrapping. In 2026: unstorage for multi-driver storage in modern frameworks (Nuxt, Nitro), keyv for simple persistent key-value needs, cache-manager for multi-tier caching strategies.
Key Takeaways
- unstorage: ~3M weekly downloads — UnJS ecosystem, powers Nuxt/Nitro storage, 20+ drivers
- keyv: ~10M weekly downloads — simple
get/set/deletewith TTL, adapter-based backends - cache-manager: ~1M weekly downloads — multi-tier caching, wrap pattern, tag invalidation
- unstorage supports
getItem,setItem,getKeys,watch— filesystem-like API - keyv is the simplest —
await kv.set("key", value, ttl)/await kv.get("key") - cache-manager's
wrap()pattern: check cache → miss → compute → store → return
unstorage
unstorage — universal storage layer:
Basic usage
import { createStorage } from "unstorage"
// In-memory (default):
const storage = createStorage()
await storage.setItem("package:react", {
name: "react",
downloads: 5_000_000,
score: 92.5,
})
const pkg = await storage.getItem("package:react")
// → { name: "react", downloads: 5000000, score: 92.5 }
// List keys:
const keys = await storage.getKeys("package:")
// → ["package:react", "package:vue", ...]
// Check existence:
const exists = await storage.hasItem("package:react") // true
// Delete:
await storage.removeItem("package:react")
Drivers
import { createStorage } from "unstorage"
import fsDriver from "unstorage/drivers/fs"
import redisDriver from "unstorage/drivers/redis"
import cloudflareKVBindingDriver from "unstorage/drivers/cloudflare-kv-binding"
// Filesystem:
const fsStorage = createStorage({
driver: fsDriver({ base: "./data" }),
})
// Redis:
const redisStorage = createStorage({
driver: redisDriver({
url: process.env.REDIS_URL,
ttl: 3600, // Default TTL: 1 hour
}),
})
// Cloudflare KV (in Workers):
const kvStorage = createStorage({
driver: cloudflareKVBindingDriver({ binding: "MY_KV" }),
})
Mount points (multi-driver)
import { createStorage } from "unstorage"
import memoryDriver from "unstorage/drivers/memory"
import redisDriver from "unstorage/drivers/redis"
import fsDriver from "unstorage/drivers/fs"
const storage = createStorage()
// Mount different drivers at different paths:
storage.mount("cache:", redisDriver({ url: process.env.REDIS_URL }))
storage.mount("data:", fsDriver({ base: "./data" }))
storage.mount("temp:", memoryDriver())
// Access transparently:
await storage.setItem("cache:package:react", { score: 92 }) // → Redis
await storage.setItem("data:config", { theme: "dark" }) // → Filesystem
await storage.setItem("temp:session:abc", { userId: 123 }) // → Memory
Watch for changes
// Watch for storage changes:
const unwatch = await storage.watch((event, key) => {
console.log(`${event}: ${key}`)
// "update:package:react"
// "remove:package:vue"
})
keyv
keyv — simple key-value with TTL:
Basic usage
import Keyv from "keyv"
// In-memory (default):
const kv = new Keyv()
// Set with TTL (milliseconds):
await kv.set("package:react", { name: "react", score: 92 }, 3600_000) // 1 hour
// Get:
const pkg = await kv.get("package:react")
// → { name: "react", score: 92 } (or undefined if expired)
// Delete:
await kv.delete("package:react")
// Clear all:
await kv.clear()
// Check if key exists:
const exists = await kv.has("package:react")
Storage adapters
import Keyv from "keyv"
// Redis:
const kv = new Keyv("redis://localhost:6379")
// PostgreSQL:
const kv = new Keyv("postgresql://user:pass@localhost:5432/db")
// SQLite:
const kv = new Keyv("sqlite://./data/cache.sqlite")
// MongoDB:
const kv = new Keyv("mongodb://localhost:27017/cache")
// With options:
const kv = new Keyv({
uri: "redis://localhost:6379",
namespace: "packages", // Key prefix: "packages:key"
ttl: 3600_000, // Default TTL: 1 hour
serialize: JSON.stringify,
deserialize: JSON.parse,
})
Namespaces
// Separate data by namespace:
const packageCache = new Keyv({ namespace: "packages" })
const userCache = new Keyv({ namespace: "users" })
const sessionStore = new Keyv({ namespace: "sessions", ttl: 86400_000 })
await packageCache.set("react", { score: 92 })
await userCache.set("user-123", { name: "Royce" })
// Keys in Redis:
// "packages:react"
// "users:user-123"
// No collision between namespaces
As HTTP cache
import Keyv from "keyv"
import KeyvRedis from "@keyv/redis"
import http from "node:http"
const cache = new Keyv({ store: new KeyvRedis("redis://localhost:6379") })
// Cache-aside pattern:
async function getCachedPackage(name: string) {
const cached = await cache.get(`pkg:${name}`)
if (cached) return cached
const data = await fetchFromNpm(name)
await cache.set(`pkg:${name}`, data, 300_000) // Cache 5 minutes
return data
}
cache-manager
cache-manager — multi-tier caching:
Basic setup
import { caching } from "cache-manager"
// In-memory cache:
const cache = await caching("memory", {
max: 1000, // Max 1000 items
ttl: 60 * 1000, // Default TTL: 60 seconds
})
// Set / Get:
await cache.set("package:react", { name: "react", score: 92 })
const pkg = await cache.get("package:react")
// Delete:
await cache.del("package:react")
// Reset:
await cache.reset()
The wrap() pattern (cache-aside)
import { caching } from "cache-manager"
const cache = await caching("memory", { ttl: 300_000 }) // 5 min TTL
// wrap() = check cache → miss → compute → store → return:
async function getPackageScore(name: string): Promise<number> {
return cache.wrap(`score:${name}`, async () => {
// This only runs on cache miss:
console.log(`Cache miss — computing score for ${name}`)
const data = await fetchFromNpm(name)
return calculateScore(data)
})
}
// First call: cache miss → fetches and computes → stores → returns
await getPackageScore("react") // ~500ms
// Second call: cache hit → returns immediately
await getPackageScore("react") // ~1ms
Multi-tier caching (memory → Redis)
import { caching, multiCaching } from "cache-manager"
import { redisStore } from "cache-manager-ioredis-yet"
// Tier 1: Memory (fast, limited):
const memoryCache = await caching("memory", {
max: 500,
ttl: 30_000, // 30 seconds
})
// Tier 2: Redis (slower, larger):
const redisCache = await caching(redisStore, {
host: "localhost",
port: 6379,
ttl: 300_000, // 5 minutes
})
// Multi-tier: checks memory first, then Redis:
const cache = multiCaching([memoryCache, redisCache])
// Read: memory → Redis → compute
// Write: stores in BOTH tiers
const score = await cache.wrap("score:react", async () => {
return calculateScore("react")
})
Tag-based invalidation
import { caching } from "cache-manager"
const cache = await caching("memory", { ttl: 600_000 })
// Store with tags:
await cache.set("package:react", data, { tags: ["packages", "frontend"] })
await cache.set("package:vue", data, { tags: ["packages", "frontend"] })
await cache.set("package:express", data, { tags: ["packages", "backend"] })
// Invalidate all "frontend" packages:
await cache.store.tags?.invalidate(["frontend"])
// react and vue caches cleared, express still cached
Feature Comparison
| Feature | unstorage | keyv | cache-manager |
|---|---|---|---|
| Multi-driver | ✅ (20+ drivers) | ✅ (adapters) | ✅ (stores) |
| TTL support | ✅ (driver-level) | ✅ | ✅ |
| Namespaces | ✅ (mount points) | ✅ | ❌ |
| Multi-tier cache | ❌ | ❌ | ✅ |
| wrap() pattern | ❌ | ❌ | ✅ |
| Tag invalidation | ❌ | ❌ | ✅ |
| Watch/subscribe | ✅ | ❌ | ❌ |
| Edge runtime | ✅ (CF KV, Vercel) | ❌ | ❌ |
| TypeScript | ✅ | ✅ | ✅ |
| Weekly downloads | ~3M | ~10M | ~1M |
When to Use Each
Choose unstorage if:
- Building with Nuxt/Nitro — unstorage is the native storage layer
- Need multiple storage backends mounted at different paths
- Edge runtime support (Cloudflare KV, Vercel KV, Deno KV)
- Want filesystem-like API (
getItem,setItem,getKeys)
Choose keyv if:
- Simple key-value storage with TTL — minimal API
- Need namespaced data across Redis, Postgres, SQLite, or MongoDB
- Lightweight — zero config for in-memory, one URL for backends
- Just need get/set/delete with expiration
Choose cache-manager if:
- Multi-tier caching strategy (memory → Redis → database)
- The
wrap()pattern is your primary caching pattern - Need tag-based cache invalidation
- Building an API that benefits from layered cache architecture
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on unstorage v1.x, keyv v5.x, and cache-manager v5.x.
Compare caching, storage, and key-value packages on PkgPulse →