How to store files and objects with Netlify Blobs
Netlify Blobs is key-value storage for uploads, generated files, and cache-like state. How to use it, which store type to pick, and what belongs in a database instead.
Key takeaways
Install @netlify/blobs, call getStore("my-store") inside a Function, and use set and get to write and read. Store credentials are wired up for you, so there’s no configuration and no keys to manage. Use it for file uploads, generated output, and cache-like state. For anything per-user, transactional, or relational, use Netlify DB instead.
getStore(name)is site-scoped and shared across every deploy context. Read the failure modes below before you use it. Deploy Preview code can overwrite production data.getDeployStore(name)is scoped to one deploy and isolated from production. That’s your safe default for anything throwaway.- Blobs have no built-in access control. The function serving them is the gate. Default to private.
- Not a database. No counters, no balances, no read-modify-write logic, even with retries.
- Always null-check
get. A missing key returnsnull, not an error. - Eventual consistency by default. Writes are readable immediately; updates and deletes propagate within 60 seconds. Opt into strong consistency when you need it.
Somewhere to put the file
Your form takes an image. Your background job produces a sitemap. Your API caches an expensive response. All three need a place to put a chunk of data that isn’t a row in a table and shouldn’t be committed to your repo.
That’s what Blobs is. A key-value store, available from Functions, Edge Functions, and Build Plugins, with the credentials already sorted out. You get a store by name and start writing to it.
The interesting part isn’t the API, which is small and behaves the way you’d expect. It’s picking the right store type, and knowing where the boundary sits between “this is an object” and “this is data that needs a database.” Get those two right and Blobs mostly disappears into the background, which is what you want from storage.
When to use Blobs, and when not to
Good fits:
- User file and image uploads
- Persisting form or contact-form submissions
- Output from Background Functions: sitemaps, processed media, bulk-email results
- Read-only asset stores
- Cache-like state where you’re comfortable managing expiry yourself
Not for per-user, transactional, or relational data. Counters, balances, and sessions belong in Netlify DB, Netlify’s managed Postgres. This isn’t a soft preference. The blobs guidance is explicit: don’t build counters, balances, or read-modify-write logic on a blob key, even with onlyIfMatch retries. Last write wins, and the only concurrency controls are onlyIfMatch and onlyIfNew. If your data has invariants that must hold under concurrent writes, you want transactions, and Blobs doesn’t have them.
A few other places Blobs won’t go: Go Functions can’t access Blobs at all, and Blobs isn’t supported under Netlify’s HIPAA-compliant hosting.
Worked example
Install with npm install @netlify/blobs. The Fetch API is required, which Node 18+ has built in; otherwise pass a custom fetch.
import { getStore, getDeployStore, listStores } from "@netlify/blobs";Two ways to open a store. Use the options-object form when you need consistency or a custom fetch, since the string form can’t pass them:
const store = getStore("file-uploads"); // string formconst store = getStore({ name: "animals", consistency: "strong" }); // options formsiteID, token, deployID, and region are set automatically inside Functions, Edge Functions, and Build Plugins. Don’t pass them manually there.
Saving an upload with metadata
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 key = uuid();
const uploads = getStore("file-uploads"); await uploads.set(key, file, { metadata: { country: context.geo.country.name } });
return new Response("Submission saved");};The Blobs API call is the same in an Edge Function, but the runtime differs: Edge Functions run on Deno, so import Context from @netlify/edge-functions (not @netlify/functions), and use Netlify.env.get() instead of process.env for any environment variables.
For JSON, use setJSON:
const uploads = getStore("json-uploads");await uploads.setJSON(key, data, { metadata: { country: context.geo.country.name } });Reading it back
const uploads = getStore("file-uploads");const entry = await uploads.get(key); // string by defaultif (entry === null) { return new Response(`Could not find ${key}`, { status: 404 });}return new Response(entry);That null check matters. A missing key resolves to null rather than throwing, so skipping the check gets you a confusing downstream error instead of a clean 404. Pass type for other formats: get(key, { type: "json" | "arrayBuffer" | "blob" | "stream" | "text" }).
Conditional writes
Write only if the key is new:
const { modified } = await store.set("jane@netlify.com", "Jane Doe", { onlyIfNew: true });if (!modified) return new Response("Email already exists", { status: 400 });Write only if the entry still matches an ETag you hold:
const { modified } = await store.set(key, "New Jane", { onlyIfMatch: etag });if (!modified) return new Response("Cached data is stale", { status: 400 });These are useful for claim-a-key patterns. They are not a substitute for transactions.
Listing
const { blobs } = await store.list(); // auto-paginates all pages// blobs: [ { etag: "\"etag1\"", key: "..." }, ... ]For manual pagination, store.list({ paginate: true }) returns an AsyncIterator. You can also treat / as a hierarchy:
const { blobs, directories } = await store.list({ directories: true }); // top levelconst catList = await store.list({ directories: true, prefix: "cats/" }); // inside cats/Use the trailing slash on prefix. Without it, cats also matches catsuit.
Expiring things yourself
Blobs have no server-side TTL. Store a timestamp in metadata, check it on read, delete when stale:
await uploads.set(key, await req.text(), { metadata: { expiration: new Date("2024-01-01").getTime() }});const entry = await uploads.getWithMetadata(key);const { expiration } = entry.metadata;if (expiration && expiration < Date.now()) { await uploads.delete(key);}From a build plugin
Build plugins can read from any of the site’s stores but write only to deploy-specific stores:
import { readFile } from "node:fs/promises";import { getDeployStore } from "@netlify/blobs";import { v4 as uuid } from "uuid";
export const onPostBuild = async () => { const file = await readFile("some-file.txt", "utf8"); const uploads = getDeployStore("file-uploads"); await uploads.set(uuid(), file);};Common failure modes
You used getStore() and your Deploy Preview ate production data. This is the big one and it deserves its own read: getStore(name) is site-scoped and shared across all deploy contexts. Code running on a preview reads, overwrites, and deletes the same data your production site is serving. Use getDeployStore() or a context-specific store name for anything you’d be unhappy to lose. There’s a full walkthrough at Why your deploy preview can delete production data in Netlify Blobs.
You exposed a blob without meaning to. Blobs have no built-in access control. The function that serves them is the only gate, so gate reads behind an authenticated function rather than serving them openly. And never accept a caller-supplied key straight through to a store holding sensitive data; that’s a read-anything primitive handed to whoever calls your endpoint.
You built a counter on a blob key. It will look fine until two writes land close together, then it’ll be quietly wrong. Last write wins. Move it to Netlify DB.
Your update didn’t show up for a minute. Default consistency is eventual. New writes are globally readable immediately, but updates and deletes propagate within 60 seconds. Opt into strong consistency per store or per read when you need read-after-write:
const store = getStore({ name: "animals", consistency: "strong" }); // whole storeawait store.get("dog", { consistency: "strong" }); // single readThe CLI always uses strong consistency, which is why a value can look correct via netlify blobs:get and stale in your function.
Your file-based blobs didn’t upload. .netlify/blobs/deploy is wiped before every build, so files have to be created during the build by your build command or a plugin. Files committed to the repo beforehand are not uploaded. Also: metadata sidecar files must be valid JSON or the deploy fails.
You can’t read production data locally. Netlify Dev uses a sandboxed local store. No file-based uploads, and no access to production data. That’s deliberate.
You upgraded past @netlify/blobs 6.5.0 and your data vanished. A namespacing change means site-wide stores written on 6.5.0 or earlier become inaccessible. Migrate with the current CLI:
netlify recipes blobs-migrate YOUR_STORE_NAMEA store operation failed and you’re tempted to work around it. Surface the error and read the function logs. Don’t reach for undocumented REST endpoints to retry.
Reference
Store methods
| Method | Returns |
|---|---|
set(key, value, { metadata, onlyIfMatch, onlyIfNew }) | { modified, etag }. value is ArrayBuffer | Blob | string. |
setJSON(key, value, { metadata, onlyIfMatch, onlyIfNew }) | { modified, etag } |
get(key, { consistency, type }) | Blob in the requested format, or null |
getWithMetadata(key, { consistency, etag, type }) | { data, etag, metadata } or null |
getMetadata(key, { consistency, etag }) | { metadata, etag } or null. Cheap existence check. |
list({ directories, paginate, prefix }) | { blobs, directories }. Auto-paginates unless paginate: true. |
delete(key) | undefined |
deleteAll() | { deletedBlobs }. Deletes the whole store; 0 if it didn’t exist. |
Module function: listStores({ paginate }) returns { stores } and excludes deploy-specific stores.
Store types
| Function | Scope | Use for |
|---|---|---|
getStore(name) | Site-wide, shared across all deploy contexts | Production data you intend to persist across deploys |
getDeployStore(name) | One deploy, isolated from production | Throwaway and per-deploy data. Build-plugin writes. |
Limits
| Thing | Limit |
|---|---|
| Store names | No /, no :, max 64 bytes |
| Keys | Non-empty, can’t start with /, max 600 bytes, any Unicode |
| Object size | 5 GB |
| Metadata size | 2 KB |
| Pagination pages | 1,000 entries or stores per page |
Regions
Deploy-specific stores default to the function’s region. Override it:
const uploads = getDeployStore({ name: "file-uploads", region: "ap-southeast-2" });See the available regions.
Also worth knowing
- Deploy deletion cleans up deploy-specific stores only. Other stores need manual deletion or your own expiry logic.
- Downloading a deploy does not include its deploy-specific blobs.
- Locking a published deploy does not prevent writes to its deploy-specific stores.
- Blobs are encrypted at rest and in transit, and reachable only through your own site.
- Inspect from the CLI with
netlify blobs:list,:get,:set,:delete. See the CLI reference.
FAQs
How do I store file uploads on Netlify?
Call getStore("file-uploads") from a Function and set the file with a unique key. Netlify Blobs handles the storage, and credentials are configured automatically inside Functions.
Is Netlify Blobs a database? No. It’s key-value object storage. For per-user, transactional, or relational data such as counters, balances, or sessions, use Netlify DB, Netlify’s managed Postgres.
What’s the difference between getStore and getDeployStore?
getStore is site-scoped and shared across every deploy context, including Deploy Previews. getDeployStore is scoped to a single deploy and isolated from production. Use the deploy store for anything throwaway.
Do Netlify Blobs have a TTL? No server-side expiration. Store an expiry timestamp in metadata, check it on read, and delete when it’s passed.
Are Netlify Blobs public? Not by default, and they’re reachable only through your own site, but there’s no built-in access control either. Whatever function serves a blob is the access gate, so gate reads behind authentication when the data is sensitive.
How big can a blob be? Up to 5 GB per object, with metadata capped at 2 KB.
Why is my updated blob returning the old value?
Default consistency is eventual. Updates and deletes propagate within 60 seconds. Pass { consistency: "strong" } on the store or the individual read for read-after-write.
Can I use Blobs from a Go function? No. Go Functions can’t access Blobs.
Related
This article is generated from Netlify’s open-source agent guidance at netlify/context-and-tools, the same reference our AI coding agents use.
Got something to store? Start at netlify.new.