Buforowanie w Next.js — unstable_cache kontra Redis

Jak dobrać cache w Next.js 16? Data Cache, legacy unstable_cache, use cache i Redis bez uproszczeń, które potem psują architekturę.

Opublikowano

24 września 2025 09:55

Czytanie

6 min czytania

Aktualizacja

15 kwietnia 2026 11:52

Next.js App Router ma wbudowany system cachowania. Rozszerzony fetch, unstable_cache, automatyczna deduplikacja i nowsze Cache Components robią sporo za Ciebie. Ale czy to wystarczy? Kiedy warto sięgnąć po zewnętrzny Redis?

W tym artykule porównam oba podejścia, ale od razu doprecyzuję jedną rzecz: w Next.js 16 unstable_cache jest już API, czyli Application Programming Interface, definiuje sposób komunikacji między aplikacjami lub modułami. legacy i długofalowo warto patrzeć też w stronę use cache oraz Cache Components. Mimo to nadal spotkasz je w realnych projektach, więc trzeba rozumieć oba światy.

Krótka odpowiedź: Next.js oferuje wbudowany system cachowania (rozszerzony fetch, unstable_cache, a po włączeniu Cache Components także use cache) odpowiedni dla prostych projektów i platform takich jak Vercel. Redis sprawdza się tam, gdzie potrzebujesz współdzielonego cache między wieloma instancjami, zaawansowanych operacji (sorted sets, pub/sub) lub środowisk self-hosted. W praktyce najlepszym rozwiązaniem bywa architektura hybrydowa: wbudowany cache jako szybka warstwa lokalna i Redis jako współdzielony magazyn globalny.

Wbudowany cache w Next.js

Next.js oferuje kilka mechanizmów cachowania:

1. Rozszerzony fetch

Code
// Domyślnie: bez Data Cache
const data = await fetch('https://api.example.com/posts')
 
// Rewalidacja co 60 sekund
const data = await fetch('https://api.example.com/posts', {
  next: { revalidate: 60 },
})
 
// Bez cache
const data = await fetch('https://api.example.com/posts', {
  cache: 'no-store',
})

2. unstable_cache

Dla danych z bazy, SDK, lub innych źródeł nie-HTTP:

Code
import { unstable_cache } from 'next/cache'
 
const getCachedPosts = unstable_cache(
  async () => {
    return db.post.findMany()
  },
  ['posts'], // klucz cache
  {
    revalidate: 3600, // 1 godzina
    tags: ['posts'], // do rewalidacji on-demand
  },
)

W nowych projektach myśl o tym jako o przejściowym API. Nadal jest użyteczne, ale dokumentacja Next.js 16 wprost wskazuje use cache jako kierunek docelowy.

3. React cache

Deduplikacja w ramach jednego renderowania:

Code
import { cache } from 'react'
 
export const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } })
})
 
// Wielokrotne wywołania w jednym renderze = jeden query

Gdzie naprawdę żyje cache Next.js?

To kluczowe pytanie, bo tu łatwo o zbyt proste odpowiedzi.

W produkcji fetch(..., { cache: 'force-cache' }) i unstable_cache() korzystają z Data Cache Next.js, które potrafi przetrwać między requestami, a nawet deploymentami. To nie jest wyłącznie zwykła pamięć procesu. Jednocześnie dokładna topologia tego cache zależy od platformy i regionu, więc nie zakładaj globalnie idealnej spójności bez sprawdzenia infrastruktury.

W praktyce:

  • development zachowuje się inaczej niż produkcja i potrafi trzymać odpowiedzi także przez HMR,
  • Vercel dobrze integruje się z Data Cache, ale nadal myśl kategoriami regionów i opóźnień propagacji,
  • self-hosted może wymagać świadomej strategii, jeśli potrzebujesz współdzielonego cache między instancjami.

To właśnie tutaj Redis zaczyna mieć sens: nie dlatego, że wbudowany cache Next.js jest "fałszywy", ale dlatego, że Redis daje Ci własny, współdzielony i łatwiejszy do obserwowania magazyn danych.

Redis jako cache

Redis to zewnętrzny, współdzielony cache:

Code
Użytkownik A → Instancja 1 → Redis MISS → Fetch → Redis WRITE
Użytkownik B → Instancja 2 → Redis HIT → Zwrot danych (bez fetch!)

Wszystkie instancje widzą ten sam cache.

Setup z Upstash

Code
// lib/cache.ts
import { Redis } from '@upstash/redis'
 
const redis = Redis.fromEnv()
 
export async function cached<T>(
  key: string,
  fn: () => Promise<T>,
  ttl: number = 3600,
): Promise<T> {
  // Sprawdź cache
  const cached = await redis.get<T>(key)
  if (cached !== null) {
    return cached
  }
 
  // Pobierz dane
  const data = await fn()
 
  // Zapisz w cache (@upstash/redis serializuje JSON automatycznie)
  await redis.setex(key, ttl, data)
 
  return data
}
 
export async function invalidate(key: string): Promise<void> {
  await redis.del(key)
}
Code
// lib/data.ts
import { cached } from './cache'
 
export async function getPosts() {
  return cached(
    'posts:all',
    () => db.post.findMany({ orderBy: { createdAt: 'desc' } }),
    300, // 5 minut
  )
}

Porównanie: unstable_cache vs Redis

Aspektunstable_cache / Data CacheRedis
SetupBardzo prostyWymaga osobnego serwisu
WspółdzielenieZależne od platformyJawnie współdzielone
LatencjaZwykle bardzo niskaNiska, ale sieciowa
TrwałośćZależna od platformyKontrolowana przez Ciebie
SkalowalnośćDobra, ale mniej przewidywalnaBardzo dobra
KosztWliczony w platformęDodatkowy
RewalidacjarevalidateTag / updateTagManualna lub przez własne API
DebugŚredniŁatwiejszy

Kiedy użyć unstable_cache?

Idealne dla:

  1. Dane rzadko się zmieniające — konfiguracja, tłumaczenia, statyczne treści
  2. Projekty na Vercel lub platformie dobrze wspierającej Data Cache
  3. Proste przypadki — nie chcesz dodatkowej zależności
  4. Cache blisko warstwy renderowania — gdy chcesz wykorzystać wbudowaną rewalidację Next.js
Code
// Dobre użycie unstable_cache
import { unstable_cache } from 'next/cache'
 
// Konfiguracja aplikacji — zmienia się rzadko
export const getConfig = unstable_cache(
  async () => {
    return db.config.findFirst()
  },
  ['app-config'],
  { revalidate: 3600 }, // 1 godzina
)
 
// Tłumaczenia — statyczne
export const getTranslations = unstable_cache(
  async (locale: string) => {
    return import(`@/locales/${locale}.json`)
  },
  ['translations'],
  { revalidate: false }, // nigdy nie wygasa
)

Kiedy użyć Redis?

Idealne dla:

  1. Dane współdzielone globalnie — sesje, liczniki, leaderboardy
  2. Self-hosted / nie-Vercel — potrzebujesz współdzielonego cache
  3. Wymagana trwałość — cache przeżywa restarty
  4. Zaawansowane operacje — sorted sets, pub/sub, TTL per-klucz
  5. Debug i monitoring — chcesz widzieć co jest w cache
Code
// Dobre użycie Redis
 
// Sesje użytkowników
await redis.setex(`session:${sessionId}`, 86400, session)
 
// Liczniki w czasie rzeczywistym
await redis.incr(`pageviews:${slug}`)
 
// Leaderboard
await redis.zadd('leaderboard', { score: 100, member: 'user:123' })
 
// Cache z różnymi TTL
await redis.setex('hot-data', 60, data) // 1 minuta
await redis.setex('warm-data', 3600, data) // 1 godzina
await redis.setex('cold-data', 86400, data) // 24 godziny

Strategia hybrydowa (czasem najlepsza)

W produkcji często łączę oba podejścia:

Code
// lib/cache.ts
import { unstable_cache } from 'next/cache'
import { Redis } from '@upstash/redis'
 
const redis = Redis.fromEnv()
 
// Poziom 1: unstable_cache (szybki, per-instancja)
// Poziom 2: Redis (wolniejszy, współdzielony)
 
export function hybridCache<T>(
  key: string,
  fn: () => Promise<T>,
  options: {
    localTtl?: number // TTL w unstable_cache
    redisTtl?: number // TTL w Redis
    tags?: string[]
  } = {},
) {
  const { localTtl = 60, redisTtl = 3600, tags = [] } = options
 
  return unstable_cache(
    async () => {
      // Sprawdź Redis
      const cached = await redis.get<T>(key)
      if (cached !== null) {
        return cached
      }
 
      // Pobierz dane
      const data = await fn()
 
      // Zapisz w Redis (@upstash/redis serializuje JSON automatycznie)
      await redis.setex(key, redisTtl, data)
 
      return data
    },
    [key],
    { revalidate: localTtl, tags },
  )
}
Code
// Użycie
export const getPopularPosts = hybridCache(
  'posts:popular',
  () =>
    db.post.findMany({
      orderBy: { views: 'desc' },
      take: 10,
    }),
  {
    localTtl: 60, // lokalny cache: 1 minuta
    redisTtl: 300, // Redis: 5 minut
    tags: ['posts'],
  },
)

Przepływ:

  1. Request trafia do instancji
  2. Sprawdź unstable_cache / Data Cache → HIT = zwróć
  3. Sprawdź Redis (globalny) → HIT = zwróć + zapisz lokalnie
  4. MISS = pobierz z bazy → zapisz w Redis → zapisz lokalnie

Rewalidacja

unstable_cache — wbudowane narzędzia

Code
import { revalidateTag, revalidatePath } from 'next/cache'
 
// Rewaliduj wszystko z tagiem 'posts'
revalidateTag('posts', 'max')
 
// Rewaliduj konkretną ścieżkę
revalidatePath('/blog')

Redis — manualna kontrola

Code
// lib/cache.ts
export async function invalidatePattern(pattern: string): Promise<number> {
  const keys = await redis.keys(pattern)
  if (keys.length === 0) return 0
 
  await redis.del(...keys)
  return keys.length
}
 
// Użycie
await invalidatePattern('posts:*') // wszystkie posty
await invalidatePattern('user:123:*') // wszystko dla usera 123

Hybrydowa rewalidacja

Code
// app/actions/posts.ts
'use server'
 
import { revalidateTag } from 'next/cache'
import { redis } from '@/lib/redis'
 
export async function createPost(data: PostData) {
  const post = await db.post.create({ data })
 
  // Rewaliduj oba poziomy cache
  revalidateTag('posts', 'max') // Data Cache / unstable_cache
  await redis.del('posts:all', 'posts:popular') // Redis
 
  return post
}

Benchmarki

Testowałem na prostym scenariuszu: pobierz 100 postów z PostgreSQL. Traktuj te liczby jako orientacyjne. Wyniki mocno zależą od hostingu, regionu, warm/cold startów i tego, czy mierzysz lokalny development, czy produkcję.

MetodaPierwszy requestCache hit
Bez cache~150msN/A
unstable_cache~150ms~1ms
Redis (Upstash)~150ms~8ms
Hybrid~150ms~1ms (local) / ~8ms (Redis)

Wnioski:

  • wbudowany cache Next.js zwykle wygrywa na czystej latencji przy HIT,
  • Redis wygrywa przewidywalnością i współdzieleniem między instancjami,
  • hybryda ma sens dopiero wtedy, gdy naprawdę rozumiesz dwa poziomy invalidation.

Typowe błędy

1. Cache bez TTL

Code
// ❌ Źle — dane nigdy nie wygasają
await redis.set('posts', data)
 
// ✅ Dobrze — zawsze ustaw TTL
await redis.setex('posts', 3600, data)

2. Brak obsługi cache miss

Code
// ❌ Źle — zakłada że cache zawsze istnieje
const data = await redis.get('key')
return data.items // 💥 Error jeśli null
 
// ✅ Dobrze — obsłuż null
const data = await redis.get('key')
if (!data) {
  return fetchFreshData()
}
return data

3. Cache stampede

Gdy cache wygaśnie, wszystkie requesty jednocześnie pobierają dane:

Code
// ✅ Rozwiązanie: lock
export async function cachedWithLock<T>(
  key: string,
  fn: () => Promise<T>,
  ttl: number,
): Promise<T> {
  const cached = await redis.get<T>(key)
  if (cached) return cached
 
  const lockKey = `lock:${key}`
  // SET NX EX — atomowe: ustaw jeśli nie istnieje + wygaśnięcie w jednej komendzie
  const acquired = await redis.set(lockKey, '1', { nx: true, ex: 10 })
 
  if (acquired) {
    try {
      const data = await fn()
      await redis.setex(key, ttl, data)
      return data
    } finally {
      await redis.del(lockKey)
    }
  }
 
  // Ktoś inny pobiera — czekaj i sprawdź ponownie
  await new Promise((r) => setTimeout(r, 100))
  return cachedWithLock(key, fn, ttl)
}

FAQ

Czym różni się unstable_cache od use cache w Next.js?

unstable_cache to starsze API, nadal dostępne w Next.js 16, ale oznaczone jako legacy. use cache to nowszy kierunek, który Next.js promuje jako docelowy sposób na cachowanie danych w App Router, przy czym działa w modelu Cache Components i wymaga jego świadomego włączenia. W nowych projektach warto patrzeć w tę stronę, natomiast w projektach, które już korzystają z unstable_cache, nie ma pilnej potrzeby migracji tylko dlatego, że API zmieniło status.

Kiedy Redis jest lepszy niż wbudowany cache Next.js?

Redis jest lepszym wyborem, gdy Twoja aplikacja działa na wielu instancjach serwera i wymaga wspólnego stanu cache, gdy hostingujesz ją samodzielnie poza Vercel, lub gdy potrzebujesz zaawansowanych operacji takich jak sorted sets, pub/sub, leaderboardy czy sesje użytkowników z kontrolowanym TTL.

Czy Redis spowalnia aplikację?

Redis wprowadza opóźnienie sieciowe (zwykle kilka–kilkanaście ms), więc cache hit z Redisa jest wolniejszy niż hit z pamięci procesu. Jednak przy wielu instancjach bez Redis każda może mieć własne, niespójne dane. Dla większości aplikacji produkcyjnych opóźnienie Redisa jest akceptowalne, a korzyść ze spójności danych przeważa.

Co to jest cache stampede i jak go uniknąć?

Cache stampede to sytuacja, gdy wiele równoczesnych requestów trafia na wygasły klucz cache i wszystkie jednocześnie próbują pobrać dane ze źródła. Rozwiązaniem jest blokada (lock) z atomową operacją SET NX EX w Redis: tylko jeden request pobiera dane i zapisuje je do cache, pozostałe czekają i korzystają z gotowego wyniku.

Czy powinienem cachować wszystkie zapytania do bazy danych?

Nie. Cachowanie ma sens dla danych, które często się powtarzają i rzadko się zmieniają. Dane sesji użytkownika, konfiguracja aplikacji czy popularne wpisy blogowe to dobry kandydat. Dane wrażliwe na świeżość (np. stan zamówienia w trakcie realizacji) lub unikalne dla każdego żądania najczęściej nie powinny być agresywnie cachowane.

Jak poprawnie unieważniać cache w architekturze hybrydowej?

W modelu hybrydowym (unstable_cache + Redis) trzeba invalidować oba poziomy jednocześnie. W Server Actions lub route handlerach wywołaj revalidateTag() dla warstwy Next.js i redis.del() dla kluczy Redis. Pominięcie jednego poziomu może prowadzić do sytuacji, gdzie jeden poziom serwuje nieaktualne dane.

Czy Redis Upstash sprawdza się na produkcji?

Upstash 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 dostępny przez HTTP, co czyni go prostym w integracji z Next.js (szczególnie na Vercel). Sprawdza się dobrze przy umiarkowanym ruchu i projektach, które nie chcą zarządzać własnym serwerem Redis. Przy bardzo wysokim natężeniu requestów lub wymaganiach nisko-latencyjnych warto rozważyć self-hosted Redis lub dedykowaną ofertę Redis Cloud z niską latencją geograficzną.

Podsumowanie — decision tree

Code
Czy potrzebujesz współdzielonego cache między instancjami?
├─ NIE → `unstable_cache` / `use cache`
└─ TAK → Czy to self-hosted / nie-Vercel?
         ├─ TAK → Redis
         └─ NIE → Czy potrzebujesz zaawansowanych operacji (sorted sets, pub/sub)?
                  ├─ TAK → Redis
                  └─ NIE → `unstable_cache` / `use cache` (lub hybryda, jeśli masz konkretny powód)

Moja praktyka:

  • Proste projekty z App Router → najpierw wbudowany cache Next.js
  • Aplikacje z realnym ruchem i wieloma instancjami → rozważ Redis lub model hybrydowy
  • Self-hosted z wymaganiem współdzielenia cache → Redis jako primary cache bywa bezpieczniejszym wyborem

Źródła i dokumentacja


Masz problemy z wydajnością aplikacji Next.js? Skontaktuj się ze mną — pomogę zoptymalizować strategię cachowania dla Twojego przypadku.

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
Anthropic uderza w Figmę i Adobe — oto Claude Design

Anthropic uderza w Figmę i Adobe — oto Claude Design

Anthropic wypuścił właśnie narzędzie AI do tworzenia stron, landing page'ów i prezentacji z promptu. Oto co wiemy o Claude Design i Opus 4.7 — i co to oznacza dla developerów.

Maciej Sala

Maciej Sala

Founder Strivelab

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