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
| Endpoint | Window | Free tier | Paid tier |
|---|---|---|---|
GET /api/v1/messages/poll | 60s | 100 requests | 100 requests |
POST /api/v1/messages/send | 60s | 600 messages | 2000 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
| Action | Units consumed |
|---|---|
GET /poll?limit=1 with empty queue | 1 |
GET /poll?limit=50 that returns 50 messages | 1 |
POST /send with 50 messages | 50 |
POST /send with 1 message, rejected before enqueue | 0 (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.