Vercel Edge Config vs KV vs Blob
Vercel's three storage options serve different needs—feature flags, sessions, and files. Here's when to use each.
Vercel offers three storage options, each optimized for different use cases. Pick the wrong one and you're either overpaying for simple feature flags or hitting rate limits on user uploads. Here's the decision matrix for infrastructure and architecture decisions.
Edge Config: Low-Latency Global Reads
Edge Config is a distributed key-value store designed for frequent reads and infrequent writes. Think feature flags, A/B testing configurations, redirects—data that stays relatively static but needs to be accessible globally with minimal latency.
// Feature flag in Edge Config
import { get } from '@vercel/edge-config';
export async function middleware(request: NextRequest) {
const config = await get();
const betaEnabled = config?.features?.beta ?? false;
if (!betaEnabled && request.nextUrl.pathname.startsWith('/beta')) {
return NextResponse.redirect(new URL('/', request.url));
}
}- Read latency: <15ms at P99, often <1ms
- Works in: Middleware and Edge Functions only
- Write latency: Varies (not real-time)
- No redeploy needed: Update via API or Dashboard
The killer feature: reads don't block. Config is pushed to edge locations proactively, so every edge location has local access. Use Edge Config when you need millisecond-level read performance and can tolerate eventual consistency on writes.
Redis (formerly KV): Fast Key-Value Storage
Vercel KV has been migrated to Upstash Redis via the Marketplace. Use Redis for ephemeral data, caching layers, rate limiting, and anything that needs fast reads with immediate write consistency.
// Rate limiting with Redis
import { Redis } from '@upstash/redis';
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
});
export async function rateLimit(ip: string) {
const key = `rate-limit:${ip}`;
const count = await redis.incr(key);
if (count === 1) {
await redis.expire(key, 60); // 1 minute window
}
if (count > 10) {
throw new Error('Rate limit exceeded');
}
}Redis shines for per-user or per-request data that doesn't make sense in Edge Config:
- Session data: Temporary tokens, active sessions
- Counters: Rate limiting, analytics, usage tracking
- Caching: Database query results, expensive computations
- Pub/Sub: Real-time notifications, WebSocket alternatives
Unlike Edge Config, Redis writes are immediate. You get read-after-write consistency, making it suitable for data that updates frequently and must reflect changes instantly.
Blob: Object Storage for Files
Blob is for files—images, videos, PDFs, any binary data. Think S3 replacement with Vercel-native integration. Upload at build time or runtime, serve through CDN, pay based on storage averages and data transfer.
// Server-side upload
import { put } from '@vercel/blob';
const blob = await put('avatars/user-123.jpg', file, {
access: 'public', // or 'private' for authenticated reads
});
// Direct client-side upload (no Function bandwidth)
import { upload } from '@vercel/blob/client';
const { url } = await upload('profile.png', file, {
access: 'public',
handleUploadUrl: '/api/upload/auth',
});Blob isn't for key-value data—it's for actual files stored as objects. The distinction matters:
- Public blobs: Anyone with URL can access (CDN-delivered)
- Private blobs: Authenticated reads via your Function
- Client uploads: Direct browser-to-Blob, bypassing your Function
- CDN caching: Up to 1 month by default
Use Blob for user uploads, static assets that aren't part of your git repo, media files, document storage—anything where you'd traditionally use S3.
The Comparison Table
| Feature | Edge Config | Redis | Blob |
|---|---|---|---|
| Use case | Feature flags, A/B tests, redirects | Caching, sessions, rate limiting | File storage (images, videos) |
| Read latency | <15ms P99 (edge-local) | ~10-50ms (depends on region) | CDN-cached (similar to static) |
| Write latency | Varies (eventual consistency) | Immediate | ~100-500ms upload |
| Works in | Middleware, Edge Functions | All runtimes | All runtimes, client-side |
| Data type | JSON-like (up to 64KB/item) | Strings, numbers, JSON | Binary files (any size) |
| Billing | Per read/write operation | Per operation + storage | Storage + data transfer |
Common Mistake: Using Blob for Config
// ❌ Wrong: Blob is for files, not config
const config = await fetch('https://blob.vercel-storage.com/config.json')
.then(r => r.json());
// ✅ Right: Edge Config is for config
const config = await get();
// ✅ Right: Redis for dynamic config that needs fast writes
const config = await redis.hgetall('app-config');Fetching a JSON file from Blob on every request adds latency and data transfer costs. Edge Config is purpose-built for this—use it.
Common Mistake: Edge Config for Session Data
// ❌ Wrong: Edge Config has item size limits (64KB)
await edgeConfig.set('session-123', { userData: hugeObject });
// ✅ Right: Redis handles large payloads efficiently
await redis.hset('session-123', 'data', JSON.stringify(userData));Edge Config items are capped at 64KB. Sessions, user profiles, cart data—anything per-user that might exceed this limit belongs in Redis.
Common Mistake: Redis for Feature Flags
// ❌ Wrong: Feature flags need ultra-low latency everywhere
const flag = await redis.get('feature.beta');
// ✅ Right: Edge Config is pre-distributed globally
const config = await get();
const flag = config?.features?.beta ?? false;In Middleware running at the edge, a round-trip to Redis can take 50-100ms. Edge Config reads are <15ms. For feature flags that must evaluate instantly at the edge, use Edge Config.
Decision Flowchart
// Storing data? Ask yourself:
1. Is it a file (image, video, PDF)?
→ YES → Use Blob
2. Is it user-specific, frequently updated, or needs immediate writes?
→ YES → Use Redis
3. Is it global config, feature flags, or A/B test segments?
→ YES → Use Edge Config
// If still unsure:
// - Need <15ms global reads for Middleware → Edge Config
// - Need session storage or rate limiting → Redis
// - Need to store user uploads or media → BlobCombining Them: The Real-World Pattern
Production apps often use all three:
// Middleware: Use Edge Config for feature flags (ultra-fast)
export async function middleware(request: NextRequest) {
const config = await get();
if (!config?.features?.newLayout) return NextResponse.next();
// Redis: Check rate limit
const ip = request.ip ?? '127.0.0.1';
if (await isRateLimited(ip)) {
return NextResponse.redirect(new URL('/rate-limited', request.url));
}
return NextResponse.next();
}
// Server Component: Cache expensive DB queries in Redis
async function getUserData(userId: string) {
const cached = await redis.get(`user:${userId}`);
if (cached) return JSON.parse(cached);
const data = await db.users.findUnique({ where: { id: userId } });
await redis.setex(`user:${userId}`, 3600, JSON.stringify(data));
return data;
}
// API Route: Handle user upload to Blob
export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get('file') as File;
const blob = await put(`uploads/${crypto.randomUUID()}.png`, file, {
access: 'public',
});
return Response.json({ url: blob.url });
}Each tool has a purpose. Don't default to one and shoehorn everything into it. Edge Config for global reads, Redis for dynamic data, Blob for files.
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement