1. Навіщо взагалі замислюватися про кеш і edge у ChatGPT App
У класичному вебзастосунку ви теж дбаєте про швидкість, але там користувач принаймні бачить спінер. У ChatGPT App ситуація цікавіша: користувач спілкується з моделлю й інколи вирішує викликати ваш App. Віджет має зʼявитися й досить швидко показати щось корисне.
Практика проста: latency (затримка) = гроші. Що довше ви відповідаєте, то вищий шанс, що користувач піде. А зайві виклики LLM і бекенда — це прямі витрати на моделі та інфраструктуру. Кешування зменшує і те, і інше.
Додамо специфіку ChatGPT Apps:
- Запити від ChatGPT до вашого App йдуть мережею та проходять через кілька шарів. Кожна мілісекунда на кожному етапі додається до сумарної затримки.
- У MCP/HTTP‑ендпойнтів є реальні тайм‑аути (зокрема у serverless‑функцій Vercel і edge‑функцій). Якщо ви не встигаєте, ChatGPT бачить помилку й може навіть почати «галюцинувати» відповідь.
- Багато даних у GiftGenius не змінюються щосекунди: структура каталогу подарунків, добірки «топ‑ідей» для різних сегментів, налаштування функцій. Немає сенсу щоразу навантажувати базу чи зовнішнє API повторними запитами.
Саме тут і стають у пригоді:
- CDN і edge‑кеш, щоб швидко роздавати статику та кешований JSON.
- HTTP‑кеш із Cache-Control/ETag/SWR, щоб повторні запити були швидшими й дешевшими.
- Edge‑функції Vercel, щоб виконувати просту лінійну логіку якомога ближче до ChatGPT і користувача, але не перетворювати їх на «мінібекенд».
2. Анатомія затримок у GiftGenius і точки кешування
Спершу корисно чесно розібратися, де саме виникає затримка (latency).
sequenceDiagram
participant User as Користувач
participant ChatGPT as ChatGPT
participant App as ChatGPT App (Apps SDK)
participant GW as MCP Gateway / Edge
participant GiftAPI as Gift REST API / мікросервіс подарунків
participant DB as Каталог/БД
User->>ChatGPT: "Підбери подарунок братові"
ChatGPT->>App: Виклик інструмента + рендер віджета
App->>GW: HTTP / MCP запит (категорії, добірки)
GW->>GiftAPI: HTTP (REST)
GiftAPI->>DB: Запит каталогу/рекомендацій
DB-->>GiftAPI: Відповідь
GiftAPI-->>GW: Відповідь (JSON)
GW-->>App: Відповідь (JSON)
App-->>ChatGPT: Віджет з результатами
ChatGPT-->>User: Повідомлення + UI
Де тут можна «зрізати кут»?
- Між ChatGPT і вашим периметром — CDN/edge‑кеш (Vercel CDN/Edge Network), який може роздавати незмінні асети віджета та кешований JSON без походу на ваш origin‑сервер.
- Між Gateway і внутрішніми REST/HTTP‑сервісами (Gift REST API, Commerce REST API тощо) та базою — кеш застосунку (Redis/у памʼяті/БД‑кеш), щоб не ганяти однакові запити (наприклад, «список категорій подарунків») по десять разів.
У цій лекції ми зосередимося саме на HTTP/edge‑шарі, бо він найближчий до ChatGPT і Vercel.
3. Види кешу в нашій архітектурі
Оскільки наша архітектура — це «багатошаровий пиріг», то й кешів у ній кілька.
| Тип кешу | Де живе | Для чого підходить |
|---|---|---|
| Браузерний кеш | Усередині клієнта ChatGPT (браузер/desktop) | Статика віджета, іконки, шрифти (обмежено контрольований) |
| CDN / edge‑кеш | На edge‑вузлах Vercel/Cloudflare | Статика + загальний JSON (категорії, конфігурації, загальні добірки) |
| Кеш застосунку | Усередині вашого MCP Gateway або бекенд‑сервісів (Redis, in‑memory) | Результати важких запитів до БД/зовнішніх API |
| БД‑кеш/матеріалізація | У самій БД (materialized views тощо) | Попередньо обчислені агрегати, аналітика |
Зараз зосередимося на перших двох: HTTP‑кеш + CDN/edge.
4. HTTP‑кеш: Cache-Control, max-age і s-maxage
Передусім HTTP‑кешем керує заголовок Cache-Control. Він визначає, чи може браузер/клієнт ChatGPT та/або CDN кешувати вашу відповідь — і як довго.
Головне:
- max-age — скільки секунд браузер може кешувати відповідь.
- s-maxage — скільки секунд може кешувати спільний кеш (CDN/проксі).
- public — відповідь можна кешувати у спільному кеші.
- private — відповідь лише для конкретного клієнта; CDN її не кешує.
У GiftGenius, наприклад:
- JS/CSS/шрифти віджета — версіоновані файли (з хешем у назві), їх можна сміливо віддавати з Cache-Control: max-age=31536000, immutable.
- JSON зі списком категорій подарунків — однаковий для всіх користувачів, тож логічно ставити public, s-maxage=60 (або більше).
Найпростіший обробник Next.js (Route Handler) для GET /api/gifts/categories, який кешується на CDN на 60 секунд:
// app/api/gifts/categories/route.ts
import { NextResponse } from "next/server";
export const runtime = "nodejs"; // звичайна serverless‑функція
export async function GET() {
// тут могли б ходити в БД/зовнішнє API
const categories = [
{ id: "for_brother", title: "Подарунки братові" },
{ id: "for_mom", title: "Подарунки мамі" },
];
return NextResponse.json(categories, {
headers: {
// дозволяємо CDN кешувати на 60 секунд
"Cache-Control": "public, s-maxage=60",
},
});
}
Vercel CDN зберігатиме відповідь 60 секунд, і всі запити ChatGPT за цим JSON упродовж цього вікна взагалі не дійдуть до вашої функції. Це миттєво й дешево.
5. ETag: відбиток контенту і 304 Not Modified
ETag — це умовний «відбиток пальця» ресурсу, зазвичай хеш вмісту. Працює це так:
- Сервер віддає відповідь із заголовком ETag: "v1-abc123".
- Наступного разу клієнт надсилає заголовок If-None-Match: "v1-abc123".
- Якщо сервер вважає, що контент не змінився, він відповідає 304 Not Modified без тіла.
Важливо: ETag економить трафік, але не обовʼязково зменшує затримку, бо все одно потрібен повний мережевий цикл (round-trip) до сервера. У контексті ChatGPT Apps це корисно для «важких» JSON‑відповідей, але очікувати дива швидкості лише від ETag не варто — для цього краще SWR і edge‑кеш.
Приклад простого ETag у Next.js‑обробнику (без крипто‑хешів, щоб не занурюватися в деталі):
// 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) {
// Контент не змінився — віддаємо 304
return new NextResponse(null, { status: 304, headers: { ETag: ETAG } });
}
return NextResponse.json(CONFIG, {
headers: {
ETag: ETAG,
"Cache-Control": "public, s-maxage=300",
},
});
}
У реальному житті ви, звісно, обчислюватимете ETag із хешу даних або використовуватимете версію запису в БД.
6. Stale‑While‑Revalidate (SWR): швидко і достатньо свіжо
SWR — це підхід «покажи старе одразу, а нове підтягни у фоні». Його можна реалізувати:
- На рівні HTTP‑заголовка Cache-Control з параметром stale-while-revalidate.
- На рівні UI, використовуючи бібліотеки на кшталт swr/react-query, які тримають локальний кеш і роблять повторні запити у фоні.
SWR у HTTP‑заголовку
Типовий заголовок:
Cache-Control: public, s-maxage=60, stale-while-revalidate=300
Сенс:
- У перші 60 секунд CDN віддає свіжу версію.
- Від 61‑ї до 360‑ї секунди CDN може віддати застарілу відповідь миттєво, а у фоні запустити запит до origin по нову версію.
- Після 360 секунд запит по новий контент стає блокувальним.
Користувач (і ChatGPT) отримує відповідь миттєво навіть на піку навантаження, а ви у фоні мʼяко оновлюєте кеш. Для GiftGenius це ідеально, наприклад, для «топ‑добірок подарунків до Нового року» — вони не змінюються щосекунди.
Приклад:
// app/api/gifts/top/route.ts
import { NextResponse } from "next/server";
export async function GET() {
const topGifts = [
{ id: "coffee_mug", title: "Горнятко з написом" },
{ id: "smart_led", title: "Розумна лампа" },
];
return NextResponse.json(topGifts, {
headers: {
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
},
});
}
SWR у UI‑віджеті (React)
Віджет GiftGenius працює в пісочниці ChatGPT і може використовувати будь‑який React‑код. Ви вже вмієте викликати свій API через window.fetch. Додамо бібліотеку swr і організуємо кеш на боці віджета:
// 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 } // в чаті фокус змінюється дивно, вимкнемо
);
if (isLoading && !data) return <div>Завантажуємо ідеї...</div>;
return (
<ul>
{data?.map((gift: any) => (
<li key={gift.id}>{gift.title}</li>
))}
</ul>
);
}
Як це працює:
- Під час першого рендеру йде запит до нашого API.
- Результат кладеться в кеш swr усередині віджета.
- За повторних рендерів (або нових відповідей, де ChatGPT знову вставить цей віджет із тим самим ключем) дані беруться з кешу. Користувач не бачить «миготіння» і спінерів, а у фоні може піти оновлення.
Так ми поєднуємо два рівні SWR:
- На CDN/HTTP — щоб не навантажувати origin.
- В UI — щоб не навантажувати користувача.
Якщо зібрати все разом:
- Простий Cache-Control (max-age/s-maxage) — базовий шар: даємо CDN і клієнтам право кешувати відповіді та знижуємо навантаження.
- ETag + If-None-Match — додаємо, коли важливо економити трафік для «важких» JSON, але при цьому миримося з мережевим round-trip.
- stale-while-revalidate — вмикаємо, коли нам важлива миттєва віддача навіть трохи застарілих даних (каталоги, топ‑добірки).
- SWR в UI (бібліотека swr/react-query) — окремий шар, щоб згладжувати повторні рендери віджета й тримати локальний кеш у пісочниці ChatGPT.
7. Що кешувати в GiftGenius і як довго
Спробуємо розкласти дані GiftGenius за «шарами кешування».
Можна кешувати на рівні CDN/edge
Це все, що однаково для всіх (або для широких сегментів) і рідко змінюється:
- Статика віджета: JS/CSS, шрифти, іконки — умовно «назавжди» (рік) з immutable.
- Структура каталогів подарунків: категорії, розділи, фільтри — на хвилини/години.
- Загальні добірки («найкращі ідеї для колег до 50 $») — на хвилини/десятки хвилин, особливо в пікові сезони.
Тут ідеально підходять public, s-maxage + stale-while-revalidate.
Краще кешувати в застосунку/Redis
Дані, що динамічніші, але все одно часто повторюються:
- Результати «важких» зовнішніх API (наприклад, ви берете курси валют або актуальні ціни із зовнішнього магазину).
- Часто запитувані сегменти рекомендацій (за статтю/віком/приводом).
Тут CDN уже не завжди підійде, адже дані можуть залежати від токена/організації/тенанта. Кешуйте на рівні MCP Gateway або внутрішніх REST‑сервісів: це повністю під вашим контролем і не змішує дані різних користувачів.
Не можна кешувати (у спільних кешах)
Те, що привʼязано до конкретного користувача:
- Особисті замовлення та їхні статуси.
- Платіжна інформація, адреси, електронна пошта.
- Конкретні рекомендації на основі приватної історії замовлень (якщо вона чутлива).
Це можна кешувати лише на рівні застосунку з акуратною семантикою (і обовʼязково без витоків між користувачами), але точно не в public CDN‑кеші.
8. Edge‑шар: CDN проти edge‑функцій
Важливо не плутати дві схожі, але різні речі:
- CDN / edge‑кеш — зберігає заздалегідь обчислені відповіді; логіки там майже немає.
- Edge‑функції (Vercel Edge / Cloudflare Workers) — невеликі фрагменти коду, що виконуються на edge‑вузлах.
Досвід показує: Edge ≠ Serverless. Багато розробників намагаються перенести туди важку бізнес‑логіку, запити до LLM і обробку BLOB, а потім дивуються тайм‑аутам і лімітам. Edge‑функції:
- Стартують дуже швидко (cold start майже нульовий).
- Але мають жорсткі обмеження на CPU, час виконання й доступні API (часто без повноцінного Node.js, без довготривалих сокетів тощо).
Коли edge‑функція — хороша ідея
У контексті GiftGenius і ChatGPT App edge‑функції корисні для:
- Легкої маршрутизації: за заголовками locale, x-openai-user-location або tenant ID визначити, до якого регіонального backend‑кластера скеровувати запит.
- Додавання простих заголовків, фіч‑прапорців, A/B‑маршрутизації.
- Швидких read‑only‑ендпойнтів, які читають дані з edge‑KV або з CDN‑кешу і практично нічого не обчислюють.
Коли edge‑функція — погана ідея
- Довгі запити до зовнішніх API.
- Виклики LLM‑моделей.
- Складна логіка checkoutʼу.
- MCP‑інструменти з важкою бізнес‑логікою.
Для всього цього у вас є звичайні serverless‑функції Next.js (наприклад, runtime = "nodejs") або взагалі окремі сервіси/кластери.
Приклад edge‑функції в Next.js 16
Зробимо маленький маршрут GET /api/geo-router, який за заголовком x-openai-user-location (умовно) поверне, до якого регіонального кластера варто звертатися.
// app/api/geo-router/route.ts
import { NextRequest, NextResponse } from "next/server";
export const runtime = "edge"; // виконуємо на 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",
},
});
}
Такий ендпойнт:
- Працює дуже швидко (edge).
- Не робить нічого складного.
- Може кешуватися CDN.
9. Edge і кеш у загальній архітектурі GiftGenius
Зберімо все в одну картинку.
flowchart TD
ChatGPT[(ChatGPT / Користувач)]
CDN["CDN / Edge Cache (Vercel)"]
EdgeFn["Edge Functions (маршрутизація, фіч‑прапорці)"]
GW[MCP Gateway]
GiftAPI["Gift REST API Cluster"]
CommerceAPI["Commerce REST API Cluster"]
DB[(DB/External APIs)]
ChatGPT --> CDN
CDN -->|кеш‑хіт| ChatGPT
CDN -->|кеш‑міс| EdgeFn
EdgeFn --> GW
GW --> GiftAPI
GW --> CommerceAPI
GiftAPI --> DB
CommerceAPI --> DB
Типовий сценарій:
- ChatGPT‑віджет запитує /api/gifts/categories.
- CDN перевіряє кеш. Якщо там є свіжа або «stale, але ще придатна» версія — одразу віддає її, навіть не чіпаючи EdgeFn/GW.
- Якщо кешу немає — запит потрапляє в EdgeFn (якщо він увімкнений) і/або одразу в GW.
- GW за потреби використовує внутрішній Redis‑кеш для важких операцій або ходить у внутрішні REST‑сервіси і далі — в БД.
- Відповідь повертається назад, потрапляє в CDN/edge‑кеш і роздається іншим користувачам.
Така побудова:
- Знижує затримку для віджета і ChatGPT.
- Скорочує навантаження на MCP Gateway і бекенд‑кластери.
- Зменшує вартість викликів LLM/БД (менше повторних запитів).
10. Невеликі практичні фрагменти для GiftGenius
Кеш категорій + Next.js revalidate
До цього ми говорили лише про API‑ендпойнти. Але Next.js дає схожі механізми і для самих сторінок — через ISR (revalidate).
Приклад server component, який отримує список категорій із revalidate = 60:
// app/(widget)/categories/page.tsx
export const revalidate = 60; // ISR: перескладаємо раз на 60 сек
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>
);
}
У робочому середовищі Vercel генеруватиме й кешуватиме HTML‑вивід цієї сторінки. Це корисно в ситуаціях, коли ваш віджет/інтерфейс відкритий не лише через ChatGPT, а й як звичайна вебсторінка (наприклад, панель для налагодження або лендинг).
Простий кеш застосунку у бекенд‑сервісі
Це вже не edge‑шар, а кеш застосунку (Redis/in‑memory усередині вашого Gift REST API або іншого бекенд‑сервісу). Але доречно показати, як він виглядає в найпростішому варіанті:
// псевдокод усередині 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 секунд кеш
}
const data = await fetchRealCategories();
cache.set(key, { ts: Date.now(), data });
return data;
}
У реальному проєкті ви, звісно, заміните Map на Redis/Memcached, але ідея та сама: менше ходимо в БД/зовнішнє API.
Якщо стиснути все це до одного тезису, він буде таким: спочатку чітко вирішіть, що можна кешувати і де (CDN, edge, Redis, БД), а вже потім вмикайте «магічні» прапорці платформи. Кеш — це не галочка в конфігу, а частина архітектури. Він впливає і на швидкість, і на стабільність, і на витрати.
11. Типові помилки під час роботи з кешем і edge‑шаром
Помилка № 1: «Кешуємо все підряд, аби швидше».
Класика: розробник ставить Cache-Control: public, s-maxage=3600 узагалі на всі JSON‑відповіді. За кілька годин зʼясується, що один користувач бачить замовлення іншого, а ChatGPT починає оперувати застарілими даними про наявність товару. Для персональних або чутливих даних потрібен або private‑кеш, або повне вимкнення CDN‑кешу та кешування на рівні застосунку з акуратною ізоляцією.
Помилка № 2: плутанина між max-age і s-maxage.
Дехто ставить лише max-age і очікує, що CDN кешуватиме рівно стільки ж. Насправді max-age стосується передусім браузера, а для спільного кешу потрібен s-maxage. У підсумку браузер кешує, а CDN — ні, і origin продовжує задихатися під навантаженням, хоча «кеш же поставили». Правильний шлях — явно вказувати s-maxage для CDN.
Помилка № 3: очікування, що ETag пришвидшить усе на світі.
ETag чудово економить трафік, особливо для великих JSON‑файлів, але мережевий round-trip усе одно залишається. У світі ChatGPT App це означає: модель однаково чекає відповіді від вашого сервера, хай і 304 без тіла. Якщо вам важлива саме затримка, потрібні edge‑кеш + SWR, а ETag — допоміжний механізм.
Помилка № 4: спроба перенести важку бізнес‑логіку в edge‑функції.
«Давайте викликатимемо зовнішню LLM, рахуватимемо складні добірки і ходитимемо у три зовнішні API прямо з Vercel Edge — там же швидко!» Потім починається біль: ліміти за часом виконання, відсутність нормального Node.js, дивні помилки. Edge добрий для легкої маршрутизації й A/B, а вся важка робота має йти в звичайні serverless‑функції або окремі бекенд‑кластери.
Помилка № 5: відсутність стратегії інвалідації кешу.
Зробили кеш «на годину» — усе літає. Потім бізнес каже: «ми змінили ціни/категорії/обмеження, чому в ChatGPT все по‑старому?» Розробники починають натискати кнопки вручну, чистити кеші й перезапускати сервіси. Для важливих даних потрібно заздалегідь продумати, як ви скидатимете кеш (по webhookʼу з адмінки, за версією, за ключем), а не розраховувати на «воно саме за годину оновиться».
Помилка № 6: ігнорування звʼязку кеш ↔ вартість.
Іноді розробники думають про кеш лише як про швидкість. В екосистемі LLM це ще й про гроші: кожен зайвий виклик до моделі та зовнішнього API коштує. Без кешу MCP‑сервер може почати звертатися до зовнішнього сервісу/моделі так часто, що рахунок за місяць неприємно здивує. Правильне кешування знижує і затримку, і витрати.
Помилка № 7: змішування даних різних локалей/регіонів в одному кеші.
GiftGenius працює в кількох країнах, але в кеші використовується один ключ top_gifts. Підсумок: користувач зі США бачить гривні або іншу неочікувану валюту й магазини іншої країни, а користувач із Європи — долари та магазини зі США. Під час кешування завжди враховуйте ключі на кшталт locale, currency, tenant у назві кеш‑ключа або в маршруті (наприклад, /api/{locale}/gifts/top).
Помилка № 8: повна залежність від «магії» Next.js/платформи.
ISR, revalidate, автоматичний CDN — усе це круто. Але якщо ви не розумієте, що саме відбувається «під капотом», легко отримати неочікувані ефекти. Наприклад, сторінка показує старий контент, а API віддає новий; ChatGPT бачить одне, а користувачі в браузері — інше. Варто витратити час і розібратися, як працюють Cache-Control, ETag і SWR‑патерн, а Next.js використовувати як зручну обгортку, а не як «чорну скриньку».
Помилка № 9: відсутність різниці між dev/staging/production у кеші.
У dev‑середовищі кеш часто заважає відладці («я ж змінив дані, чому ChatGPT усе ще бачить старі добірки?»). Корисно мати конфіг, який у dev кеш майже вимикає (або робить TTL у кілька секунд), а в production — вмикає агресивніше кешування. Інакше ви або витрачаєте зайві час і нерви під час розробки, або випадково випускаєте production без кешу і ловите шторм запитів до внутрішніх бекенд‑кластерів за MCP Gateway.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ