Store and retrieve unstructured data like file uploads, images, documents, JSON, and cache-like state with Netlify Blobs. It is a zero-config key/value store accessible from Functions, Edge Functions, and Build Plugins. Reach for this when you handle a file or image upload, persist Background Function output like sitemaps or processed media, cache API responses, store per-deploy assets, or need a simple key/value store from a serverless function. Covers creating site and deploy stores, reading and writing values and JSON, listing keys with metadata, atomic conditional writes, consistency modes, region selection, and file-based uploads. Not for per-user, transactional, or relational data — use Netlify DB for that.
The canonical home for this skill is netlify-blobs in netlify/context-and-tools
Modern syntax — import from @netlify/blobs, open a store, operate on it:
import { getStore } from "@netlify/blobs";
const store = getStore("file-uploads"); // site-wide
await store.set(key, value, { metadata: { … } });
const entry = await store.get(key); // null if missingDeploy-specific isolation:
import { getDeployStore } from "@netlify/blobs";
const store = getDeployStore("file-uploads");Requires Fetch API (Node.js 18+). Functions written in Go cannot access Blobs. Not for per-user/transactional/relational data — use Netlify DB.
getStore) are shared across ALL deploy contexts. Code on a deploy preview reads, overwrites, and deletes production data. Never run destructive tests or seed throwaway data from previews — use getDeployStore() or a context-specific store name for isolation.getStore defaults to us-east-2 regardless of where your functions run — no error or warning is raised. To use another region you must pass region on every getStore call for that store (reads, writes, deletes); a call that omits it hits us-east-2 and won't see data held elsewhere. Changing a store's region does not migrate data.onlyIfMatch retries. That's transactional data; use Netlify DB.consistency: "strong" if a read must see the latest write immediately (slower reads).getStore(name) — site-wide; persists across deploys, readable from all contexts.getDeployStore(name) — scoped to one deploy; use for isolation and for any write from a Build Plugin or file-based upload.getDeployStore).Both accept a positional form getStore(name, { region, siteID, token }) / getDeployStore(name, { deployID, region, siteID, token }) or an object form getStore({ name, consistency, region, siteID, token, fetch }). siteID, deployID, and token are set automatically inside Functions/Edge Functions/Build Plugins; supply them explicitly only to override (e.g. siteID of another site you own). region is auto-set only for getDeployStore (defaults to your functions region); for getStore it is not auto-set and defaults to us-east-2. Site ID = API site_id = NETLIFY_SITE_ID = the UI's Project ID.
Persist an upload (Function):
import { getStore } from "@netlify/blobs";
import type { Context } from "@netlify/functions";
import { v4 as uuid } from "uuid";
export default async (req: Request, context: Context) => {
const form = await req.formData();
const file = form.get("file") as File;
const uploads = getStore("file-uploads");
await uploads.set(uuid(), file, {
metadata: { country: context.geo.country.name }
});
return new Response("Submission saved");
};Persist JSON — use setJSON:
const uploads = getStore("json-uploads");
await uploads.setJSON(key, data, { metadata: { … } });Read a blob (null if missing):
const entry = await uploads.get(key);
if (entry === null) return new Response("Not found", { status: 404 });
return new Response(entry);Read with metadata:
const { data, metadata } = await uploads.getWithMetadata(key);Delete / delete a whole store:
await uploads.delete(key);
const { deletedBlobs } = await uploads.deleteAll(); // 0 if store didn't existStore-opening: getStore, getDeployStore, listStores (imported from @netlify/blobs).
Store instance methods: get, getWithMetadata, getMetadata, set, setJSON, list, delete, deleteAll.
set(key, value, { metadata, onlyIfMatch, onlyIfNew }) — value is ArrayBuffer | Blob | string. Overwrites by default. Resolves { modified, etag }.
setJSON(key, value, { metadata, onlyIfMatch, onlyIfNew }) — same, value any JSON-serializable.
get(key, { consistency, type }) — type one of text (default) / json / arrayBuffer / blob / stream. Resolves the value, or null if missing.
getWithMetadata(key, { consistency, etag, type }) — resolves { data, etag, metadata }, or null if missing. If etag matches the passed value, data is null (cache still fresh).
getMetadata(key, { consistency, etag, type }) — resolves { metadata, etag }, or null if missing. Check existence without downloading the blob.
list({ directories, paginate, prefix }) — resolves { blobs: [{ etag, key }], directories: string[] }.
listStores({ paginate }) — resolves { stores: string[] }. Does not include deploy-specific stores.
delete(key) — resolves undefined.
deleteAll() — resolves { deletedBlobs }; deleting a store is deleting all its blobs.
onlyIfNew: true — write only if the key does not exist.onlyIfMatch: etag — write only if current ETag matches (optimistic concurrency).modified boolean to detect success/failure.const { modified } = await emails.set("jane@netlify.com", "Jane Doe", { onlyIfNew: true });
if (!modified) return new Response("Email already exists", { status: 400 });(These are for single-key create-if-absent / compare-and-set, not for building transactional counters.)
Group keys with /. list({ directories: true }) returns top-level directories plus root blobs. Drill in with prefix — the prefix must include a trailing slash ("cats/"), or keys like catsuit also match.
const { blobs, directories } = await animals.list({ directories: true });
const cats = await animals.list({ directories: true, prefix: "cats/" });Server pages up to 1,000 entries (list) / 1,000 stores (listStores). Handled automatically by default; pass paginate: true for an AsyncIterator:
for await (const entry of store.list({ paginate: true })) {
console.log(entry.blobs);
}Pass a cached etag to getWithMetadata/getMetadata; if it matches, data is null (your copy is fresh). Compare the whole value including surrounding quotes and any weakness prefix.
Default is eventual (single-region, edge-cached; propagation within 60s). Opt into strong per store or per read:
const store = getStore({ name: "animals", consistency: "strong" }); // store level
const dog = await store.get("dog", { consistency: "strong" }); // operation levelThe Netlify CLI always uses strong consistency.
Valid regions (a smaller set than the function regions): us-east-1, us-east-2, eu-central-1, ap-southeast-1, ap-southeast-2.
getDeployStore) default to your functions region; region is auto-set in Functions/Edge Functions/Build Plugins. Override explicitly:
const uploads = getDeployStore({ name: "file-uploads", region: "ap-southeast-2" });getStore) default to us-east-2 and do not follow your functions region. region is never auto-set here.
const profiles = getStore({ name: "user-profiles", region: "eu-central-1" });Gotcha — pass region on every call. A site-wide store only reaches non-default-region data if every getStore call (reads, writes, deletes) passes the same region. Omit it and the call silently uses us-east-2 — no error is raised. Changing a store's region does not migrate data: the store appears empty in the new region while the original data stays in the old one. To move data, copy each entry to a store opened in the new region, then delete from the old.
fetchIf you can't use Node.js 18, supply your own fetch:
const uploads = getStore({ fetch, name: "file-uploads" });For framework/tool authors integrating without a build plugin. Place blob files under .netlify/blobs/deploy in the site's base directory; Netlify uploads them (preserving directory structure) after the build, before the deploy.
Netlify deletes .netlify/blobs/deploy before each build — files committed to the repo are NOT uploaded. You must create blob files during the build (build command or plugin).
Attach metadata with a sibling JSON file prefixing the blob filename with $ and ending .json:
.netlify/blobs/deploy/
├─ dogs/
│ ├─ good-boy.jpg
│ └─ $good-boy.jpg.json
├─ cat.jpg
└─ mouse.jpg (no metadata)Metadata files must be valid JSON or the deploy fails. Requires continuous deployment or CLI deploys.
Roll your own: set with a timestamp in metadata → getWithMetadata to check → delete if expired.
netlify blobs:list/get/set/delete exist for inspection — see the CLI blobs command reference for details. The CLI always uses strong consistency and requires a site-wide store.
Netlify Dev uses a sandboxed local store: no file-based uploads, and you cannot read production data locally.
/ or :, max 64 bytes./, any Unicode, max 600 bytes.@netlify/blobs 6.5.0 → 7.0.0)Site-wide stores written with v6.5.0 or earlier become inaccessible after upgrading (namespacing change). Migrate per store with the latest Netlify CLI:
netlify recipes blobs-migrate YOUR_STORE_NAMEMigrated stores are accessible with v7.0.0+.
These are org conventions, not docs facts — merged into the rendered skill by ctx-gen and never generated. Owned by the skills maintainer.
netlify blobs:list/get/set/delete exist for inspection; the CLI
reference is their source of truth — link, don't restate.getDeployStore() or a context-specific store name for isolation.onlyIfMatch retries. That's transactional data; use Netlify DB.getDeployStore).0830047
Canonical home
since May 18, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.