netlify-blobs

v2026.09.24

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.

GitHub
Install command
npx skhub add netlify/netlify-blobs
Markdown
SKILL.md

Netlify Blobs

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 missing

Deploy-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.

Footguns (read first)

  • Site-scoped stores (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.
  • Site-wide stores do NOT follow your functions region. 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.
  • Last write wins. No concurrency control. Do not build counters, balances, or read-modify-write logic on a blob key — even with onlyIfMatch retries. That's transactional data; use Netlify DB.
  • No built-in access control. The serving function is the gate. Default to private: gate reads behind an authenticated function rather than exposing blobs publicly. Treat user input as unsafe — don't serve arbitrary caller-supplied keys; scope keys with something callers can't tamper with.
  • Eventual consistency by default — updates/deletions take up to 60s to propagate. Pass consistency: "strong" if a read must see the latest write immediately (slower reads).
  • When an operation fails, surface the error and read the function logs. Do not invent REST endpoints or side-channel APIs to retry.

Store selection

  • 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.
  • Build Plugins: can read from any of the site's stores, but can write only to deploy-specific stores (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.

Common tasks

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 exist

API surface

Store-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.

Atomic conditional writes

  • onlyIfNew: true — write only if the key does not exist.
  • onlyIfMatch: etag — write only if current ETag matches (optimistic concurrency).
  • Inspect the returned 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.)

Listing hierarchically

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/" });

Pagination

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);
}

Conditional requests / local caching

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.

Configuration

Consistency

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 level

The Netlify CLI always uses strong consistency.

Regions

Valid regions (a smaller set than the function regions): us-east-1, us-east-2, eu-central-1, ap-southeast-1, ap-southeast-2.

  • Deploy-specific stores (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" });
    
  • Site-wide stores (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.

Custom fetch

If you can't use Node.js 18, supply your own fetch:

const uploads = getStore({ fetch, name: "file-uploads" });

File-based uploads (deploy-specific stores)

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.

Deploy-specific store lifecycle

  • Kept in sync on rollback; cleaned up with automatic deploy deletion.
  • Downloading a deploy does NOT download deploy-specific blobs.
  • Locking a published deploy does NOT prevent writing to its deploy-specific stores.

Expiration (no built-in TTL)

Roll your own: set with a timestamp in metadata → getWithMetadata to check → delete if expired.

CLI

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.

Local development

Netlify Dev uses a sandboxed local store: no file-based uploads, and you cannot read production data locally.

Constraints

  • Store names: no / or :, max 64 bytes.
  • Keys: non-empty, cannot start with /, any Unicode, max 600 bytes.
  • Object max 5 GB; metadata max 2 KB. (Byte limits, not char counts — some UTF-8 chars are multi-byte.)
  • Blobs encrypted at rest and in transit; accessible only through your own site.
  • Not part of Netlify's HIPAA-compliant hosting offering.
  • Review any third-party build plugin's code before trusting it with blob access.

Migration (@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_NAME

Migrated stores are accessible with v7.0.0+.

<!-- system: agent-context/blobs/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->

Netlify house rules (blobs)

These are org conventions, not docs facts — merged into the rendered skill by ctx-gen and never generated. Owned by the skills maintainer.

  1. Blobs is not a database. For dynamic, per-user, or transactional data, use Netlify DB — Blobs is for objects, files, and cache-like state.
  2. When a store operation fails, surface the error and read the function logs — do not invent REST endpoints or side-channel APIs to retry.
  3. netlify blobs:list/get/set/delete exist for inspection; the CLI reference is their source of truth — link, don't restate.
  4. Blobs have no built-in access control — the serving function is the gate. When in doubt, default to private: gate reads behind an authenticated function rather than exposing blobs publicly.
  5. Site-scoped stores 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.
  6. Don't build counters, balances, or read-modify-write logic on a blob key — even with onlyIfMatch retries. That's transactional data; use Netlify DB.
  7. Build plugins: state BOTH halves — they can read from any of the site's stores, but write only to deploy-specific stores (getDeployStore).
Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

skills/netlify-blobs

Default branch

main

Latest commit

0830047

Tree SHA

6c0367e