Rate limiting

Per-credential rate limits for the PrivacyFlow public API — credentialId bucket keys, independent poll/send namespaces, and the in-memory model.

PrivacyFlow enforces two rate limits: one for poll and one for send. They are independent — exhausting one does not block the other. Both are per-credential and in-memory.

Limits

EndpointWindowFree tierPaid tier
GET /api/v1/messages/poll60s100 requests100 requests
POST /api/v1/messages/send60s600 messages2000 messages

The send limit counts messages, not requests. A single 50-message POST consumes 50 units. If the batch alone would exceed the remaining budget, the entire request is rejected with 429 before any enqueue.

Note

Paid tier is unlocked by an active subscription purchased in the dashboard’s crypto-billing flow. See crypto payments.

Bucket key: credentialId

Each credential carries a stable credentialId (separate from the key string). The limiter builds its bucket key as <endpoint>:<credentialId>, falling back to <endpoint>:<raw-key> only if a credential somehow lacks a credentialId.

The upshot: rotating your API key string keeps your rate budget. You don’t reset to a fresh 600/100 by rotating. This is intentional — rotation is a security hygiene operation, not a quota reset.

Separate namespaces

The poll and send buckets use different key prefixes (poll: vs send:). Burst-polling does not starve your send budget; burst-sending does not starve your poll budget. Test coverage verifies this isolation.

Response headers

Every successful poll and send carries:

X-RateLimit-Remaining: <int>

A 429 response additionally carries:

X-RateLimit-Remaining: 0
X-RateLimit-Reset: <epoch_ms>

In-memory, per-process

The limiter is an in-memory Map per process. Practical implications:

  • Multi-replica deployments give each replica its own budget. If your provider runs the API behind N pods, your effective cap is N × tier_limit. This behavior may change. If you observe higher-than-documented capacity, multi-replica deployment is the cause.
  • Single-pod deployments share one cap. A single-process worker pipeline hits the wall sooner than a horizontally-scaled one.
  • Cleanup every 300s. Expired buckets are evicted so memory stays bounded under bursty traffic. There is no per-key persistence; limits are window-anchored in the rolling 60 seconds.

Backing off locally

Don’t wait for 429. Use X-RateLimit-Remaining on each call and apply local backoff when you cross your chosen threshold (e.g., remaining < 5). The MCP server, Agent Zero channel, and the n8n node all do this. For poll, a 3-second interval stays well under the 100/minute ceiling; for send, batch up to 50 per request and treat totalAccepted as the spend.

What counts as one unit

ActionUnits consumed
GET /poll?limit=1 with empty queue1
GET /poll?limit=50 that returns 50 messages1
POST /send with 50 messages50
POST /send with 1 message, rejected before enqueue0 (rejected before count)

Tip

If you need to send a large broadcast (say 500 messages), chunk into 50-message batches and watch X-RateLimit-Remaining on each response. A free-tier budget of 600 messages/minute handles a 500-message broadcast in one minute with margin.

Last updated: