---
title: "How to store files and objects with Netlify Blobs | Netlify Knowledge Base"
description: "Guides and articles to help you get the most out of the Netlify platform."
source: "https://www.netlify.com/knowledge-base/how-to-store-files-and-objects-with-netlify-blobs/"
last_updated: "2026-09-23T19:51:21.000Z"
---
## 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 returns `null`, 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 form
```

`siteID`, `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 read
```

The 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_NAME
```

**A 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](https://docs.netlify.com/build/functions/configuration#region).

### 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](https://cli.netlify.com/commands/blobs/).

## 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

-   [Netlify CLI blobs commands](https://cli.netlify.com/commands/blobs/)
-   [Function regions](https://docs.netlify.com/build/functions/configuration#region)

This article is generated from Netlify’s open-source agent guidance at [netlify/context-and-tools](https://github.com/netlify/context-and-tools), the same reference our AI coding agents use.

* * *

Got something to store? Start at [netlify.new](https://netlify.new).