Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik20 min czytania

Upstash, Redis i kolejki rozliczane za żądanie

Upstash daje Redis, kolejki i wyszukiwanie wektorowe rozliczane za żądanie, dostępne po HTTP. Cennik, limity, ograniczanie ruchu i typowe błędy.

Upstash, Redis i kolejki rozliczane za żądanie

Upstash to zestaw usług danych zaprojektowanych pod funkcje uruchamiane na żądanie: pamięć podręczna zgodna z Redisem, kolejka wiadomości, wyszukiwanie wektorowe i mechanizm długo działających procesów. Wyróżniają je dwie rzeczy: dostęp po zwykłym HTTP zamiast po gniazdach TCP oraz rozliczenie za wykonane polecenia, a nie za czas działania serwera.

Dlaczego HTTP zamiast TCP ma tu znaczenie

Klasyczny klient Redisa otwiera połączenie TCP i trzyma je otwarte. W aplikacji działającej na własnym serwerze to zaleta, bo połączenie zestawia się raz i używa przez wiele żądań. W funkcji uruchamianej na żądanie ten model się rozpada.

Powód jest prosty: każde wywołanie funkcji może trafić na nową instancję, więc każde zestawia własne połączenie. Przy tysiącu równoległych wywołań powstaje tysiąc połączeń, a serwer Redisa ma ich limit i zaczyna odrzucać kolejne. Do tego część środowisk uruchomieniowych, zwłaszcza tych działających blisko użytkownika, w ogóle nie pozwala na otwieranie gniazd TCP.

Dostęp po HTTP omija oba problemy. Nie ma stanu połączenia, więc nie ma czym się wysycić, a żądanie da się wysłać z każdego środowiska, które potrafi wykonać zwykłe zapytanie sieciowe. Cena jest taka, że pojedyncza operacja ma większy narzut niż po TCP, bo dochodzi nagłówek HTTP i uzgadnianie szyfrowania. Przy jednym poleceniu na żądanie jest to niezauważalne, przy pięćdziesięciu w pętli już nie, i właśnie dlatego łączenie poleceń w jedną paczkę ma tu większe znaczenie niż w klasycznym Redisie.

Co się stało z Kafką

To najważniejsza poprawka względem starszych materiałów o tej platformie, także wcześniejszej wersji tego tekstu. Upstash oferował usługę Kafki, ale jej nie ma. Okres wycofywania zaczął się 11 września 2024 roku i trwał pół roku, a usługę wyłączono 11 marca 2025.

Powód podany przez firmę był taki, że zamiast utrzymywać Kafkę, woli skupić zasoby na QStash i na mechanizmie długo działających procesów. Kierunek jest logiczny: Kafka to narzędzie do strumieni o dużej przepustowości, obsługiwane zwykle przez zespół platformowy, a Upstash celuje w programistę aplikacji, który potrzebuje kolejki i nie chce administrować brokerem.

Jeśli trafisz na poradnik pokazujący pakiet klienta Kafki od tej firmy, jest nieaktualny. Bieżący zestaw produktów obejmuje Redis, wyszukiwanie wektorowe, QStash, mechanizm procesów, wyszukiwanie pełnotekstowe oraz środowisko testowe.

Czym jest Upstash?

Upstash to serverless data platform oferująca Redis, QStash i wyszukiwanie wektorowe 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.

Bieżące produkty platformy:

  • Upstash Redis, pamięć podręczna zgodna z Redisem, dostępna po HTTP
  • QStash, kolejka wiadomości z ponawianiem i harmonogramem
  • Vector, wyszukiwanie po podobieństwie wektorowym
  • Workflow, długo działające procesy odporne na restart funkcji

Model rozliczeń i kiedy się opłaca

Różnica względem klasycznej instancji Redisa nie polega na cenie jednostkowej, tylko na kształcie krzywej kosztów, i to ona decyduje, czy ta usługa jest dla Ciebie.

Serwer rozliczany za czas kosztuje tyle samo o trzeciej w nocy przy zerowym ruchu, co w szczycie. Rozliczenie za polecenia daje zero przy zerowym ruchu i rośnie liniowo wraz z użyciem. Dla projektu pobocznego, aplikacji wewnętrznej albo produktu przed pierwszymi klientami to różnica między kilkunastoma dolarami miesięcznie a niczym.

Krzywe przecinają się w pewnym punkcie i warto wiedzieć gdzie. Przy stawce dwudziestu centów za sto tysięcy poleceń, dziesięć milionów poleceń miesięcznie kosztuje dwadzieścia dolarów. Tyle samo mniej więcej kosztuje mała, stale działająca instancja u konkurencji, która przy tym wolumenie się nie spoci. Powyżej tego progu rozliczenie za polecenia zaczyna przegrywać, i wtedy sensowniej przejść na plan ze stałą opłatą, dostępny w tej samej usłudze.

Praktyczna reguła brzmi więc: rozliczenie za polecenia bierz przy ruchu nieregularnym albo małym, plan stały przy ruchu przewidywalnym i dużym. Zmiana między nimi nie wymaga migracji danych, więc nie jest to decyzja na zawsze.

Jest jeszcze jedna pozycja, o której łatwo zapomnieć przy szacowaniu. Płacisz osobno za przechowywanie, dwadzieścia pięć centów za gigabajt, i za ruch wychodzący powyżej progu zawartego w planie. Przy pamięci podręcznej z małymi wartościami te pozycje są zaniedbywalne, przy trzymaniu w niej dużych obiektów przestają takie być.

Upstash a alternatywy

CechaUpstashRedis samodzielnieRedis w chmurzeVercel KV
Rozliczenieza polecenia albo stałeza serwerza godzinęza użycie
Dostęp po HTTPtak, natywnieniezwykle nietak
Praca w środowisku brzegowymtaknienietak
Koszt przy zerowym ruchuzeropełnypełnyzero
Administracjabrakpełnaograniczonabrak
Przenośnośćwysoka, zgodne APInajwyższawysokaniska

Zestawienie warto czytać przez pryzmat tego, gdzie działa Twoja aplikacja. W środowisku brzegowym albo w funkcjach na żądanie realnymi opcjami są tylko rozwiązania po HTTP. Na własnym serwerze albo w kontenerze, który żyje długo, klasyczny Redis po TCP będzie szybszy i tańszy, bo połączenie zestawia się raz.

Upstash Redis

Instalacja

Code
Bash
# SDK for JavaScript/TypeScript
npm install @upstash/redis

# SDK for Python
pip install upstash-redis

Konfiguracja

TSlib/redis.ts
TypeScript
// 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
ENV
# .env.local
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=AXxxxx

Podstawowe operacje

Code
TypeScript
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

Code
TypeScript
// 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

Code
TypeScript
// 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 100

Set operations

Code
TypeScript
// 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

Code
TypeScript
// 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 10

JSON operations (RedisJSON)

Code
TypeScript
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) // Discount

Pipelining i Transactions

Code
TypeScript
// 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ów

Ograniczanie liczby zapytań

Upstash oferuje dedykowaną bibliotekę do rate limiting:

Code
Bash
npm install @upstash/ratelimit

Podstawowe ograniczanie

Code
TypeScript
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 ograniczania

Code
TypeScript
// 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
})

Ograniczanie na użytkownika i klucz API

Code
TypeScript
// 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.

Code
Bash
npm install @upstash/qstash

Podstawowe użycie

Code
TypeScript
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)

Code
TypeScript
// 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

TSapp/api/process/route.ts
TypeScript
// 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

Code
TypeScript
// 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)

Code
TypeScript
// 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' },
})

Integracje

Next.js Edge Functions

TSapp/api/cache/route.ts
TypeScript
// 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)

Code
TypeScript
// 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

TSworker.ts
TypeScript
// 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

TSlib/session.ts
TypeScript
// 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')
  }
}

Buforowanie we wzorcu SWR

TSlib/cache.ts
TypeScript
// 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

PlanCenaPoleceniaRozmiar danychBazy
Free0 USD500 tys. miesięcznie256 MB1
Za użycie0,20 USD za 100 tys. poleceńbez limitudo 100 GBdo 100
Stały, od10 USD miesięczniebez limitu250 MBzależnie od planu
Stały, wyżej20, 100, 200, 400, 800, 1500 USDbez limituod 1 GB do 500 GBzależnie od planu

Do tego dochodzą dwie pozycje liczone osobno. Przechowywanie kosztuje 0,25 dolara za gigabajt, przy czym pierwszy gigabajt jest bezpłatny. Ruch wychodzący jest darmowy do dwustu gigabajtów miesięcznie, powyżej trzy centy za gigabajt. Repliki do odczytu w innych regionach oraz zestaw funkcji produkcyjnych są dodatkowo płatne.

Ostatnia pozycja zasługuje na liczbę, bo dotyczy dokładnie tego, co jest tu argumentem sprzedażowym. Repliki do odczytu wycenia się od pięciu do siedmiuset pięćdziesięciu dolarów miesięcznie za region, zależnie od planu. Globalna sieć brzegowa nie jest więc czymś, co dostajesz razem z bazą, tylko decyzją do podjęcia przy każdym regionie osobno. Przy trzech kontynentach ta pozycja potrafi przewyższyć koszt samych poleceń, więc policz ją, zanim uznasz odczyt blisko użytkownika za rozstrzygnięty.

Zmiana w planie darmowym, o której warto wiedzieć przy czytaniu starszych poradników: dawny limit dziesięciu tysięcy poleceń dziennie zastąpiono w marcu 2025 roku limitem miesięcznym pięciuset tysięcy. Praktycznie oznacza to, że nagły skok ruchu w jednym dniu nie odcina już dostępu na resztę doby.

Kolejka i wyszukiwanie wektorowe rozliczają się według własnych stawek, odpowiednio za sto tysięcy wiadomości i za sto tysięcy zapytań, a każdy z tych produktów ma osobny poziom darmowy. Przy szacowaniu budżetu policz je oddzielnie, bo limity się nie sumują.

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 scenariuszy użycia. Dostęp po HTTP działa w środowiskach, które nie pozwalają otwierać gniazd TCP, w tym w funkcjach brzegowych.

Czy nadal da się korzystać z Kafki w Upstash?

Nie. Usługę wycofano 11 marca 2025 roku po półrocznym okresie przejściowym. Firma skierowała użytkowników do QStash i do mechanizmu długo działających procesów. Poradniki pokazujące pakiet klienta Kafki od tej firmy są nieaktualne.

Ograniczanie ruchu, czyli najczęstsze zastosowanie

Z obserwacji projektów, w których ta usługa się pojawia, wynika, że najczęściej trafia tam nie jako pamięć podręczna, tylko jako licznik do ograniczania liczby żądań. Powód jest praktyczny: potrzebujesz stanu współdzielonego między instancjami funkcji, a jest to najmniejszy możliwy kawałek stanu, jaki da się wyobrazić.

Warto rozumieć różnicę między algorytmami, bo wybór ma widoczne konsekwencje. Okno stałe jest najprostsze i najtańsze, ale przepuszcza podwójny ruch na styku dwóch okien, bo użytkownik może wyczerpać limit pod koniec jednego i od razu cały limit na początku następnego. Okno przesuwne usuwa ten problem kosztem większej liczby operacji. Wiadro z żetonami pozwala na krótkie skoki ponad limit, co lepiej odpowiada rzeczywistemu zachowaniu ludzi klikających szybciej niż zwykle.

Praktyczna wskazówka dotyczy klucza, po którym liczysz. Adres IP jest kuszący, ale w sieciach firmowych i przy operatorach komórkowych setki osób dzieli jeden adres, więc limit trafia w niewinnych. Tam, gdzie użytkownik jest zalogowany, licz po jego identyfikatorze, a adresu używaj wyłącznie na trasach dostępnych bez logowania.

Druga wskazówka dotyczy trybu awaryjnego, istotna zwłaszcza przy trasach API w Next.js. Jeśli usługa nie odpowie, Twój kod musi wiedzieć, co robić: przepuścić żądanie czy je odrzucić. Przepuszczanie jest bezpieczniejsze dla użytkowników i ryzykowne przy nadużyciach, odrzucanie odwrotnie. Domyślne zachowanie biblioteki warto sprawdzić i świadomie potwierdzić, zamiast odkrywać je przy pierwszej awarii.

Typowe błędy

Pierwszy to wykonywanie wielu poleceń w pętli. Przy dostępie po HTTP każde polecenie to osobne żądanie sieciowe z pełnym narzutem, więc pętla po stu kluczach oznacza sto wywołań i opóźnienie liczone w sekundach. Łączenie poleceń w jedną paczkę rozwiązuje to jednym wywołaniem i jest tu ważniejsze niż w klasycznym Redisie.

Drugi to trzymanie w pamięci podręcznej dużych obiektów. Rozliczenie obejmuje ruch wychodzący, więc odczyt półmegabajtowego dokumentu przy każdym żądaniu podnosi rachunek szybciej niż liczba poleceń. Do dużych treści lepiej trzymać w pamięci podręcznej identyfikator albo skrót, a samą treść pobierać z magazynu obiektów.

Trzeci to brak czasu wygaśnięcia na kluczach. Wpis bez terminu żyje wiecznie i zajmuje płatne miejsce, a przy pamięci podręcznej prawie zawsze da się określić, jak długo dane są aktualne. Ustawianie terminu przy zapisie jest tańsze niż późniejsze sprzątanie.

Czwarty to poleganie na poleceniach blokujących. Operacje czekające na pojawienie się elementu nie mają sensu przy dostępie po HTTP i nie są dostępne. Jeśli budujesz na nich kolejkę, potrzebujesz QStash albo innego mechanizmu, a nie listy w Redisie.

Piąty to zakładanie natychmiastowej spójności między regionami. Zapis idzie do instancji głównej i dociera do replik z opóźnieniem, więc odczyt z innego regionu tuż po zapisie może zwrócić starą wartość. Przy licznikach i ograniczaniu ruchu ma to znaczenie i trzeba to uwzględnić w projekcie.

Bieżące stawki opisuje strona cennika Upstash, a komunikat o wycofaniu Kafki znajdziesz na blogu firmy.