Upstash Redis w Next.js — sesje, cache, rate limiting i liczniki

Szeroki przewodnik po Upstash Redis w Next.js: sesje server-side, cache, rate limiting i proste liczniki bez stawiania własnej infrastruktury Redis.

Opublikowano

24 listopada 2025 15:35

Czytanie

6 min czytania

Aktualizacja

15 kwietnia 2026 11:52

Upstash Redis rozwiązuje bardzo konkretny problem nowoczesnych aplikacji Next.js: potrzebujesz współdzielonego stanu między requestami, ale nie chcesz utrzymywać własnej instancji Redis.

Problem w tym, że tradycyjny Redis wymaga serwera, zarządzania połączeniami i pilnowania kosztów. Upstash zmienia tę grę — to serverless Redis z HTTP API, czyli Application Programming Interface, definiuje sposób komunikacji między aplikacjami lub modułami., idealnie dopasowany do architektury Next.js i Vercel.

W tym artykule pokażę trzy najczęstsze zastosowania Upstash Redis w produkcyjnych aplikacjach Next.js i zatrzymam się na poziomie, który wystarcza w większości projektów: sensowny setup, działające przykłady i najważniejsze decyzje architektoniczne bez rozbudowywania osobnego deep dive dla każdego use case'u.

Krótka odpowiedź: Upstash Redis to Serverless to model uruchamiania kodu, w którym nie zarządzasz ręcznie serwerem, a płacisz zwykle za wykonania lub użycie. Redis z HTTP API, który idealnie pasuje do architektury Next.js i Vercel — nie wymaga stałego połączenia TCP ani własnej infrastruktury. Najczęstsze zastosowania to: sesje server-side z natychmiastowym unieważnianiem, cache odpowiedzi zewnętrznych API oraz rate limiting chroniący endpointy przed nadużyciami. Warto po niego sięgać, gdy potrzebujesz współdzielonego stanu między requestami i instancjami, a nie tylko lokalnego cache Next.js.

Czym jest Upstash Redis?

Upstash to serverless Redis z kilkoma kluczowymi różnicami:

  • HTTP API — nie potrzebujesz stałego połączenia TCP
  • Pay-per-request — płacisz za operacje, nie za czas działania
  • Global replication — dane blisko użytkowników
  • Kompatybilność z Redis — większość komend działa tak samo
Code
npm install @upstash/redis

Po utworzeniu bazy w Upstash Console (lub przez Vercel Marketplace) dostajesz dwie zmienne środowiskowe:

Code
UPSTASH_REDIS_REST_URL="https://xyz.upstash.io"
UPSTASH_REDIS_REST_TOKEN="AXxx..."

Konfiguracja klienta

Code
// lib/redis.ts
import { Redis } from '@upstash/redis'
 
export const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})
 
// Lub jeszcze prościej — automatycznie czyta z env
export const redis = Redis.fromEnv()

Od tego momentu masz dostęp do pełnego API Redis.

Zastosowanie 1: Sesje użytkowników

Domyślne sesje w Next.js (np. przez NextAuth) używają JWT, czyli JSON Web Token, to podpisany token używany często do autoryzacji i przekazywania tożsamości użytkownika. lub bazy danych. Redis oferuje trzecią opcję — szybkie sesje server-side z możliwością natychmiastowego unieważnienia.

Prosta implementacja sesji

Code
// lib/session.ts
import { redis } from './redis'
import { cookies } from 'next/headers'
import { nanoid } from 'nanoid'
 
const SESSION_TTL = 60 * 60 * 24 * 7 // 7 dni w sekundach
 
interface Session {
  userId: string
  email: string
  createdAt: number
}
 
export async function createSession(
  userId: string,
  email: string,
): Promise<string> {
  const sessionId = nanoid(32)
  const session: Session = {
    userId,
    email,
    createdAt: Date.now(),
  }
 
  await redis.setex(`session:${sessionId}`, SESSION_TTL, session)
 
  ;(await cookies()).set('session_id', sessionId, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: SESSION_TTL,
  })
 
  return sessionId
}
 
export async function getSession(): Promise<Session | null> {
  const sessionId = (await cookies()).get('session_id')?.value
  if (!sessionId) return null
 
  const data = await redis.get<Session>(`session:${sessionId}`)
  if (!data) return null
 
  return data
}
 
export async function destroySession(): Promise<void> {
  const sessionId = (await cookies()).get('session_id')?.value
  if (!sessionId) return
 
  await redis.del(`session:${sessionId}`)
  ;(await cookies()).delete('session_id')
}

Użycie w Server Component

Code
// app/dashboard/page.tsx
import { getSession } from '@/lib/session'
import { redirect } from 'next/navigation'
 
export default async function DashboardPage() {
  const session = await getSession()
 
  if (!session) {
    redirect('/login')
  }
 
  return (
    <main>
      <h1>Witaj, {session.email}</h1>
    </main>
  )
}

Wylogowanie przez Server Action

Code
// app/actions/auth.ts
'use server'
 
import { destroySession } from '@/lib/session'
import { redirect } from 'next/navigation'
 
export async function logout() {
  await destroySession()
  redirect('/login')
}

Zalety sesji w Redis:

  • Natychmiastowe wylogowanie — usuń klucz i sesja znika
  • Wylogowanie ze wszystkich urządzeń — usuń wszystkie klucze session:* dla użytkownika
  • Brak rozmiaru JWT — sesja może przechowywać więcej danych
  • TTL — sesje automatycznie wygasają

Zastosowanie 2: Cache danych API

Zewnętrzne API są wolne i często mają limity requestów. Redis pozwala cachować odpowiedzi i serwować je błyskawicznie.

Helper do cachowania

Code
// lib/cache.ts
import { redis } from './redis'
 
interface CacheOptions {
  ttl?: number // sekundy
  tags?: string[]
}
 
export async function cached<T>(
  key: string,
  fn: () => Promise<T>,
  options: CacheOptions = {},
): Promise<T> {
  const { ttl = 3600 } = options
 
  // Sprawdź cache
  const cached = await redis.get<T>(key)
  if (cached !== null) {
    return cached
  }
 
  // Pobierz świeże dane
  const data = await fn()
 
  // Zapisz w cache (@upstash/redis serializuje JSON automatycznie)
  if (ttl > 0) {
    await redis.setex(key, ttl, data)
  } else {
    await redis.set(key, data)
  }
 
  return data
}
 
export async function invalidate(key: string): Promise<void> {
  await redis.del(key)
}
 
export async function invalidateMany(keys: string[]): Promise<void> {
  if (keys.length > 0) {
    await redis.del(...keys)
  }
}

Ważna uwaga produkcyjna: unikałbym masowego KEYS pattern jako standardowej strategii invalidacji. W małym projekcie to przejdzie, ale przy większej skali lepiej trzymać jawne klucze, wersjonować namespace albo utrzymywać własne mapy tagów do usuwania.

Cachowanie odpowiedzi API

Code
// lib/api/products.ts
import { cached, invalidate } from '@/lib/cache'
 
interface Product {
  id: string
  name: string
  price: number
}
 
export async function getProducts(): Promise<Product[]> {
  return cached(
    'products:all',
    async () => {
      const res = await fetch('https://api.example.com/products')
      return res.json()
    },
    { ttl: 300 }, // 5 minut
  )
}
 
export async function getProduct(id: string): Promise<Product | null> {
  return cached(
    `products:${id}`,
    async () => {
      const res = await fetch(`https://api.example.com/products/${id}`)
      if (!res.ok) return null
      return res.json()
    },
    { ttl: 600 }, // 10 minut
  )
}
 
// Po aktualizacji produktu
export async function invalidateProductCache(id: string): Promise<void> {
  await invalidate(`products:${id}`)
  await invalidate('products:all')
}

Użycie w Server Component

Code
// app/products/page.tsx
import { getProducts } from '@/lib/api/products'
 
export default async function ProductsPage() {
  const products = await getProducts() // pierwsze żądanie: API, kolejne: Redis
 
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  )
}

Zastosowanie 3: Rate limiting

Rate limiting chroni API przed nadużyciami. Upstash oferuje dedykowaną bibliotekę @upstash/ratelimit, która implementuje sprawdzone algorytmy.

Code
npm install @upstash/ratelimit

Konfiguracja rate limitera

Code
// lib/ratelimit.ts
import { Ratelimit } from '@upstash/ratelimit'
import { redis } from './redis'
 
// 10 żądań na 10 sekund (sliding window)
export const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, '10 s'),
  analytics: true, // opcjonalne statystyki w Upstash Console
  prefix: 'ratelimit',
})
 
// Warianty dla różnych endpointów
export const strictRatelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(5, '1 m'), // 5 na minutę
  prefix: 'ratelimit:strict',
})
 
export const authRatelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(5, '15 m'), // 5 prób logowania na 15 minut
  prefix: 'ratelimit:auth',
})

Jaki algorytm wybrać?

Nie musisz analizować całej teorii kolejek, żeby wdrożyć sensowny limiter:

  • Sliding window to najlepszy domyślny wybór dla większości projektów, bo daje przewidywalne zachowanie bez dużych skoków na granicach okna.
  • Fixed window jest najprostszy, ale potrafi przepuścić nienaturalny burst na styku dwóch okien czasowych.
  • Token bucket ma sens, gdy chcesz pozwolić na krótkie piki ruchu, ale nadal ograniczać średnią intensywność zapytań.

Jeśli nie masz bardzo specyficznych wymagań, zacznij od slidingWindow.

Middleware z rate limitingiem

Code
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { ratelimit } from '@/lib/ratelimit'
 
export async function middleware(request: NextRequest) {
  // Tylko dla API routes
  if (!request.nextUrl.pathname.startsWith('/api')) {
    return NextResponse.next()
  }
 
  const ip = request.headers.get('x-real-ip') ?? request.headers.get('x-forwarded-for')?.split(',')[0] ?? 'anonymous'
  const { success, limit, reset, remaining } = await ratelimit.limit(ip)
 
  if (!success) {
    return NextResponse.json(
      { error: 'Too many requests' },
      {
        status: 429,
        headers: {
          'X-RateLimit-Limit': limit.toString(),
          'X-RateLimit-Remaining': remaining.toString(),
          'X-RateLimit-Reset': reset.toString(),
        },
      },
    )
  }
 
  const response = NextResponse.next()
  response.headers.set('X-RateLimit-Limit', limit.toString())
  response.headers.set('X-RateLimit-Remaining', remaining.toString())
  response.headers.set('X-RateLimit-Reset', reset.toString())
 
  return response
}
 
export const config = {
  matcher: '/api/:path*',
}

IP, user ID czy oba?

Najprostszy identyfikator to IP, ale nie zawsze jest wystarczający. Za load balancerem albo na Vercelu musisz uważać na nagłówki proxy, zwykle korzystając z x-forwarded-for lub podobnego źródła adresu klienta. Dla endpointów po zalogowaniu lepszym kluczem bywa user:${userId}, bo chroni to konkretną tożsamość, a nie tylko adres sieciowy współdzielony z innymi użytkownikami. W praktyce często najlepiej działa układ mieszany: IP dla tras publicznych i formularzy anonimowych, user ID dla akcji wykonywanych po autoryzacji.

Rate limiting w Server Action

Code
// app/actions/contact.ts
'use server'
 
import { headers } from 'next/headers'
import { strictRatelimit } from '@/lib/ratelimit'
 
export async function sendContactForm(formData: FormData) {
  const ip = (await headers()).get('x-forwarded-for') ?? 'anonymous'
 
  const { success } = await strictRatelimit.limit(ip)
 
  if (!success) {
    return { error: 'Zbyt wiele wiadomości. Spróbuj później.' }
  }
 
  // ... logika wysyłania formularza
 
  return { success: true }
}

Bonus: Liczniki i statystyki

Redis świetnie nadaje się do liczników w czasie rzeczywistym:

Code
// lib/analytics.ts
import { redis } from './redis'
 
export async function trackPageView(slug: string): Promise<number> {
  const key = `pageviews:${slug}`
  return redis.incr(key)
}
 
export async function getPageViews(slug: string): Promise<number> {
  const views = await redis.get<number>(`pageviews:${slug}`)
  return views ?? 0
}
 
// Licznik z TTL (dzienne statystyki)
export async function trackDailyVisit(userId: string): Promise<void> {
  const today = new Date().toISOString().split('T')[0]
  const key = `visits:${today}`
 
  await redis.sadd(key, userId)
  await redis.expire(key, 60 * 60 * 24 * 7) // 7 dni
}
 
export async function getDailyUniqueVisitors(): Promise<number> {
  const today = new Date().toISOString().split('T')[0]
  return redis.scard(`visits:${today}`)
}
Code
// app/blog/[slug]/page.tsx
import { trackPageView, getPageViews } from '@/lib/analytics'
 
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const views = await trackPageView(slug)
 
  return (
    <article>
      <span>{views} wyświetleń</span>
      {/* ... */}
    </article>
  )
}

Takie liczniki sprawdzają się do prostych metryk technicznych albo paneli admina. Do klasycznego trackingu pageview nie wrzucałbym inkrementacji bezpośrednio w render Server Component, bo render może wykonać się więcej niż raz. Do analityki użytkownika lepsze są eventy wysyłane jawnie z klienta albo przez route handler.

Kiedy Redis nie jest potrzebny

To ważny kontrapunkt, bo Redis łatwo dodać zbyt wcześnie.

Jeśli potrzebujesz tylko:

  • cache'owania fetch() w App Router,
  • rewalidacji po tagach,
  • prostego ISR, czyli Incremental Static Regeneration, pozwala odświeżać strony statyczne po czasie bez pełnego rebuildu. i odświeżania stron po webhooku,

to często wystarczy wbudowany model cache Next.js. Redis ma sens wtedy, gdy naprawdę potrzebujesz współdzielonego stanu między requestami i instancjami: sesji server-side, rate limitingu, liczników, kolejki albo wspólnego cache dla wielu procesów.

Koszty i limity

Upstash ma darmowy tier i model pay-as-you-go, ale konkretne limity i ceny warto zawsze sprawdzić w aktualnym cenniku. Dla większości projektów developmentowych i małych aplikacji próg wejścia jest niski, ale przy intensywnym cachowaniu i rate limitingu dobrze policzyć liczbę operacji, nie tylko rozmiar danych.

FAQ

Czym Upstash Redis różni się od zwykłego Redisa?

Upstash używa HTTP API zamiast stałego połączenia TCP, co czyni go kompatybilnym ze środowiskami serverless (Vercel Edge, AWS Lambda). Rozliczenia są per-request, a nie za czas działania instancji, więc idealnie skaluje się do zera. Większość standardowych komend Redis działa tak samo, ale część wzorców opartych o długie lub blokujące połączenia wymaga innego podejścia niż w klasycznym kliencie TCP. Upstash obsługuje np. pub/sub przez SSE, czyli Server-Sent Events, pozwala serwerowi przesyłać strumień aktualizacji do przeglądarki przez jedno połączenie HTTP., ale nie każdy scenariusz znany z tradycyjnego Redisa będzie równie wygodny w architekturze serverless.

Jak skonfigurować Upstash Redis w projekcie Next.js?

Zainstaluj pakiet @upstash/redis, utwórz bazę w Upstash Console lub przez Vercel Marketplace i dodaj do .env.local dwie zmienne: UPSTASH_REDIS_REST_URL oraz UPSTASH_REDIS_REST_TOKEN. Następnie inicjalizuj klienta przez Redis.fromEnv() — biblioteka automatycznie odczyta zmienne środowiskowe. Klient działa zarówno w Server Components, jak i w Server Actions oraz Route Handlers.

Kiedy używać Upstash do sesji zamiast JWT?

Sesje Redis mają trzy kluczowe zalety nad JWT: można je natychmiast unieważnić (wystarczy usunąć klucz), nie mają ograniczenia rozmiaru (JWT ma limit ~8 KB), a dane sesji nigdy nie trafiają do klienta. JWT sprawdza się w systemach rozproszonych bez wspólnego backendu, ale dla standardowych aplikacji Next.js sesje Redis dają lepszą kontrolę.

Jak działa rate limiting z Upstash Ratelimit?

Biblioteka @upstash/ratelimit implementuje gotowe algorytmy (sliding window, fixed window, token bucket). Konfigurujesz limiter z Redisem i parametrami okna, a następnie wywołujesz ratelimit.limit(identifier) — zwraca obiekt { success, limit, remaining, reset }. Możesz użyć go w middleware Next.js dla wszystkich tras API lub bezpośrednio w Server Action.

Czy Upstash Redis nadaje się do cache'owania w Next.js App Router?

Tak, szczególnie gdy potrzebujesz cache współdzielonego między wieloma instancjami lub procesami. Do prostego cachowania pojedynczych requestów fetch() wystarczy wbudowany mechanizm Next.js (next: { revalidate: 60 }). Redis ma sens, gdy cache musi być spójny między wieloma równoległymi instancjami serwera lub gdy potrzebujesz jawnej invalidacji z zewnętrznego systemu (np. webhooka CMS, czyli Content Management System, to system do zarządzania treścią bez ręcznej edycji kodu.).

Jak liczyć wyświetlenia strony z Upstash Redis?

Użyj komendy redis.incr('pageviews:slug') — atomicznie zwiększa licznik i zwraca nową wartość. Uważaj jednak, żeby nie wywoływać inkrementacji bezpośrednio w render Server Component, bo komponent może renderować się więcej niż raz. Bezpieczniejsze podejście to osobny Route Handler lub Server Action wyzwalany jawnie po stronie klienta.

Ile kosztuje Upstash Redis i czy jest darmowy tier?

Upstash oferuje darmowy tier z podstawowymi limitami — wystarczającymi do projektów deweloperskich i małych aplikacji. Płatne plany rozliczane są per-request, więc koszt rośnie proporcjonalnie do ruchu. Przed wdrożeniem na produkcję warto oszacować liczbę operacji Redis (nie tylko transferu danych), bo rate limiting i cache mogą generować dużą ich liczbę. Aktualne limity i ceny zawsze sprawdzaj w cenniku Upstash.

Podsumowanie

Upstash Redis to szwajcarski scyzoryk dla aplikacji Next.js:

  • Sesje — szybkie, bezpieczne, z natychmiastowym unieważnianiem
  • Cache — przyspieszenie API i redukcja kosztów zewnętrznych usług
  • Rate limiting — ochrona przed nadużyciami
  • Liczniki — real-time analytics bez zewnętrznych narzędzi

Serverless model Upstash oznacza, że nie płacisz za idle time i nie musisz zarządzać infrastrukturą. Integracja z Vercel Marketplace sprawia, że setup zajmuje minuty.

Źródła i dokumentacja


Chcesz więcej o cachowaniu? Sprawdź buforowanie w Next.js — unstable_cache vs Redis lub poznaj Server Actions, które dobrze współpracują z limiterami i współdzielonym stanem.

Pracuję z tym zawodowo.

Jeśli chcesz przełożyć ten temat na lepszą architekturę frontendu, uporządkować React lub Next.js i podnieść jakość pracy zespołu, skontaktuj się ze mną. Pomagam zamieniać wiedzę z artykułów w praktyczne decyzje technologiczne.

O autorze

Maciej Sala

Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.

Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje własne projekty.

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Fachowe porównanie Astro.js i Next.js z perspektywy developera pracującego na co dzień w Next.js. Architektura, wydajność, SEO, DX, koszty i konkretne use case — z benchmarkami i przykładami kodu.

Maciej Sala

Maciej Sala

Founder Strivelab