Skip to content

Guarantees & Limits

Because Layeron Storage compiles directly into native Cloudflare R2 and KV resources inside your Cloudflare account, it inherits both high performance and specific structural rules. Use these limits and consistency models when you choose between bucket storage, KV storage, signed URLs, and managed encryption.


Buckets and KV namespaces have completely different consistency behaviors under the hood:

Cloudflare R2 provides strong global read-after-write consistency.

  • Once a bucket.put() or bucket.delete() completes successfully, any subsequent bucket.get() or bucket.list() call from anywhere in the world is guaranteed to instantly see the updated object or deletion.
  • bucket.put(key, value, { ifNotExists: true }) uses an R2 conditional create. Existing objects fail with storage_key_already_exists and HTTP status 409.
  • Best For: Data files, documents, user assets, and any system where immediate state updates are critical.

Cloudflare KV provides eventual global consistency.

  • When you perform a kv.put() or kv.delete(), the update is instantly visible in the local edge region where the write occurred. However, it can take up to 60 seconds for the update to replicate to all other Cloudflare edge caches worldwide.
  • kv.put(key, value, { ifNotExists: true }) checks the current KV-visible value before writing. A visible existing key fails with storage_key_already_exists and HTTP status 409; cross-region visibility follows Cloudflare KV replication.
  • KV is highly optimized for read-heavy workloads (delivering near-zero latency reads directly from the edge cache).
  • Best For: User configurations, feature flags, routing indexes, or static session data. Do not use KV if you need immediate global writes (e.g. distributed locking or transaction ledger records).

Understand payload boundaries before writing code to prevent out-of-memory or validation errors.

MetricBucket (R2)Key-Value (KV)Description
Max Object Size5 TB25 MBThe maximum size of a single stored value payload.
Max Key Size1,024 bytes512 bytesThe maximum character length of a target storage key.
Value FormatsBytes, Strings, BlobsBytes, Strings, BlobsPermitted data formats for Storage payloads.
Max Metadata Size2 KB per objectKey-value dictionary size stored with Bucket objects.

Both variants support automatic data cleanup, but they use different mechanics:

You can configure a global lifecycle rule on a Bucket to expire and delete old objects after a specific number of days.

  • Setting: lifecycle: { deleteAfterDays: 30 }
  • Layeron deploys this as a native Cloudflare R2 lifecycle rule. Cloudflare R2 performs the asynchronous cleanup, so your app Worker does not scan or delete old objects.

You can configure a global or per-key TTL for KV storage. Expirations automatically delete values after a specified number of seconds.

  • Minimum Expiration: Expirations must be at least 60 seconds (1 minute) in the future. Cloudflare KV ignores TTL delays shorter than 60 seconds.
  • Usage:
    • Global default: Configure ttlSeconds in your KV constructor.
    • Per-key custom override: Pass ttlSeconds in your put operation.
      Terminal window
      await sessionStore.put("session_123", sessionData, {
      ttlSeconds: 3600, // Expire this specific key in exactly 1 hour
      })

When you supply a Secret reference to your Storage configuration, Layeron enables managed encryption using the industry-standard AES-GCM-256 algorithm.

Terminal window
const files = storage.bucket({
name: "confidential-files",
encryption: {
secret: secret("vault-key"), // Secure Secret binding
},
})
  • Encryption Boundary: Payloads are encrypted on-the-fly inside the secure Storage Product Worker before they are written to Cloudflare R2 or KV. When reading, payloads are decrypted inside the product worker before being returned to your Gateway route.
  • Zero-Knowledge Cloud: Cloudflare’s storage layer only sees encrypted binary data and randomized initialization vectors. The plaintext secret remains securely injected into your secure worker runtime and never gets written to disk.
  • Signed URLs: Encrypted buckets can still use .signedUrl(). Layeron signs the URL with the configured Secret and routes the request through the Storage Product Worker so reads can decrypt bytes and writes can encrypt bytes.

Storage records the active Secret version beside every encrypted value. New writes use the current Secret version. Reads use the stored keyVersion, so older objects remain readable when the Secret value is rotated and previous versions are retained.

Use retain_forever for encryption keys that protect stored data:

Terminal window
const vaultKey = secret.random({
name: "vault-key",
namespace: "storage",
bytes: 32,
rotation: {
everyDays: 90,
retain: {
mode: "retain_forever",
},
},
})

Managed-encryption signed URLs also include the signing Secret version. A URL generated before a rotation can still validate until it expires when the old Secret version remains retained.

Use .signedUrl() on a bucket when a client needs a short-lived read or write link.

Terminal window
const uploadUrl = await storageSignedUrl(files, "avatars/user_123.png", {
action: "write",
expiresInSeconds: 300,
contentType: "image/png",
maxSizeBytes: 5 * 1024 * 1024,
})

Layeron chooses the signing path from your bucket configuration:

  • A bucket without managed encryption uses a Layeron-managed Cloudflare R2 API token. Layeron creates the token during deploy, stores the credentials as a managed Secret resource, and injects the Secret into the Storage Product Worker.
  • A bucket with managed encryption uses the configured encryption Secret. The signed request goes through the Storage Product Worker so Layeron can encrypt uploads and decrypt downloads.
  • A signed URL expires at expiresInSeconds. The default lifetime is 600 seconds.
  • A write signed URL accepts PUT or POST. A read signed URL accepts GET or HEAD.

Set oneTime: true when the URL should work once:

Terminal window
const downloadUrl = await storageSignedUrl(files, "exports/report.pdf", {
action: "read",
expiresInSeconds: 300,
oneTime: true,
})

Layeron records the consumed token in the Storage state database. The first valid request can read or write the object. A later replay receives HTTP 410 with storage_token_already_used.

Use host and path when signed URL traffic should use a dedicated hostname and path:

Terminal window
const files = storage.bucket({
name: "files",
host: "files.example.com",
path: "/__layeron/r2",
})

Generated signed URLs use that prefix, for example https://files.example.com/__layeron/r2/.... Layeron deploys a Worker route and custom domain for the Storage Product Worker. In local dev, signed URLs use the local dev server with the same path prefix.