Перейти к содержимому

KV Store

KV Store — это распределённое хранилище ключ-значение (key-value), доступное для ваших приложений через Runtime API. Используйте его для хранения данных сессий, кэша, конфигураций и других небольших данных.

KV Store предоставляет:

  • Низкую задержку — данные хранятся на edge, рядом с вашим приложением
  • Глобальную доступность — данные доступны из любого региона
  • Простой API — базовые операции: get, set, delete, list, has
  • Изоляцию по окружениям — каждое окружение имеет свой KV Store
СценарийПример
Сессии пользователейХранение JWT токенов или сессионных данных
КэшКэширование API ответов, страниц
КонфигурацияFeature flags, настройки приложения
Rate limitingСчётчики запросов по IP
A/B тестированиеНазначение пользователей в группы
  1. Откройте страницу окружения

    Перейдите в проект → выберите окружение → вкладка KV Store

  2. Просмотр записей

    Все записи отображаются в таблице с ключом, значением и TTL (время жизни)

  3. Добавление записи

    Нажмите Add Entry и укажите:

    • Key — уникальный ключ
    • Value — значение (текст или JSON)
    • TTL — время жизни в секундах (опционально)
  4. Редактирование

    Нажмите на запись для изменения значения или TTL

  5. Удаление

    Нажмите иконку удаления рядом с записью для удаления

  • Используйте поле поиска для фильтрации по ключу
  • Поддерживается префиксный поиск (например, session: найдёт все ключи начинающиеся с session:)

Установите SDK и импортируйте модуль KV:

Окно терминала
npm install @onreza/runtime
# или: pnpm add / yarn add / bun add
import { kv, isKVAvailable } from '@onreza/runtime/kv';
// Получение значения
const value = await kv.get('my-key');
// Получение JSON
const data = await kv.get('config', { type: 'json' });
// Установка значения с TTL (в секундах)
await kv.set('session:123', { user: 'alice' }, { ttl: 86400 });
// Проверка существования ключа
const exists = await kv.has('my-key');
// Список ключей по префиксу с пагинацией
const { keys, cursor } = await kv.list({ prefix: 'session:', limit: 100 });
// Удаление
await kv.delete('my-key');
// Получение нескольких ключей за один запрос
const [user, profile] = await kv.getMany(
['user:123', 'profile:123'],
{ type: 'json' }
);
// Получение значения вместе с метаданными
const { value, metadata } = await kv.getWithMetadata('cache:user:123', { type: 'json' });

→ Полный API reference: Runtime SDK

KV Store работает в Compute Apps с тем же API:

// app/api/session/route.ts (Next.js App Router)
import { kv } from '@onreza/runtime/kv';
export async function GET(request: Request) {
const token = request.headers.get('authorization')?.replace('Bearer ', '');
if (!token) return new Response('Unauthorized', { status: 401 });
// Работает и в Edge Functions, и в Compute Apps
const session = await kv.get(`session:${token}`, { type: 'json' });
if (!session) return new Response('Session expired', { status: 401 });
return Response.json(session);
}

KV Store поддерживает хранение бинарных данных через Uint8Array:

// Запись бинарных данных
const binaryData = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // "Hello"
await kv.set('binary-key', binaryData);
// Чтение бинарных данных
const value = await kv.get('binary-key');
// value: Uint8Array [72, 101, 108, 108, 111]
// Сохранение изображения в KV Store
async function storeImage(key, imageBuffer) {
// imageBuffer — Uint8Array или Buffer
await kv.set(key, new Uint8Array(imageBuffer));
}
// Получение изображения
async function getImage(key) {
const data = await kv.get(key);
if (data instanceof Uint8Array) {
return data; // бинарные данные
}
return null;
}
// Использование
await storeImage('avatar:user123', imageBuffer);
const avatar = await getImage('avatar:user123');
МетрикаHobbyPro
StorageНедоступен2 GB

Стоимость сверх лимита (Pro): ₽20/ГБ. Подробнее: Лимиты и квоты.

ПараметрЛимит
Размер ключа512 байт
Размер значения1 MB (включая бинарные данные)
TTL минимум60 секунд
TTL максимум1 год
Ключей в batch (getMany)100
Ключей в list1000 за запрос

Используйте префиксы для организации данных:

session:<user-id> # Сессии пользователей
cache:<api-endpoint> # Кэш API
config:feature-flags # Конфигурация
rate:<ip-address> # Rate limiting
blob:<file-id> # Бинарные данные

Устанавливайте TTL для временных данных:

  • Сессии — 24 часа
  • Кэш — 5-15 минут
  • Rate limiting — 1 минута
// Сессия на 24 часа
await kv.set(`session:${userId}`, sessionData, { ttl: 86400 });
// Кэш на 5 минут
await kv.set(`cache:${url}`, response, { ttl: 300 });
import { kv } from '@onreza/runtime/kv';
try {
const value = await kv.get('my-key');
if (value === null) {
// Ключ не найден
return defaultValue;
}
return value;
} catch (error) {
// Fallback при недоступности KV
console.error('KV error:', error);
return defaultValue;
}

Самый распространённый паттерн — проверить кэш, при промахе загрузить данные и сохранить:

import { kv } from '@onreza/runtime/kv';
async function getCachedData<T>(key: string, fetcher: () => Promise<T>, ttl = 300): Promise<T> {
// 1. Проверяем кэш
const cached = await kv.get(key, { type: 'json' });
if (cached !== null) return cached as T;
// 2. Загружаем данные
const data = await fetcher();
// 3. Сохраняем в кэш
await kv.set(key, data, { ttl });
return data;
}
// Использование
const user = await getCachedData(
`cache:user:${userId}`,
() => fetchUserFromAPI(userId),
600 // 10 минут
);

При каждой мутации обновляем и источник данных, и кэш одновременно:

import { kv } from '@onreza/runtime/kv';
async function updateUserProfile(userId: string, data: { name: string }) {
// 1. Записываем в источник данных (БД, внешний API и т.д.)
await fetch(`https://api.example.com/users/${userId}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
// 2. Сразу обновляем кэш — читатели получат свежие данные
await kv.set(`cache:user:${userId}`, data, { ttl: 600 });
}

Отдаём устаревшие данные мгновенно, обновляем кэш в фоне:

import { kv } from '@onreza/runtime/kv';
async function getWithSWR<T>(key: string, fetcher: () => Promise<T>, ttl = 300): Promise<T> {
const cached = await kv.get(key, { type: 'json' }) as T | null;
// Фоновое обновление (не ждём результата)
const refreshPromise = fetcher().then(data =>
kv.set(key, data, { ttl })
).catch(() => {}); // игнорируем ошибки фонового обновления
if (cached !== null) return cached;
// Кэш пуст — ждём загрузки
const data = await fetcher();
await kv.set(key, data, { ttl });
return data;
}

Удаление группы связанных ключей:

import { kv } from '@onreza/runtime/kv';
async function invalidateByPrefix(prefix: string) {
let cursor: string | undefined;
do {
const result = await kv.list({ prefix, limit: 100, cursor });
await Promise.all(result.keys.map(key => kv.delete(key)));
cursor = result.cursor;
} while (cursor);
}
// Удалить весь кэш пользователя
await invalidateByPrefix(`cache:user:${userId}`);

Если KV недоступен, приложение продолжает работать:

import { kv, isKVAvailable } from '@onreza/runtime/kv';
async function getData(key: string, fetcher: () => Promise<unknown>) {
if (isKVAvailable()) {
try {
const cached = await kv.get(key, { type: 'json' });
if (cached !== null) return cached;
} catch {
// KV недоступен — загружаем напрямую
}
}
const data = await fetcher();
// Попытка сохранить в кэш (не критично при ошибке)
if (isKVAvailable()) {
kv.set(key, data, { ttl: 300 }).catch(() => {});
}
return data;
}

Полный паттерн: создание, проверка, продление и завершение сессии через KV.

lib/session.ts
import { kv } from '@onreza/runtime/kv';
import { cookies } from 'next/headers';
const SESSION_TTL = 86400; // 24 часа
const SESSION_COOKIE = 'session_id';
interface Session {
userId: string;
email: string;
role: string;
}
export async function createSession(data: Session): Promise<string> {
const sessionId = crypto.randomUUID();
await kv.set(`session:${sessionId}`, data, { ttl: SESSION_TTL });
return sessionId;
}
export async function getSession(): Promise<Session | null> {
const sessionId = (await cookies()).get(SESSION_COOKIE)?.value;
if (!sessionId) return null;
const session = await kv.get(`session:${sessionId}`, { type: 'json' }) as Session | null;
// Продлеваем TTL при каждом запросе
if (session) {
await kv.set(`session:${sessionId}`, session, { ttl: SESSION_TTL });
}
return session;
}
export async function destroySession(): Promise<void> {
const sessionId = (await cookies()).get(SESSION_COOKIE)?.value;
if (sessionId) await kv.delete(`session:${sessionId}`);
}
app/api/login/route.ts
import { createSession } from '@/lib/session';
import { cookies } from 'next/headers';
export async function POST(request: Request) {
const { email, password } = await request.json();
// Проверка учётных данных (ваша логика)
const user = await verifyCredentials(email, password);
if (!user) return Response.json({ error: 'Invalid credentials' }, { status: 401 });
const sessionId = await createSession({
userId: user.id,
email: user.email,
role: user.role,
});
(await cookies()).set('session_id', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 86400,
});
return Response.json({ ok: true });
}
app/api/me/route.ts
import { getSession } from '@/lib/session';
export async function GET() {
const session = await getSession();
if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 });
return Response.json({ userId: session.userId, role: session.role });
}

Cache-aside с инвалидацией и getMany для batch-загрузки.

lib/cache.ts
import { kv } from '@onreza/runtime/kv';
import { prisma } from '@onreza/database';
const USER_TTL = 3600; // 1 час
export async function getUserById(id: string) {
const cacheKey = `cache:user:${id}`;
// Проверяем кэш
const cached = await kv.get(cacheKey, { type: 'json' });
if (cached) return cached;
// Промах — запрашиваем из БД
const user = await prisma.user.findUnique({ where: { id } });
if (!user) return null;
await kv.set(cacheKey, user, { ttl: USER_TTL });
return user;
}
// Batch-загрузка нескольких пользователей
export async function getUsersByIds(ids: string[]) {
const cacheKeys = ids.map(id => `cache:user:${id}`);
const cached = await kv.getMany(cacheKeys, { type: 'json' });
// Находим отсутствующие в кэше
const missing = ids.filter((_, i) => cached[i] === null);
if (missing.length > 0) {
const dbUsers = await prisma.user.findMany({
where: { id: { in: missing } },
});
// Сохраняем в кэш параллельно
await Promise.all(
dbUsers.map(user =>
kv.set(`cache:user:${user.id}`, user, { ttl: USER_TTL })
)
);
// Подставляем в результат
for (const user of dbUsers) {
const i = ids.indexOf(user.id);
if (i !== -1) cached[i] = user;
}
}
return cached;
}
// Инвалидация при обновлении
export async function updateUser(id: string, data: { name?: string; email?: string }) {
const user = await prisma.user.update({ where: { id }, data });
await kv.delete(`cache:user:${id}`);
return user;
}

Ограничение числа запросов на пользователя через KV-счётчик.

lib/rate-limit.ts
import { kv } from '@onreza/runtime/kv';
interface RateLimitResult {
allowed: boolean;
remaining: number;
resetIn: number; // секунды до сброса
}
export async function rateLimit(
identifier: string,
limit = 60,
windowSec = 60
): Promise<RateLimitResult> {
const key = `ratelimit:${identifier}`;
const counter = await kv.get(key, { type: 'json' }) as
| { count: number; expiresAt: number } | null;
const now = Math.floor(Date.now() / 1000);
if (!counter || counter.expiresAt <= now) {
// Первый запрос в окне
await kv.set(key, { count: 1, expiresAt: now + windowSec }, { ttl: windowSec });
return { allowed: true, remaining: limit - 1, resetIn: windowSec };
}
if (counter.count >= limit) {
return { allowed: false, remaining: 0, resetIn: counter.expiresAt - now };
}
await kv.set(
key,
{ count: counter.count + 1, expiresAt: counter.expiresAt },
{ ttl: counter.expiresAt - now }
);
return {
allowed: true,
remaining: limit - counter.count - 1,
resetIn: counter.expiresAt - now,
};
}
// middleware.ts (Next.js)
import { rateLimit } from '@/lib/rate-limit';
export async function middleware(request: Request) {
const ip = request.headers.get('x-forwarded-for') ?? 'unknown';
const { allowed, remaining, resetIn } = await rateLimit(`ip:${ip}`, 60, 60);
if (!allowed) {
return new Response('Too Many Requests', {
status: 429,
headers: { 'Retry-After': String(resetIn) },
});
}
const response = await fetch(request);
response.headers.set('X-RateLimit-Remaining', String(remaining));
return response;
}
Environment VariablesKV Store
НазначениеКонфигурация, секретыДинамические данные
ИзменениеТребует redeployМгновенно доступно
ДоступЧтение только в приложенииЧтение/запись в runtime
ОбъёмДо 4 KB на значениеДо 1 MB на значение
КоличествоДо 200 переменныхДо 100 000 записей (Pro)
Бинарные данныеНе поддерживаетсяПоддерживается (Uint8Array)
  • Проверьте что окружение существует
  • Убедитесь что вы используете план Pro
  • Проверьте что код выполняется внутри Edge Function или Compute App
  • Вы превысили лимит запросов
  • Добавьте кэширование на уровне приложения
  • Ключ мог истечь (TTL)
  • Проверьте правильность написания ключа
  • Убедитесь что вы обращаетесь к правильному окружению