EdgeOne Pages KV is a globally distributed key-value persistent storage service deployed across multiple edge nodes. Data follows an eventual consistency model and synchronizes globally within 60 seconds.
⚠️ KV Storage is only available in Edge Functions — NOT supported in Node Functions.
Prerequisites (MUST complete before using KV)
You must enable KV Storage in the EdgeOne Pages console before writing any code.
Step 1 — Enable KV Storage
- Log in to the EdgeOne Pages console
- Navigate to the "KV Storage" page
- Click "Apply Now" to activate (free tier includes 1 GB storage)
Step 2 — Create a Namespace
A namespace is the unit of data isolation (like a separate database):
- On the "KV Storage" page, click "Create Namespace"
- Enter a name (e.g.
my-kv-store) - Wait for creation to complete
One account can create up to 10 namespaces.
Step 3 — Bind Namespace to Project
After creating a namespace, bind it to your EdgeOne Pages project and assign a variable name.
Option A — From KV Storage page:
- Open the namespace → "Associated Projects" tab
- Click "Bind Project"
- Select project and set the variable name (e.g.
my_kv)
Option B — From Project settings:
- Open project details → "KV Storage" menu
- Click "Bind Namespace"
- Select the namespace and set the variable name (e.g.
my_kv)
The variable name becomes a global variable in your Edge Function code. Different namespaces can use different variable names.
Core Concept: KV is a Global Variable
⚠️ CRITICAL: The KV namespace is accessed as a global variable — it is NOT on
context.env.
// ❌ WRONG — KV is NOT in context.env
export async function onRequest(context) {
const KV = context.env.KV;
await KV.get('key');
}
// ✅ CORRECT — my_kv is a global variable (name set when binding)
export async function onRequest(context) {
const value = await my_kv.get('key');
}
API Reference
All methods are called on the global KV variable (e.g. my_kv).
put — Write data
put(key: string, value: string | ArrayBuffer | ArrayBufferView | ReadableStream): Promise<void>
key: Key name (≤ 512 bytes, alphanumeric and underscores only)value: Value (≤ 25 MB)- Returns
Promise<void>— alwaysawaitto confirm write
await my_kv.put('count', '100');
await my_kv.put('user', JSON.stringify({ name: 'Alice' }));
get — Read data
get(key: string, type?: 'text' | 'json' | 'arrayBuffer' | 'stream'): Promise<string | object | ArrayBuffer | ReadableStream | null>
typedefaults to'text'; use'json'to auto-deserialize- Returns
nullif key does not exist
const count = await my_kv.get('count'); // '100' (string)
const user = await my_kv.get('user', 'json'); // { name: 'Alice' }
const buf = await my_kv.get('file', 'arrayBuffer');
delete — Remove data
delete(key: string): Promise<void>
await my_kv.delete('count');
list — Enumerate keys
list(options?: { prefix?: string; limit?: number; cursor?: string }): Promise<ListResult>
ListResult:
{
complete: boolean; // true if all keys have been returned
cursor: string; // pagination cursor for next page
keys: Array<{ name: string }>;
}
Single page:
const result = await my_kv.list({ prefix: 'user:' });
// result.keys → [{ name: 'user:123' }, { name: 'user:456' }]
Paginate through all keys:
let allKeys = [];
let result;
let cursor;
do {
result = await my_kv.list({ prefix: 'user:', limit: 256, cursor });
allKeys.push(...result.keys);
cursor = result.cursor;
} while (!result.complete);
Examples
Page view counter
// edge-functions/api/counter.js
export async function onRequest({ request }) {
// my_kv is a global variable — NOT context.env.my_kv
let count = await my_kv.get('page_views');
count = count ? Number(count) + 1 : 1;
await my_kv.put('page_views', String(count));
return new Response(JSON.stringify({ views: count }), {
headers: { 'Content-Type': 'application/json' },
});
}
CRUD with KV
// edge-functions/api/users/[id].js
export async function onRequestGet({ params }) {
const user = await my_kv.get(`user:${params.id}`, 'json');
if (!user) {
return new Response(JSON.stringify({ error: 'Not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
});
}
return new Response(JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' },
});
}
export async function onRequestPost({ params, request }) {
const data = await request.json();
await my_kv.put(`user:${params.id}`, JSON.stringify(data));
return new Response(JSON.stringify({ success: true }), {
status: 201,
headers: { 'Content-Type': 'application/json' },
});
}
export async function onRequestDelete({ params }) {
await my_kv.delete(`user:${params.id}`);
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' },
});
}
List all keys with prefix
// edge-functions/api/users.js
export async function onRequestGet() {
const users = [];
let result;
let cursor;
do {
result = await my_kv.list({ prefix: 'user:', limit: 256, cursor });
const fetched = await Promise.all(
result.keys.map(k => my_kv.get(k.name, 'json'))
);
users.push(...fetched.filter(Boolean));
cursor = result.cursor;
} while (!result.complete);
return new Response(JSON.stringify({ users }), {
headers: { 'Content-Type': 'application/json' },
});
}
Common Errors
| Symptom | Cause | Fix |
|---|---|---|
ReferenceError: my_kv is not defined |
KV not enabled or namespace not bound | Enable KV in console → create namespace → bind to project |
Accessing context.env.KV returns undefined |
KV is a global variable, not on context.env |
Use my_kv.get(...) directly (global) |
get() returns a Promise object, not the value |
Missing await |
Always await KV operations |
| KV works in production but fails locally | Project not linked | Run edgeone pages link |
| KV not available in Node Functions | KV only works in Edge Functions | Move KV logic to Edge Functions, or use an external database in Node Functions |
Limits
| Resource | Limit |
|---|---|
| Storage per account (free tier) | 1 GB |
| Key length | ≤ 512 bytes |
| Value size | ≤ 25 MB |
| Namespaces per account | 10 |
| Consistency | Eventual (≤ 60 s global sync) |
| List results per call | 256 max |
| Supported runtime | Edge Functions only |
Best Practices
- Use key prefixes to organize data:
user:123,cart:456,config:theme - Handle null —
get()returnsnullfor missing keys - Batch reads with
Promise.all()afterlist()instead of sequential awaits - Always wrap writes in try/catch for error handling
- Serialize objects — use
JSON.stringify()forput()and'json'type forget()
Local Development
# 1. Link to a remote project (required for KV access)
edgeone pages link
# 2. Start dev server
edgeone pages dev
# 3. Test
curl http://localhost:8088/api/counter
Production Deployment
- Enable KV Storage in the console
- Create a namespace
- Bind it to the project (set the variable name)
- Deploy:
edgeone pages deploy
Blob Storage
Distributed object storage for Makers Functions. Ideal for images, documents, user uploads, AI-generated content.
Limits: 1GB storage per free-tier account. Currently Node.js SDK only (
@edgeone/pages-blob).
Install
npm install @edgeone/pages-blob
Get a Store
import { getStore } from "@edgeone/pages-blob";
// Inside Makers Functions (auto-configured)
const store = getStore("my-store");
// With strong consistency
const store = getStore({ name: "my-store", consistency: "strong" });
Consistency Model
| Mode | Behavior | Use when |
|---|---|---|
"eventual" (default) |
Edge-cached, fastest, data syncs in seconds | Content display, tolerate brief staleness |
"strong" |
Bypasses cache, reads from primary storage | Counters, state machines, must read latest |
API Reference
| Method | Description |
|---|---|
store.set(key, value, opts?) |
Write an object (string / ArrayBuffer / Blob / ReadableStream) |
store.setJSON(key, value, opts?) |
Write JSON (auto-serialized) |
store.get(key, opts?) |
Read an object (returns null if not found) |
store.getWithHeaders(key, opts?) |
Read with response headers (etag, content-type) |
store.delete(key) |
Delete an object |
store.list(opts?) |
List objects (prefix filter, directory grouping, pagination) |
store.createUploadUrl(key, opts?) |
Generate pre-signed PUT URL for client-side direct upload |
Write
await store.set("photos/cat.jpg", imageBuffer);
await store.set("notes/todo.txt", "Buy milk");
await store.setJSON("user/preferences", { theme: "dark", lang: "zh-CN" });
// Only write if key doesn't exist
await store.set("init.json", data, { onlyIfNew: true });
Read
const text = await store.get("hello.txt"); // string
const json = await store.get("config.json", { type: "json" }); // parsed object
const buffer = await store.get("image.png", { type: "arrayBuffer" }); // ArrayBuffer
const stream = await store.get("large.zip", { type: "stream" }); // ReadableStream
// Strong consistency read
const fresh = await store.get("counter", { consistency: "strong" });
type options: "text" (default) | "json" | "arrayBuffer" | "blob" | "stream"
List
// All objects
const { blobs } = await store.list();
// By prefix
const { blobs } = await store.list({ prefix: "photos/" });
// Directory grouping
const { blobs, directories } = await store.list({ prefix: "photos/", directories: true });
// Manual pagination
const page1 = await store.list({ paginate: false });
const page2 = await store.list({ paginate: false, cursor: page1.cursor });
Client-Side Direct Upload
Generate a pre-signed URL for large file uploads without going through Functions:
const { url, key, expiresAt } = await store.createUploadUrl("files/photo.jpg", {
expireSeconds: 3600,
contentType: "image/jpeg",
});
// Client PUTs directly to `url`
Delete
await store.delete("photos/cat.jpg"); // silent if key doesn't exist
List All Stores
import { listStores } from "@edgeone/pages-blob";
const { stores } = await listStores(); // [{ name: "my-store" }, ...]