Upstash - Kompletny Przewodnik po Serverless Data Platform
Czym jest Upstash?
Upstash to serverless data platform oferująca Redis, Kafka i QStash z modelem pay-per-request. W przeciwieństwie do tradycyjnych rozwiązań, gdzie płacisz za działający serwer 24/7, w Upstash płacisz tylko za faktyczne użycie. To sprawia, że Upstash jest idealny dla aplikacji serverless i edge computing, gdzie zasoby są przydzielane dynamicznie.
Założony w 2020 roku, Upstash szybko zyskał popularność wśród deweloperów budujących aplikacje na Vercel, Cloudflare Workers, AWS Lambda i innych platformach serverless. Kluczową zaletą jest globalny edge network z replikami w 30+ lokalizacjach, zapewniający ultra-niskie latencje na całym świecie.
Upstash oferuje trzy główne produkty:
- Upstash Redis - Serverless Redis z REST API
- Upstash Kafka - Serverless Apache Kafka
- QStash - HTTP-based message queue z scheduled delivery
Dlaczego Upstash?
Kluczowe zalety
- Pay-per-request - Płacisz tylko za użycie, zero kosztów przy braku ruchu
- Edge-native - Globalna dystrybucja z 30+ lokalizacjami
- REST API - Działa wszędzie, nawet w Edge Functions
- Zero maintenance - Brak zarządzania infrastrukturą
- Durable storage - Dane są replikowane i trwałe
- Generous free tier - 10K requests/day za darmo
- Native integrations - Vercel, Cloudflare, Fly.io
Upstash vs Redis Cloud vs Amazon ElastiCache
| Cecha | Upstash | Redis Cloud | ElastiCache |
|---|---|---|---|
| Pricing model | Per-request | Per-hour | Per-hour |
| Free tier | 10K/day | 30MB | Brak |
| Edge locations | 30+ | 10+ | AWS regions |
| REST API | ✅ Native | ❌ TCP only | ❌ TCP only |
| Serverless | ✅ True | Częściowo | ❌ Nie |
| Min. cost | $0 | ~$5/mo | ~$12/mo |
| Auto-scaling | ✅ Automatyczny | Ręczny | Ręczny |
| Edge compatible | ✅ Tak | ❌ Nie | ❌ Nie |
Upstash Redis
Instalacja
# SDK for JavaScript/TypeScript
npm install @upstash/redis
# SDK for Python
pip install upstash-redisKonfiguracja
// lib/redis.ts
import { Redis } from '@upstash/redis'
// Opcja 1: Z environment variables
export const redis = Redis.fromEnv()
// Opcja 2: Z explicit config
export const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})
// Opcja 3: Dla Edge Runtime
export const redis = new Redis({
url: 'https://xxx.upstash.io',
token: 'AXxxxx',
automaticDeserialization: true, // Automatyczny JSON parse
})# .env.local
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=AXxxxxPodstawowe operacje
import { redis } from '@/lib/redis'
// STRING operations
await redis.set('user:1:name', 'John Doe')
const name = await redis.get('user:1:name') // "John Doe"
// SET z TTL (expiration)
await redis.set('session:abc123', { userId: 1 }, { ex: 3600 }) // 1 hour
// SET z warunkami
await redis.set('lock:resource', 'locked', { nx: true }) // Tylko jeśli nie istnieje
await redis.set('config:version', '2.0', { xx: true }) // Tylko jeśli istnieje
// GET z typowaniem
interface User {
id: number
name: string
email: string
}
const user = await redis.get<User>('user:1') // Typed!
// MGET/MSET - multiple keys
await redis.mset({
'key1': 'value1',
'key2': 'value2',
'key3': 'value3',
})
const values = await redis.mget('key1', 'key2', 'key3')
// Increment/Decrement
await redis.incr('page:views')
await redis.incrby('user:1:points', 10)
await redis.decr('inventory:item:123')
// Append
await redis.append('log:today', '\n2024-01-15: User logged in')
// String length
const length = await redis.strlen('user:1:bio')Hash operations
// HSET - ustaw pola hash
await redis.hset('user:1', {
name: 'John Doe',
email: 'john@example.com',
age: 30,
verified: true,
})
// HGET - pobierz pojedyncze pole
const email = await redis.hget('user:1', 'email')
// HMGET - pobierz wiele pól
const [name, email] = await redis.hmget('user:1', 'name', 'email')
// HGETALL - pobierz cały hash
const user = await redis.hgetall<User>('user:1')
// HINCRBY - zwiększ wartość liczbową
await redis.hincrby('user:1', 'age', 1)
// HDEL - usuń pole
await redis.hdel('user:1', 'temporary_field')
// HEXISTS - sprawdź czy pole istnieje
const hasEmail = await redis.hexists('user:1', 'email')
// HKEYS/HVALS - klucze i wartości
const keys = await redis.hkeys('user:1')
const values = await redis.hvals('user:1')List operations
// LPUSH/RPUSH - dodaj elementy
await redis.lpush('queue:tasks', 'task1', 'task2')
await redis.rpush('queue:tasks', 'task3')
// LPOP/RPOP - pobierz i usuń element
const task = await redis.lpop('queue:tasks')
const lastTask = await redis.rpop('queue:tasks')
// LRANGE - pobierz zakres
const tasks = await redis.lrange('queue:tasks', 0, -1) // Wszystkie
const firstFive = await redis.lrange('queue:tasks', 0, 4)
// LLEN - długość listy
const queueLength = await redis.llen('queue:tasks')
// LINDEX - element pod indeksem
const secondTask = await redis.lindex('queue:tasks', 1)
// LSET - ustaw element pod indeksem
await redis.lset('queue:tasks', 0, 'updated_task')
// LTRIM - przytnij listę
await redis.ltrim('notifications', 0, 99) // Zachowaj ostatnie 100Set operations
// SADD - dodaj elementy
await redis.sadd('tags:post:1', 'javascript', 'react', 'typescript')
// SMEMBERS - wszystkie elementy
const tags = await redis.smembers('tags:post:1')
// SISMEMBER - czy element należy do zbioru
const hasTag = await redis.sismember('tags:post:1', 'react')
// SCARD - liczba elementów
const tagCount = await redis.scard('tags:post:1')
// SREM - usuń element
await redis.srem('tags:post:1', 'deprecated')
// SINTER - część wspólna zbiorów
const commonTags = await redis.sinter('tags:post:1', 'tags:post:2')
// SUNION - suma zbiorów
const allTags = await redis.sunion('tags:post:1', 'tags:post:2')
// SDIFF - różnica zbiorów
const uniqueTags = await redis.sdiff('tags:post:1', 'tags:post:2')Sorted Set operations
// ZADD - dodaj z score
await redis.zadd('leaderboard', {
score: 100,
member: 'player1',
})
await redis.zadd('leaderboard',
{ score: 150, member: 'player2' },
{ score: 80, member: 'player3' }
)
// ZRANGE - pobierz zakres (ascending)
const topPlayers = await redis.zrange('leaderboard', 0, 9, {
rev: true, // Descending (highest first)
withScores: true,
})
// ZSCORE - pobierz score członka
const score = await redis.zscore('leaderboard', 'player1')
// ZRANK - pozycja w rankingu
const rank = await redis.zrank('leaderboard', 'player1')
const rankFromTop = await redis.zrevrank('leaderboard', 'player1')
// ZINCRBY - zwiększ score
await redis.zincrby('leaderboard', 10, 'player1')
// ZRANGEBYSCORE - zakres po score
const playersAbove100 = await redis.zrangebyscore('leaderboard', 100, '+inf')
// ZREMRANGEBYRANK - usuń zakres
await redis.zremrangebyrank('leaderboard', 0, 9) // Usuń najsłabszych 10JSON operations (RedisJSON)
import { redis } from '@/lib/redis'
// Zapisz obiekt JSON
await redis.json.set('product:1', '$', {
name: 'MacBook Pro',
price: 2499,
specs: {
cpu: 'M3 Pro',
ram: '18GB',
storage: '512GB',
},
tags: ['laptop', 'apple', 'pro'],
})
// Pobierz cały obiekt
const product = await redis.json.get('product:1')
// Pobierz konkretną ścieżkę
const price = await redis.json.get('product:1', '$.price')
const cpu = await redis.json.get('product:1', '$.specs.cpu')
// Aktualizuj zagnieżdżone pole
await redis.json.set('product:1', '$.price', 2299)
await redis.json.set('product:1', '$.specs.ram', '36GB')
// Operacje na tablicach JSON
await redis.json.arrappend('product:1', '$.tags', 'new-tag')
await redis.json.arrlen('product:1', '$.tags')
// Numeryczne operacje
await redis.json.numincrby('product:1', '$.price', -100) // DiscountPipelining i Transactions
// Pipeline - wiele operacji w jednym request
const pipeline = redis.pipeline()
pipeline.set('key1', 'value1')
pipeline.set('key2', 'value2')
pipeline.incr('counter')
pipeline.hset('user:1', { lastActive: Date.now() })
const results = await pipeline.exec()
// results = [['OK', null], ['OK', null], [1, null], [1, null]]
// Multi/Exec - transakcja atomowa
const tx = redis.multi()
tx.decrby('account:A:balance', 100)
tx.incrby('account:B:balance', 100)
await tx.exec() // Atomowe przeniesienie środkówRate Limiting
Upstash oferuje dedykowaną bibliotekę do rate limiting:
npm install @upstash/ratelimitPodstawowe rate limiting
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
// Stwórz rate limiter
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
analytics: true, // Włącz analytics dashboard
prefix: '@upstash/ratelimit',
})
// Middleware dla Next.js
export async function middleware(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1'
const { success, limit, remaining, reset } = await ratelimit.limit(ip)
if (!success) {
return new NextResponse('Too Many Requests', {
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString(),
},
})
}
return NextResponse.next()
}Różne strategie rate limiting
// Fixed Window - najprostszy
const fixedWindow = new Ratelimit({
redis,
limiter: Ratelimit.fixedWindow(100, '1 h'), // 100/hour
})
// Sliding Window - bardziej sprawiedliwy
const slidingWindow = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '10 s'),
})
// Token Bucket - burst-friendly
const tokenBucket = new Ratelimit({
redis,
limiter: Ratelimit.tokenBucket(10, '1 s', 30), // 10 tokens/sec, max 30
})
// Cached - wydajniejszy dla dużego ruchu
const cached = new Ratelimit({
redis,
limiter: Ratelimit.cachedFixedWindow(100, '10 s'),
ephemeralCache: new Map(), // In-memory cache
})Rate limiting per user/API key
// Rate limit per authenticated user
export async function POST(request: Request) {
const session = await getSession()
const identifier = session?.user?.id ?? 'anonymous'
const { success, remaining } = await ratelimit.limit(identifier)
if (!success) {
return Response.json(
{ error: 'Rate limit exceeded' },
{ status: 429 }
)
}
// Dodaj info do response headers
const response = await handleRequest(request)
response.headers.set('X-RateLimit-Remaining', remaining.toString())
return response
}
// Różne limity dla różnych tier-ów
const limits = {
free: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(100, '1 d'),
}),
pro: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10000, '1 d'),
}),
enterprise: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(1000000, '1 d'),
}),
}
export async function POST(request: Request) {
const user = await getUser()
const limiter = limits[user.tier] || limits.free
const { success } = await limiter.limit(user.id)
// ...
}QStash - Message Queue
QStash to serverless message queue z HTTP delivery, idealny dla background jobs i scheduled tasks.
npm install @upstash/qstashPodstawowe użycie
import { Client } from '@upstash/qstash'
const qstash = new Client({
token: process.env.QSTASH_TOKEN!,
})
// Wyślij wiadomość do endpoint
await qstash.publishJSON({
url: 'https://my-app.com/api/process',
body: {
userId: '123',
action: 'send-email',
data: { template: 'welcome' },
},
})
// Z opóźnieniem
await qstash.publishJSON({
url: 'https://my-app.com/api/reminder',
body: { userId: '123' },
delay: '10m', // 10 minut
})
// Z retry
await qstash.publishJSON({
url: 'https://my-app.com/api/webhook',
body: { event: 'order.created' },
retries: 5,
callback: 'https://my-app.com/api/callback',
failureCallback: 'https://my-app.com/api/failure',
})Scheduled messages (Cron)
// Jednorazowe scheduled message
await qstash.publishJSON({
url: 'https://my-app.com/api/report',
body: { type: 'weekly' },
notBefore: Math.floor(Date.now() / 1000) + 86400, // Za 24h
})
// Recurring schedule (cron)
const schedule = await qstash.schedules.create({
destination: 'https://my-app.com/api/daily-report',
cron: '0 9 * * *', // Codziennie o 9:00 UTC
body: JSON.stringify({ type: 'daily' }),
})
// Lista schedules
const schedules = await qstash.schedules.list()
// Usuń schedule
await qstash.schedules.delete(schedule.scheduleId)Receiver - weryfikacja wiadomości
// app/api/process/route.ts
import { Receiver } from '@upstash/qstash'
import { NextResponse } from 'next/server'
const receiver = new Receiver({
currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
})
export async function POST(request: Request) {
const signature = request.headers.get('upstash-signature')!
const body = await request.text()
// Weryfikuj że wiadomość pochodzi z QStash
const isValid = await receiver.verify({
signature,
body,
url: process.env.VERCEL_URL + '/api/process',
})
if (!isValid) {
return new NextResponse('Invalid signature', { status: 401 })
}
const data = JSON.parse(body)
// Przetwórz wiadomość
await processMessage(data)
return new NextResponse('OK')
}Batch operations
// Wyślij wiele wiadomości naraz
const messages = users.map(user => ({
url: 'https://my-app.com/api/notify',
body: JSON.stringify({ userId: user.id }),
}))
await qstash.batchJSON(messages)
// Wyślij do wielu endpoint-ów
await qstash.publishJSON({
url: [
'https://my-app.com/api/analytics',
'https://my-app.com/api/logging',
'https://webhook.site/xxx',
],
body: { event: 'user.signup', userId: '123' },
})Topics (Pub/Sub)
// Utwórz topic
await qstash.topics.create({ name: 'user-events' })
// Dodaj endpoint do topic
await qstash.topics.addEndpoint({
topic: 'user-events',
endpoint: 'https://service-a.com/webhook',
})
await qstash.topics.addEndpoint({
topic: 'user-events',
endpoint: 'https://service-b.com/webhook',
})
// Publikuj do topic - wszyscy subskrybenci otrzymają wiadomość
await qstash.publishJSON({
topic: 'user-events',
body: { event: 'user.created', userId: '123' },
})Upstash Kafka
Serverless Apache Kafka z REST API.
npm install @upstash/kafkaProducer
import { Kafka } from '@upstash/kafka'
const kafka = new Kafka({
url: process.env.UPSTASH_KAFKA_REST_URL!,
username: process.env.UPSTASH_KAFKA_REST_USERNAME!,
password: process.env.UPSTASH_KAFKA_REST_PASSWORD!,
})
const producer = kafka.producer()
// Wyślij pojedynczą wiadomość
await producer.produce('orders', {
value: JSON.stringify({
orderId: '123',
items: ['item1', 'item2'],
total: 99.99,
}),
})
// Z kluczem partycji
await producer.produce('user-events', {
key: 'user:123', // Wszystkie events tego usera w tej samej partycji
value: JSON.stringify({
event: 'page_view',
page: '/products',
}),
})
// Batch produce
await producer.produceMany([
{ topic: 'logs', value: 'Log entry 1' },
{ topic: 'logs', value: 'Log entry 2' },
{ topic: 'logs', value: 'Log entry 3' },
])Consumer
const consumer = kafka.consumer()
// Konsumuj wiadomości
const messages = await consumer.consume({
consumerGroupId: 'my-group',
instanceId: 'instance-1',
topics: ['orders'],
autoOffsetReset: 'earliest',
})
for (const message of messages) {
console.log('Topic:', message.topic)
console.log('Partition:', message.partition)
console.log('Offset:', message.offset)
console.log('Value:', message.value)
// Przetwórz wiadomość
await processOrder(JSON.parse(message.value))
}
// Commit offsets
await consumer.commit({
consumerGroupId: 'my-group',
instanceId: 'instance-1',
topics: [
{
topic: 'orders',
partitions: [
{ partition: 0, offset: messages[messages.length - 1].offset },
],
},
],
})Integracje
Next.js Edge Functions
// app/api/cache/route.ts
import { Redis } from '@upstash/redis'
export const runtime = 'edge' // Działa na Edge
const redis = Redis.fromEnv()
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const key = searchParams.get('key')
if (!key) {
return Response.json({ error: 'Key required' }, { status: 400 })
}
const cached = await redis.get(key)
if (cached) {
return Response.json({ data: cached, source: 'cache' })
}
// Fetch fresh data
const data = await fetchFreshData(key)
await redis.set(key, data, { ex: 3600 })
return Response.json({ data, source: 'fresh' })
}Vercel KV (powered by Upstash)
// Vercel KV jest oparty na Upstash Redis
import { kv } from '@vercel/kv'
// Identyczne API
await kv.set('key', 'value')
const value = await kv.get('key')
await kv.hset('hash', { field: 'value' })Cloudflare Workers
// worker.ts
import { Redis } from '@upstash/redis/cloudflare'
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const redis = new Redis({
url: env.UPSTASH_REDIS_REST_URL,
token: env.UPSTASH_REDIS_REST_TOKEN,
})
const visits = await redis.incr('page:visits')
return new Response(`This page has been visited ${visits} times`)
},
}Session Management
// lib/session.ts
import { Redis } from '@upstash/redis'
import { cookies } from 'next/headers'
import { nanoid } from 'nanoid'
const redis = Redis.fromEnv()
interface Session {
userId: string
email: string
createdAt: number
}
export async function createSession(userId: string, email: string) {
const sessionId = nanoid(32)
const session: Session = {
userId,
email,
createdAt: Date.now(),
}
await redis.set(`session:${sessionId}`, session, {
ex: 60 * 60 * 24 * 7, // 7 days
})
const cookieStore = await cookies()
cookieStore.set('session', sessionId, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7,
})
return session
}
export async function getSession(): Promise<Session | null> {
const cookieStore = await cookies()
const sessionId = cookieStore.get('session')?.value
if (!sessionId) return null
const session = await redis.get<Session>(`session:${sessionId}`)
if (!session) return null
// Refresh TTL on access
await redis.expire(`session:${sessionId}`, 60 * 60 * 24 * 7)
return session
}
export async function deleteSession() {
const cookieStore = await cookies()
const sessionId = cookieStore.get('session')?.value
if (sessionId) {
await redis.del(`session:${sessionId}`)
cookieStore.delete('session')
}
}Caching with SWR pattern
// lib/cache.ts
import { Redis } from '@upstash/redis'
const redis = Redis.fromEnv()
interface CacheOptions {
staleTime?: number // Czas po którym dane są "stale"
maxAge?: number // Maksymalny czas życia
}
export async function cachedFetch<T>(
key: string,
fetcher: () => Promise<T>,
options: CacheOptions = {}
): Promise<{ data: T; stale: boolean }> {
const { staleTime = 60, maxAge = 3600 } = options
// Sprawdź cache
const cached = await redis.get<{
data: T
timestamp: number
}>(key)
const now = Date.now()
if (cached) {
const age = (now - cached.timestamp) / 1000
if (age < staleTime) {
// Fresh - zwróć z cache
return { data: cached.data, stale: false }
}
if (age < maxAge) {
// Stale - zwróć z cache, odśwież w tle
refreshInBackground(key, fetcher, maxAge)
return { data: cached.data, stale: true }
}
}
// Brak cache lub expired - pobierz fresh
const data = await fetcher()
await redis.set(key, { data, timestamp: now }, { ex: maxAge })
return { data, stale: false }
}
async function refreshInBackground<T>(
key: string,
fetcher: () => Promise<T>,
maxAge: number
) {
try {
const data = await fetcher()
await redis.set(key, { data, timestamp: Date.now() }, { ex: maxAge })
} catch (error) {
console.error('Background refresh failed:', error)
}
}Cennik
Upstash Redis
| Plan | Cena | Requests | Bandwidth | Storage |
|---|---|---|---|---|
| Free | $0/mo | 10K/day | 50KB/req | 256MB |
| Pay as you go | $0.2/100K | Unlimited | 1MB/req | 10GB |
| Pro 2K | $180/mo | 2M/day | 5MB/req | 50GB |
| Enterprise | Custom | Custom | Custom | Custom |
Upstash Kafka
| Plan | Cena | Messages | Partitions |
|---|---|---|---|
| Free | $0/mo | 10K/day | 3 |
| Pay as you go | $0.6/100K | Unlimited | 50 |
| Enterprise | Custom | Custom | Unlimited |
QStash
| Plan | Cena | Messages | Schedules |
|---|---|---|---|
| Free | $0/mo | 500/day | 1 |
| Pay as you go | $1/100K | Unlimited | Unlimited |
| Pro | $40/mo | 500K/mo | Unlimited |
FAQ - Najczęściej Zadawane Pytania
Czy Upstash Redis jest w pełni kompatybilny z Redis?
Tak, Upstash obsługuje większość komend Redis. Główna różnica to REST API zamiast TCP, co jest zaletą dla Edge/Serverless. Niektóre komendy blokujące (BLPOP, BRPOP) nie są wspierane ze względu na HTTP.
Jak działa global edge?
Upstash replikuje dane do 30+ lokalizacji na świecie. Request jest kierowany do najbliższej repliki. Write-y idą do primary i są propagowane do replik. Read latency to typowo <10ms.
Czy dane są trwałe?
Tak, Upstash używa durable storage. Dane są replikowane i backupowane. W przeciwieństwie do klasycznego Redis w pamięci, dane przetrwają restart.
Kiedy używać QStash vs Redis?
- QStash - Dla background jobs, webhooks, scheduled tasks. Ma wbudowane retry, delivery confirmation, scheduling.
- Redis - Dla cache, sessions, rate limiting, real-time data. Synchroniczne operacje.
Czy Upstash działa z Cloudflare Workers?
Tak, to jeden z głównych use-case'ów. REST API działa w środowiskach bez TCP (Edge Functions, Workers).