1. Po co w ogóle myśleć o cache i edge w ChatGPT App
W klasycznej aplikacji webowej też zależy ci na szybkości, ale użytkownik przynajmniej widzi spinner. W ChatGPT App sytuacja jest ciekawsza. Użytkownik rozmawia z modelem, a ten czasem decyduje się wywołać twój App. Widżet powinien się pojawić i dość szybko pokazać coś użytecznego.
Praktyka jest dość jednoznaczna: latency = pieniądze. Im dłużej odpowiadasz, tym większa szansa, że użytkownik odejdzie, a dodatkowe wywołania LLM/backendu to bezpośrednie koszty modeli i infrastruktury. Cache zmniejsza jedno i drugie.
Plus specyfika ChatGPT Apps:
- Żądania od ChatGPT do twojego App idą przez sieć i różne warstwy pośrednie. Każda milisekunda na każdym kroku się sumuje.
- Endpointy MCP/HTTP mają realne timeouty (w tym funkcje serverless i funkcje edge w Vercel). Jeśli się nie wyrobisz, ChatGPT zobaczy błąd i może nawet zacząć „halucynować” odpowiedź.
- Wiele danych w GiftGenius nie zmienia się co sekundę: struktura katalogu prezentów, zbiory „top‑pomysłów” dla różnych segmentów, ustawienia funkcji. Głupio za każdym razem bić w bazę lub zewnętrzne API.
I tu właśnie wchodzą na scenę:
- CDN i edge‑cache, aby szybko serwować statyczne zasoby i cache’owalny JSON.
- HTTP‑cache z Cache-Control/ETag/SWR, aby kolejne żądania były szybsze i tańsze.
- Funkcje edge Vercel, aby wykonywać lekką logikę jak najbliżej ChatGPT i użytkownika, ale nie przerabiać ich na „mini‑backend”.
2. Anatomia opóźnień w GiftGenius i punkty cache’owania
Najpierw warto uczciwie narysować, gdzie w ogóle rodzi się latency.
sequenceDiagram
participant User as Użytkownik
participant ChatGPT as ChatGPT
participant App as ChatGPT App (Apps SDK)
participant GW as MCP Gateway / Edge
participant GiftAPI as Gift REST API / mikroserwis prezentów
participant DB as Katalog/Baza
User->>ChatGPT: "Dobierz prezent dla brata"
ChatGPT->>App: Wywołanie narzędzia + render widżetu
App->>GW: HTTP / MCP żądanie (kategorie, zbiory)
GW->>GiftAPI: HTTP (REST)
GiftAPI->>DB: Zapytanie o katalog/rekomendacje
DB-->>GiftAPI: Odpowiedź
GiftAPI-->>GW: Odpowiedź (JSON)
GW-->>App: Odpowiedź (JSON)
App-->>ChatGPT: Widżet z wynikami
ChatGPT-->>User: Wiadomość + UI
Gdzie można tu „ściąć zakręt”?
- Między ChatGPT a twoim perymetrem — CDN/edge‑cache (Vercel CDN/Edge Network), który może serwować niezmienne assety widżetu i cache’owalny JSON bez wchodzenia na twój serwer origin.
- Między Gateway a wewnętrznymi usługami REST/HTTP (Gift REST API, Commerce REST API itd.) i bazą — cache aplikacji (Redis/w pamięci/cache bazy), aby nie wołać tych samych zapytań (np. „lista kategorii prezentów”) po dziesięć razy.
W tym wykładzie skupiamy się właśnie na warstwie HTTP/edge, bo jest najbliżej ChatGPT i Vercel.
3. Rodzaje cache’u w naszej architekturze
Skoro mamy architekturę „warstwowego tortu”, to i cache’y jest kilka.
| Typ cache’u | Gdzie działa | Do czego się nadaje |
|---|---|---|
| Cache przeglądarki | Wewnątrz klienta ChatGPT (przeglądarka/desktop) | Statyki widżetu, ikony, fonty (ograniczona kontrola) |
| CDN / edge‑cache | Na węzłach edge Vercel/Cloudflare | Statyki + wspólny JSON (kategorie, konfiguracje, ogólne zbiory) |
| Cache aplikacji | Wewnątrz MCP Gateway lub usług backendowych (Redis, in‑memory) | Wyniki ciężkich zapytań do bazy/zewnętrznych API |
| Cache bazy danych/materializacja | W samej bazie (widoki zmaterializowane itd.) | Wstępnie wyliczone agregaty, analityka |
Teraz skupimy się na pierwszych dwóch: HTTP‑cache + CDN/edge.
4. HTTP‑cache: Cache-Control, max-age i s-maxage
HTTP‑cache jest sterowany przede wszystkim nagłówkiem Cache-Control. To od niego zależy, czy przeglądarka/klient ChatGPT i/lub CDN może cache’ować twoją odpowiedź i jak długo.
Kluczowe elementy:
- max-age — ile sekund przeglądarka może cache’ować odpowiedź.
- s-maxage — ile sekund może cache’ować shared cache (CDN/proxy).
- public — odpowiedź można cache’ować we wspólnym cache’u.
- private — odpowiedź tylko dla konkretnego klienta; CDN jej nie cache’uje.
W GiftGenius, na przykład:
- Pliki JS/CSS/fonty widżetu — wersjonowane pliki (z hashem w nazwie), można je śmiało zwracać z Cache-Control: max-age=31536000, immutable.
- JSON z listą kategorii prezentów — taki sam dla wszystkich użytkowników, tutaj ma sens public, s-maxage=60 (albo dłużej).
Najprostszy Route Handler Next.js dla GET /api/gifts/categories, cache’owany w CDN przez 60 sekund:
// app/api/gifts/categories/route.ts
import { NextResponse } from "next/server";
export const runtime = "nodejs"; // zwykła funkcja serverless
export async function GET() {
// tutaj moglibyśmy iść do bazy danych/zewnętrznego API
const categories = [
{ id: "for_brother", title: "Prezenty dla brata" },
{ id: "for_mom", title: "Prezenty dla mamy" },
];
return NextResponse.json(categories, {
headers: {
// pozwalamy CDN-owi cache’ować przez 60 sekund
"Cache-Control": "public, s-maxage=60",
},
});
}
Vercel CDN będzie przechowywać odpowiedź 60 sekund, a wszystkie żądania ChatGPT po ten JSON w tym oknie w ogóle nie dojdą do twojej funkcji. To natychmiastowe i tanie.
5. ETag: odcisk treści i 304 Not Modified
ETag to warunkowy „odcisk palca” zasobu, zwykle hash treści. Schemat działania:
- Serwer zwraca odpowiedź z nagłówkiem ETag: "v1-abc123".
- Przy następnym razie klient wysyła nagłówek If-None-Match: "v1-abc123".
- Jeśli serwer uzna, że treść się nie zmieniła, odpowiada 304 Not Modified bez ciała.
Ważne: ETag oszczędza transfer, ale niekoniecznie zmniejsza latency, bo i tak potrzebny jest round trip do serwera. W kontekście ChatGPT Apps jest to przydatne dla ciężkich odpowiedzi JSON, ale nie należy oczekiwać cudownej szybkości wyłącznie od ETag — do tego lepsze są SWR i edge‑cache.
Przykład prostego ETag w handlerze Next.js (bez crypto‑hashy, żeby nie komplikować):
// app/api/gifts/config/route.ts
import { NextRequest, NextResponse } from "next/server";
const CONFIG = { version: 1, showExperimentalIdeas: true };
const ETAG = `"v${CONFIG.version}"`;
export async function GET(req: NextRequest) {
const ifNoneMatch = req.headers.get("if-none-match");
if (ifNoneMatch === ETAG) {
// Treść się nie zmieniła — zwracamy 304
return new NextResponse(null, { status: 304, headers: { ETag: ETAG } });
}
return NextResponse.json(CONFIG, {
headers: {
ETag: ETAG,
"Cache-Control": "public, s-maxage=300",
},
});
}
W praktyce oczywiście policzysz ETag z hasha danych albo użyjesz wersji rekordu w bazie.
6. Stale‑While‑Revalidate (SWR): szybko i wystarczająco świeżo
SWR to podejście „pokaż stare od razu, a nowe dociągnij w tle”. Można je zrealizować:
- Na poziomie nagłówka HTTP Cache-Control z parametrem stale-while-revalidate.
- Na poziomie UI, używając bibliotek typu swr/react-query, które trzymają lokalny cache i robią odświeżenia w tle.
SWR w nagłówku HTTP
Typowy nagłówek:
Cache-Control: public, s-maxage=60, stale-while-revalidate=300
Sens:
- W pierwszych 60 sekundach CDN zwraca świeżą wersję.
- Od 61 do 360 sekundy CDN może zwrócić przeterminowaną odpowiedź natychmiast, a w tle uruchomić żądanie do origin po nową wersję.
- Po 360 sekundach żądanie po nową treść staje się blokujące.
Użytkownik (i ChatGPT) dostaje odpowiedź natychmiast nawet w szczycie obciążenia, a ty w tle łagodnie odświeżasz cache. Dla GiftGenius to idealne np. dla „top‑zestawień prezentów na Nowy Rok” — nie zmieniają się co sekundę.
Przykład:
// app/api/gifts/top/route.ts
import { NextResponse } from "next/server";
export async function GET() {
const topGifts = [
{ id: "coffee_mug", title: "Kubek z napisem" },
{ id: "smart_led", title: "Inteligentna lampa" },
];
return NextResponse.json(topGifts, {
headers: {
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
},
});
}
SWR w widżecie UI (React)
Widżet GiftGenius żyje w piaskownicy ChatGPT i może używać dowolnego kodu React. Już umiesz wywoływać swoje API przez window.fetch. Dodajmy bibliotekę swr i zorganizujmy cache po stronie widżetu:
// widget/GiftTopList.tsx
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export function GiftTopList() {
const { data, isLoading } = useSWR(
"https://api.giftgenius.com/api/gifts/top",
fetcher,
{ revalidateOnFocus: false } // w czacie focus zmienia się dziwnie, wyłączmy
);
if (isLoading && !data) return <div>Wczytujemy pomysły...</div>;
return (
<ul>
{data?.map((gift: any) => (
<li key={gift.id}>{gift.title}</li>
))}
</ul>
);
}
Jak to działa:
- Przy pierwszym renderze idzie żądanie do naszego API.
- Wynik trafia do cache swr wewnątrz widżetu.
- Przy kolejnych renderach (albo nowych odpowiedziach, gdzie ChatGPT znów wstawi ten widżet z tym samym kluczem) dane są brane z cache. Użytkownik nie widzi „migania” ani spinnerów, a w tle może pójść odświeżenie.
W ten sposób łączymy dwa poziomy SWR:
- Na CDN/HTTP — aby nie obciążać origin.
- W UI — aby nie obciążać użytkownika.
Jeśli zebrać wszystko razem:
- Prosty Cache-Control (max-age/s-maxage) — warstwa bazowa: dajemy CDN i klientom prawo cache’owania odpowiedzi i redukujemy obciążenie.
- ETag + If-None-Match — dokładamy, gdy ważna jest oszczędność transferu dla ciężkich JSON, ale godzimy się na sieciowy round trip.
- stale-while-revalidate — włączamy, gdy liczy się natychmiastowy zwrot nawet lekko przeterminowanych danych (katalogi, top‑zestawienia).
- SWR w UI (biblioteka swr/react-query) — osobna warstwa do wygładzania przerysowań widżetu i lokalnego cache w piaskownicy ChatGPT.
7. Co cache’ować w GiftGenius i jak długo
Spróbujmy rozłożyć dane GiftGenius na „warstwy cache’owalności”.
Można cache’ować na poziomie CDN/edge
Wszystko, co jest jednakowe dla wszystkich (lub szerokich segmentów) i rzadko się zmienia:
- Statyki widżetu: JS/CSS, fonty, ikony — „na zawsze” (rok) z immutable.
- Struktura katalogów prezentów: kategorie, sekcje, filtry — minuty/godziny.
- Ogólne zestawienia („najlepsze pomysły dla kolegów do 50 $”) — minuty/kilkadziesiąt minut, zwłaszcza w szczytowych sezonach.
Tutaj idealnie pasują public, s-maxage + stale-while-revalidate.
Lepiej cache’ować w aplikacji/Redis
Bardziej dynamiczne, ale jednak powtarzalne dane:
- Wyniki ciężkich zewnętrznych API (np. kursy walut, aktualne ceny ze sklepu zewnętrznego).
- Często żądane segmenty rekomendacji (wg płci/wieku/okazji).
Tutaj CDN nie zawsze się sprawdzi, bo dane mogą zależeć od tokena/organizacji/tenanta. Cache’ujemy na poziomie MCP Gateway lub usług wewnętrznych REST: masz nad tym pełną kontrolę i nie mieszają się dane różnych użytkowników.
Nie wolno cache’ować (w cache’ach wspólnych)
To, co jest związane z konkretnym użytkownikiem:
- Osobiste zamówienia i ich statusy.
- Informacje płatnicze, adresy, email.
- Konkretnie dobrane rekomendacje na podstawie prywatnej historii zamówień (jeśli to wrażliwe).
To można cache’ować wyłącznie na poziomie aplikacji z ostrożną semantyką (i koniecznie bez wycieku między użytkownikami), ale na pewno nie w public cache CDN.
8. Warstwa edge: CDN kontra funkcje edge
Ważne, by nie mylić dwóch podobnych, ale różnych bytów:
- CDN / edge‑cache — przechowuje wcześniej policzone odpowiedzi, logiki tam prawie nie ma.
- Funkcje edge (Vercel Edge / Cloudflare Workers) — małe kawałki kodu wykonywane na węzłach edge.
Doświadczenie pokazuje: Edge ≠ Serverless. Wielu deweloperów próbuje wepchnąć tam ciężką logikę biznesową, wywołania LLM i obróbkę BLOB, a potem dziwi się timeoutom i limitom. Funkcje edge:
- Startują bardzo szybko (cold start prawie zerowy).
- Ale są mocno ograniczone CPU, czasem wykonania i dostępnością API (często bez pełnego Node.js, bez długich socketów itd.).
Kiedy funkcja edge — dobry pomysł
W kontekście GiftGenius i ChatGPT App funkcje edge są przydatne do:
- Lekkiego routingu: po nagłówkach locale, x-openai-user-location albo tenant ID zdecydować, do którego regionalnego klastra backendowego wysłać żądanie.
- Dodania prostych nagłówków, feature‑flagów, A/B‑routingu.
- Szybkich endpointów read‑only, które czytają dane z edge‑KV albo z cache CDN i praktycznie nic nie liczą.
Kiedy funkcja edge — zły pomysł
- Długie zapytania do zewnętrznych API.
- Wywołania modeli LLM.
- Złożona logika checkoutu.
- Narzędzia MCP z ciężką logiką biznesową.
Do tego masz zwykłe funkcje serverless Next.js (na przykład runtime = "nodejs") albo osobne usługi/klastry.
Przykład funkcji edge w Next.js 16
Zróbmy małą trasę GET /api/geo-router, która na podstawie nagłówka x-openai-user-location (umownie) zwróci, do którego regionalnego klastra uderzać.
// app/api/geo-router/route.ts
import { NextRequest, NextResponse } from "next/server";
export const runtime = "edge"; // uruchamiamy na edge
export function GET(req: NextRequest) {
const userLocation = req.headers.get("x-openai-user-location") ?? "US";
const cluster =
userLocation.startsWith("EU") ? "eu-gift-api" : "us-gift-api";
return NextResponse.json({ cluster }, {
headers: {
"Cache-Control": "public, s-maxage=300",
},
});
}
Taki endpoint:
- Działa bardzo szybko (edge).
- Nie robi nic skomplikowanego.
- Może być cache’owany w CDN.
9. Edge i cache w ogólnej architekturze GiftGenius
Zbierzmy wszystko w jeden obrazek.
flowchart TD
ChatGPT[(ChatGPT / User)]
CDN["CDN / Edge Cache (Vercel)"]
EdgeFn["Edge Functions (routing, feature‑flagi)"]
GW[MCP Gateway]
GiftAPI["Gift REST API Cluster"]
CommerceAPI["Commerce REST API Cluster"]
DB[(DB/External APIs)]
ChatGPT --> CDN
CDN -->|cache hit| ChatGPT
CDN -->|cache miss| EdgeFn
EdgeFn --> GW
GW --> GiftAPI
GW --> CommerceAPI
GiftAPI --> DB
CommerceAPI --> DB
Typowy scenariusz:
- Widżet ChatGPT żąda /api/gifts/categories.
- CDN sprawdza cache. Jeśli jest świeża lub „stale, ale jeszcze do użycia” wersja — od razu ją zwraca, nawet nie dotykając EdgeFn/GW.
- Jeśli cache’u nie ma — żądanie wpada do EdgeFn (jeśli włączony) i/lub od razu do GW.
- GW w razie potrzeby używa wewnętrznego cache Redis do ciężkich operacji albo woła wewnętrzne usługi REST, a dalej bazę.
- Odpowiedź wraca, trafia do CDN/edge‑cache i jest serwowana innym użytkownikom.
Takie ułożenie:
- Obniża latency dla widżetu i ChatGPT.
- Zmniejsza obciążenie MCP Gateway i klastrów backendowych.
- Redukuje koszt wywołań LLM/bazy (mniej powtórnych zapytań).
10. Niewielkie praktyczne fragmenty dla GiftGenius
Cache kategorii + Next.js revalidate
Dotąd mówiliśmy tylko o endpointach API. Ale Next.js daje podobne mechanizmy także dla samych stron — przez ISR (revalidate).
Przykład server component, który pobiera listę kategorii z revalidate = 60:
// app/(widget)/categories/page.tsx
export const revalidate = 60; // ISR: przebudowa co 60 s
async function fetchCategories() {
const res = await fetch("https://api.giftgenius.com/api/gifts/categories");
return res.json();
}
export default async function CategoriesPage() {
const categories = await fetchCategories();
return (
<ul>
{categories.map((c: any) => (
<li key={c.id}>{c.title}</li>
))}
</ul>
);
}
W produkcji Vercel będzie generować i cache’ować HTML tej strony, co jest przydatne w przypadkach, gdy twój widżet/interfejs jest otwierany nie tylko przez ChatGPT, lecz także jako zwykła strona WWW (np. panel debug lub landing).
Prosta aplikacja‑cache w usłudze backend
To już nie warstwa edge, lecz cache aplikacji (Redis/in‑memory wewnątrz twojego Gift REST API lub innej usługi backend). Ale warto pokazać, jak wygląda w najprostszym wydaniu:
// pseudokod wewnątrz Gift REST API
const cache = new Map<string, any>();
async function getGiftCategories() {
const key = "gift_categories_v1";
const cached = cache.get(key);
if (cached && Date.now() - cached.ts < 60_000) {
return cached.data; // 60 sekund cache
}
const data = await fetchRealCategories();
cache.set(key, { ts: Date.now(), data });
return data;
}
W boju oczywiście zamienisz Map na Redis/Memcached, ale idea jest ta sama: rzadziej chodzimy do bazy/zewnętrznego API.
Jeśli to wszystko ścisnąć do jednego zdania: najpierw jasno zdecyduj, co można cache’ować i gdzie (CDN, edge, Redis, baza), a dopiero potem włączaj „magiczne” flagi platformy. Cache to nie checkbox w konfigu — to część architektury: wpływa i na szybkość, i na stabilność, i na koszty.
11. Typowe błędy przy pracy z cache i warstwą edge
Błąd nr 1: „Cache’ujemy wszystko jak leci, byle szybciej”.
Klasyk: deweloper ustawia Cache-Control: public, s-maxage=3600 na wszystkie odpowiedzi JSON. Po paru godzinach okazuje się, że jeden użytkownik widzi zamówienia innego, a ChatGPT operuje starymi danymi o dostępności. Dla danych personalnych lub wrażliwych potrzebny jest albo private‑cache, albo całkowite wyłączenie cache CDN i trzymanie cache na poziomie aplikacji z ostrożną izolacją.
Błąd nr 2: Pomyłka między max-age i s-maxage.
Niektórzy ustawiają tylko max-age i oczekują, że CDN będzie cache’ował dokładnie tyle samo. W rzeczywistości max-age dotyczy przede wszystkim przeglądarki, a dla cache’u współdzielonego potrzebny jest s-maxage. Efekt: przeglądarka cache’uje, a CDN — nie, i origin dalej się dusi pod obciążeniem, chociaż „przecież włączyliśmy cache”. Właściwa droga — jawnie podawać s-maxage dla CDN.
Błąd nr 3: Oczekiwanie, że ETag wszystko przyspieszy.
ETag świetnie oszczędza transfer, zwłaszcza dla dużych plików JSON, ale sieciowy round trip i tak zostaje. W świecie ChatGPT App oznacza to: model i tak czeka na odpowiedź twojego serwera, choćby to było 304 bez ciała. Jeśli liczy się opóźnienie, potrzebne są edge‑cache + SWR, a ETag to mechanizm pomocniczy.
Błąd nr 4: Próba wciśnięcia ciężkiej logiki biznesowej do funkcji edge.
„Wywołujmy zewnętrzną LLM, liczmy skomplikowane zestawienia i chodźmy do trzech zewnętrznych API prosto z Vercel Edge — przecież tam jest szybko!” A potem zaczyna się ból: limity czasu wykonania, brak pełnego Node.js, dziwne błędy. Edge jest dobry do lekkiego routingu i A/B, a cała ciężka praca powinna iść do zwykłych funkcji serverless lub osobnych klastrów backend.
Błąd nr 5: Brak strategii unieważniania cache.
Zrobiliśmy cache „na godzinę”, wszystko lata. Potem biznes mówi: „zmieniliśmy ceny/kategorie/ograniczenia, czemu w ChatGPT wszystko po staremu?” Deweloperzy zaczynają ręcznie kręcić wajchami, czyścić cache i restartować serwisy. Dla ważnych danych trzeba z góry przemyśleć: jak będziesz zrzucać cache (po webhooku z panelu, po wersji, po kluczu), a nie liczyć na „samo się za godzinę odświeży”.
Błąd nr 6: Ignorowanie związku cache ↔ koszt.
Czasem deweloperzy myślą o cache tylko w kontekście szybkości. W ekosystemie LLM to także kwestia pieniędzy: każde zbędne wywołanie modelu i zewnętrznego API kosztuje. Bez cache MCP‑serwer może zacząć tłuc zewnętrzną usługę/model tak często, że miesięczny rachunek niemiło zaskoczy. Właściwe cache’owanie obniża i latency, i rachunek.
Błąd nr 7: Mieszanie danych różnych lokalizacji/regionów w jednym cache.
GiftGenius działa w kilku krajach, ale w cache używany jest jeden klucz top_gifts. Efekt: użytkownik z USA widzi ruble i sklepy z Rosji, a użytkownik z Europy — dolary i sklepy z USA. Przy cache’owaniu zawsze uwzględniaj klucze typu locale, currency, tenant w nazwie klucza cache albo w trasie (np. /api/{locale}/gifts/top).
Błąd nr 8: Pełna zależność od „magii” Next.js/platformy.
ISR, revalidate, automatyczny CDN — to wszystko świetne. Ale jeśli nie rozumiesz, co dokładnie dzieje się pod spodem, łatwo o niespodzianki. Na przykład strona pokazuje stary content, a API zwraca nowy; ChatGPT widzi jedno, a użytkownicy w przeglądarce — drugie. Warto poświęcić czas i zrozumieć, jak działają Cache-Control, ETag i wzorzec SWR, a Next.js traktować jako wygodną otoczkę, nie czarną skrzynkę.
Błąd nr 9: Brak rozróżnienia dev/staging/production w kwestii cache.
W środowisku deweloperskim cache często przeszkadza w debugowaniu („przecież zmieniłem dane, czemu ChatGPT wciąż widzi stare zestawienia?”). Warto mieć konfigurację, która w dev prawie wyłącza cache (albo daje TTL kilka sekund), a w production — włącza agresywne cache’owanie. W przeciwnym razie albo oszalejesz przy developmencie, albo przypadkiem wypchniesz produkcję bez cache i złapiesz sztorm żądań do wewnętrznych klastrów backend za MCP Gateway.
GO TO FULL VERSION