← All posts

Write backpressure: 429 + Retry-After

OriginChainDB Team · May 6, 2026
backpressure rate-limiting design reliability api

TL;DR - When a database can’t keep up with incoming writes, it has two choices: refuse them with a 429 + Retry-After (graceful) or accept them and silently fail later (catastrophic). OriginChainDB’s backpressure is per-API-key, expressed as 429 with a precise Retry-After, and tuned so a polite client recovers within seconds. The bucket is checked at the edge, so the 429 fires within microseconds rather than stalling the socket.

What backpressure is for

Every database has a finite ceiling. Writes per second, concurrent connections, durable-commit rate - pick your bottleneck. When you hit it, the database has options:

  1. Queue indefinitely. Latencies spike, eventually clients time out, you find out from a pager.
  2. Drop writes silently. Clients think their data is durable; it isn’t.
  3. Refuse cleanly with a clear protocol contract. The client knows it failed, knows when to retry, and the database recovers without anyone needing to intervene.

OriginChainDB picks option 3. The protocol is HTTP 429 (Too Many Requests) with a Retry-After header expressing the wait in seconds.

The per-key model

Limits are scoped per API key, not per tenant or per endpoint. Each key has its own bucket; one runaway script can’t starve the others.

The buckets are:

When a request hits a depleted bucket, the response is:

HTTP/1.1 429 Too Many Requests
Retry-After: 2
Content-Type: application/json

{"error":{"code":"rate_limited","message":"write rate exceeded; retry after 2s","key_id":"k_abc..."}}

Retry-After is computed from the bucket’s refill schedule, not from a fixed-back-off table. If the server expects to drain in 1.7 seconds it returns 2; if it expects 0.3 seconds it returns 1. The client gets a real estimate, not a wild guess.

Why per-key, not per-tenant

Tenants on OriginChainDB’s dedicated configurations map to dedicated instances, and each awake Free database runs in its own engine process; the substrate doesn’t multi-tenant inside a single process. So per-tenant limits are enforced at the deployment level, not the API level.

What the API key bucket protects against is your own application misbehaving - a runaway worker, a debugging script that forgot to back off, an autonomous agent that’s stuck in a tool-call loop. The key system gives you a way to provision multiple keys for a single tenant (one per service) and isolate them.

Why HTTP 429, specifically

A few reasons:

The client side

What a polite client does:

async function writeWithBackoff(payload: any, attempt = 0): Promise<void> {
 const res = await fetch("/api/write", {
 method: "POST",
 body: JSON.stringify(payload),
 });
 if (res.status === 429) {
 if (attempt >= 5) throw new Error("rate limit exceeded retry budget");
 const delay = parseInt(res.headers.get("retry-after") ?? "1", 10);
 await sleep(delay * 1000);
 return writeWithBackoff(payload, attempt + 1);
 }
 if (!res.ok) throw new Error(`http ${res.status}`);
}

Three things to notice:

  1. Honor Retry-After - it’s not an arbitrary number, it’s the substrate’s actual refill estimate. Polling sooner just generates more 429s.
  2. Cap the retry attempts - if you’re hitting 429 on attempt 6, the client is wrong, not the substrate.
  3. No exponential backoff needed - Retry-After already encodes the right delay. Adding jitter on top is fine but not required.

What an impolite client looks like: tight retry loop, ignores Retry-After, gets stuck. The substrate handles this gracefully - it’ll keep returning 429s - but the client wastes its own runtime polling for no reason.

Limits at a glance

Default values for an entry configuration:

BucketSustainedBurstNotes
Writes1,000/sec4,000Per API key
Reads10,000/sec30,000Per API key
Concurrent in-flight-64Hard cap
Vector search200/sec1,000Counted separately from reads

Higher tiers raise sustained and burst proportionally. Enterprise plans have configurable per-key limits.

These numbers are not a marketing pitch - they’re conservative enough that a well-behaved client will rarely see them, and aggressive enough that the bucket actually does work when something’s wrong.

What the substrate does at the edge

When a bucket is exhausted, the substrate doesn’t queue the request indefinitely. The 429 fires within microseconds - there’s no socket-level “wait until I have capacity” stall.

This is deliberate. A queue at the edge means latency variance the client can’t see; an immediate 429 means the client knows immediately and can decide what to do. For autonomous agent workloads in particular, knowing “I was rate-limited” is far more useful than seeing every request take an unpredictable amount of time.

FAQ

What is HTTP 429?

HTTP 429 (Too Many Requests) is the standard status code for “you’re being rate-limited.” It’s accompanied by a Retry-After header indicating how long to wait before the client should try again.

Will my agent loop survive being rate-limited?

Yes, as long as it honors Retry-After. Most HTTP libraries do this automatically. If you wrote a custom client, make sure the retry path reads the header and sleeps the indicated amount.

Can I raise my limits?

Yes - enterprise plans let you configure per-key limits explicitly. For the free database and starter configurations, the default is what you get.

Are limits per-IP or per-API-key?

Per-API-key. IP-level limits would penalize legitimate traffic from corporate NATs.

What about read-after-write consistency under backpressure?

Unaffected. Backpressure refuses writes that would overrun the substrate; it doesn’t change consistency semantics for accepted writes.


← All posts Subscribe to RSS →