API

Blobs

Blobs store opaque bytes under their sha256 and serve them through the gateway. A row never stores the bytes themselves. It stores an ordinary string attribute such as image_blob: "blob://products/<sha256>".

Use blobs for media, source files or other binary payloads that rows point at but that should not ride the document write path.

Routes

RouteMethodBehavior
PUT /v1/namespaces/{ns}/blobsPUTStore raw bytes by sha256 and return a blob:// reference.
GET /v1/namespaces/{ns}/blobs/{sha256}GETServe the bytes by sha256.

Where the bytes live

The namespace’s store holds the bytes when it has a native binary type. Blobs larger than the store’s per-value cap, and every blob on a store without one, go to the S3-compatible object store set by S3_BUCKET. The routes and the blob:// reference are the same whichever backend holds the bytes.

StoreBackendLargest blob in the store
turbopufferA {ns}__hevlayer_blobs namespace, one row per sha256 with a bytes attribute6 MiB
Postgres (pgvector)The layer_pgvector.blobs table, one bytea row per sha25610 MiB (the gateway cap)
Stores without native bytesS3None; every blob goes to S3

The gateway accepts blobs up to 10 MiB. turbopuffer caps a bytes value at 8 MiB of base64 on the wire, which is 6 MiB of blob. A blob between 6 MiB and 10 MiB on turbopuffer goes to S3.

With no S3_BUCKET, a blob that does not fit in the store is rejected rather than dropped:

CaseStatuserror
Blob over 10 MiB413payload_too_large
Blob over the store’s cap, no S3413blob_exceeds_store_cap
Store without native bytes, no S3422object_store_not_configured

A turbopuffer blob namespace appears in namespace listings next to the namespace it serves, and deleting a namespace does not delete its blobs. The per-store values are in the store capability matrix.

Store

with open("image.jpg", "rb") as f:
    stored = await client.put_blob("products", f.read())

print(stored.ref)
body, _ := os.ReadFile("image.jpg")
stored, err := client.PutBlob(ctx, "products", body, nil)
import fs from "node:fs/promises";

const bytes = await fs.readFile("image.jpg");
const stored = await client.putBlob("products", bytes);
curl -X PUT "$LAYER_GATEWAY_URL/v1/namespaces/products/blobs" \
  -H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @image.jpg

Response:

{
  "ref": "blob://products/9f86d081884c7d659a2feaa0c55ad015...",
  "sha256": "9f86d081884c7d659a2feaa0c55ad015...",
  "size": 48213
}

The same bytes always return the same reference. The route rejects empty bodies and bodies over 10 MiB.

Write the returned ref as a normal row attribute:

{
  "id": "B0123",
  "vector": [0.1, 0.2],
  "image_blob": "blob://products/9f86d081884c7d659a2feaa0c55ad015..."
}

The removed document blobs payload shape is still rejected. Binary bytes do not traverse /v2/namespaces/{ns} writes.

Fetch

image = await client.get_blob("products", stored.sha256)
image, err := client.GetBlob(ctx, "products", stored.Sha256)
const image = await client.getBlob("products", stored.sha256);
curl "$LAYER_GATEWAY_URL/v1/namespaces/products/blobs/$SHA256" \
  -H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" \
  -o image.jpg

Successful responses include immutable cache headers:

Cache-Control: public, max-age=31536000, immutable
ETag: "<sha256>"

The gateway sniffs common image types (jpeg, png, gif, webp) for Content-Type; otherwise it returns application/octet-stream.

Warm Policy

Aerospike is the hot cache in front of whichever backend holds the bytes. A cache miss reads the store, then S3, and backfills Aerospike best-effort. PUT ...?warm=true writes one object through to cache.

For bulk warming, declare the row attributes containing blob references in the Index’s spec.blobs.referenceAttributes, then call hint_cache_warm with blobs=true and a positive blob_budget_bytes:

spec:
  blobs:
    referenceAttributes: [image_blob]
curl "$LAYER_GATEWAY_URL/v1/namespaces/products/hint_cache_warm?turbopuffer=false&documents=false&snapshots=false&blobs=true&blob_budget_bytes=104857600" \
  -H "Authorization: Bearer $LAYER_GATEWAY_API_KEY"

This example warms only blobs, up to 100 MiB of cached bytes. The gateway scans origin rows, follows same-namespace references in the declared attributes (strings or arrays), and deduplicates objects. It stops before caching the first object that would exceed the budget; the response reports blobs.budget_exhausted. The budget bounds bytes written to cache, not reads from the store or S3: concurrent reads may fetch beyond the cutoff. Missing objects and invalid references are counted and skipped. See Warm cache for SDK examples and response fields.

esc