Rate Limiting
Rate limiting prevents abuse by restricting how many requests a client can make within a time window. Pixflare uses a custom rate limiting system running on Cloudflare Workers with KV storage.
Why rate limiting matters
Without rate limits, malicious users could spam your API with uploads, generate thousands of image variants, or hammer analytics endpoints. Rate limits protect your infrastructure and ensure fair usage for all users.
This is separate from usage limits (like max 100 images on the free plan). Rate limits are short-term abuse prevention. Usage limits are long-term quotas. See the Usage Limits docs for quota information.
Current Rate Limits
Pixflare applies rate limits to four main areas:
| Endpoint | Limit | Window | Identified By | Purpose |
|---|---|---|---|---|
| POST /v1/images | 10 requests | 1 minute | User owner | Prevent upload spam |
| CDN variant generation | 1000 requests | 1 minute | IP address | Prevent variant abuse |
| GET /v1/analytics/* | 100 requests | 1 minute | User owner | Protect analytics queries |
| POST /v1/images/:id/ai/analyze | 1 request | 28 days | Per image | Prevent redundant AI processing |
Client identification
Different rate limits track clients differently:
- Upload and analytics: Tracked by user owner (email or user ID from authentication)
- CDN requests: Tracked by IP address (from CF-Connecting-IP header)
- AI analysis: Tracked per individual image ID
This means two different users can each upload 10 images per minute, but a single user can't upload more than 10 per minute across all their sessions.
What Happens When You Hit the Limit
When you exceed a rate limit, you get an HTTP 429 Too Many Requests response.
Upload endpoint
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Upload rate limit exceeded"
}
}Your upload is rejected. Wait until the window resets (1 minute from your first request in the window) and try again.
CDN variant generation
{
"code": "RATE_LIMITED",
"message": "Too many variant generation requests"
}Instead of generating the variant on-demand, Pixflare serves the original image. A background job queues up to generate the variant for future requests. This graceful degradation means images still load (just not optimized) even when rate-limited.
Analytics endpoints
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Analytics rate limit exceeded"
}
}Your analytics query is rejected. The dashboard may show stale data until the window resets.
AI image analysis
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Image was already analyzed recently. Next analysis allowed at 2025-01-15T10:30:00Z. Cooldown period: 28 days."
}
}The cooldown is per-image to prevent wasting AI credits on redundant analysis of the same image.
Missing rate limit headers
Note that Pixflare doesn't currently return standard rate limit headers like X-RateLimit-Limit, X-RateLimit-Remaining, or Retry-After. This is a known limitation. You'll need to track your request counts manually or wait for the window to reset.
How to Customize Rate Limits
Rate limits are hardcoded in configuration files. There are no environment variables to adjust them. To change rate limits, you need to edit the config files and redeploy.
Upload rate limits
Edit packages/config/src/api.ts:
export const RATE_LIMIT_UPLOAD_BURST = 10; // Max uploads per minute
export const RATE_LIMIT_UPLOAD_WINDOW_MS = 60_000; // Window duration (1 minute)
export const RATE_LIMIT_MIN_TTL_SECONDS = 60; // Min KV TTL for cleanupTo allow 20 uploads per minute, change RATE_LIMIT_UPLOAD_BURST to 20.
CDN variant generation limits
Edit packages/config/src/security.ts:
export const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute
export const RATE_LIMIT_CDN_MAX = 1000; // Max variant generations per minuteTo increase the CDN limit to 2000 requests per minute, change RATE_LIMIT_CDN_MAX to 2000.
AI reanalysis cooldown
Edit packages/config/src/ai-classification.ts:
export const AI_PROCESSING = {
REANALYSIS_COOLDOWN_DAYS: 28,
// ... other AI config
};To reduce the cooldown to 7 days, change REANALYSIS_COOLDOWN_DAYS to 7.
Analytics rate limits
Analytics rate limits are hardcoded directly in the middleware at packages/api/src/routes/api/analytics.ts (lines 30-39):
const rateLimit = await checkRateLimit(
c.env.KV_CACHE,
`analytics:${auth.owner}`,
100, // Change this number
60000 // Change window duration here
);Edit these values directly in the route file.
Apply changes
After editing config files:
# Rebuild config package
cd packages/config
pnpm build
# Rebuild API
cd ../api
pnpm build
# Deploy
pnpm deploy:prodNo per-user customization
All users get the same rate limits. There's no built-in way to give certain users higher limits. If you need this, you'd have to modify the rate limiting code to check user tier and apply different limits accordingly.
How it Works
Pixflare uses a fixed window counter algorithm with Cloudflare Workers KV as storage.
Request flow
sequenceDiagram
participant Client
participant Worker as Worker (rate-limit.ts)
participant KV as KV Storage
Client->>Worker: Make request (upload, CDN, analytics)
Worker->>KV: Read counter for key<br/>(e.g., "ratelimit:upload:user@example.com")
alt Window expired or doesn't exist
KV-->>Worker: null or expired data
Worker->>KV: Create new window<br/>(count=1, resetAt=now+60s)
Worker-->>Client: 200 OK (request allowed)
else Within active window
KV-->>Worker: {count: 5, resetAt: 1704067200}
Worker->>Worker: Increment count (6)
alt Under limit
Worker->>KV: Update count (6) with TTL
Worker-->>Client: 200 OK (remaining: 4)
else Exceeded limit
Worker-->>Client: 429 Rate Limited (remaining: 0)
end
endThe algorithm
Pixflare uses a fixed window counter:
- Define a time window (e.g., 1 minute starting at 10:00:00)
- Count all requests within that window
- If count exceeds the limit, reject the request
- When the window expires (10:01:00), reset the counter to 0
This is simple and works well with KV storage. The trade-off is that it's vulnerable to bursts at window boundaries. A user could make 10 requests at 10:00:59 and another 10 at 10:01:00 (20 requests in 2 seconds). This is generally acceptable for abuse prevention.
Storage in KV
Rate limit counters are stored in Cloudflare Workers KV with these key patterns:
- Upload:
ratelimit:upload:{owner}(e.g.,ratelimit:upload:user@example.com) - CDN:
ratelimit:cdn:{ip}(e.g.,ratelimit:cdn:1.2.3.4) - Analytics:
analytics:{owner}
Each key stores JSON data:
{
"count": 5,
"resetAt": 1704067200000
}KV automatically deletes expired keys based on the TTL set when writing (minimum 60 seconds). You don't need to manually clean up old rate limit data.
Why KV instead of alternatives
KV is perfect for rate limiting because:
- Globally distributed with low latency
- Built-in TTL for automatic cleanup
- Simple key-value API
- Included in Workers plan (no extra cost beyond operations)
Alternatives like Durable Objects are more expensive and overkill for simple counters. D1 database is slower and designed for relational data, not rapid counter increments.
Known limitations
The fixed window algorithm and KV storage have some trade-offs:
- Window boundary bursts: Users can make 2x the limit in a short time by splitting requests across window boundaries
- Race conditions: Concurrent requests from the same client might both increment the counter, allowing slightly more than the limit
- No atomic operations: KV doesn't support atomic increments, so the read-increment-write cycle has a race window
- Eventually consistent: KV is eventually consistent globally, though this rarely causes issues in practice
For most self-hosted use cases, these limitations are acceptable. If you need stricter guarantees, consider Durable Objects or a Redis-backed solution.
Implementation Details
The core rate limiting logic lives in packages/api/src/lib/rate-limit.ts.
Main function
async function checkRateLimit(
kv: KVNamespace,
key: string,
limit: number,
windowMs: number
): Promise<RateLimitResult>Returns an object with:
allowed: boolean - Whether the request should be allowedlimit: number - The maximum requests allowedremaining: number - How many requests are left in the windowresetAt: number - Unix timestamp (ms) when the window resets
How counters work
On each request:
- Read current counter from KV
- Check if the window has expired (resetAt < now)
- If expired, create a new window with count=1
- If within window, increment the count
- If count > limit, return allowed=false (don't update KV)
- Otherwise, write updated count to KV with TTL
- Return the result
The TTL is set to the remaining time in the window (or minimum 60 seconds). This ensures KV automatically cleans up expired data.
Endpoint implementations
Each rate-limited endpoint calls one of these wrapper functions:
Upload rate limit (images.ts:115-125):
const rateLimit = await checkUploadRateLimit(
c.env.KV_CACHE,
auth.owner,
RATE_LIMIT_UPLOAD_BURST,
RATE_LIMIT_UPLOAD_WINDOW_MS
);CDN rate limit (cdn.ts:436-451):
const clientIp = c.req.header('CF-Connecting-IP') || '0.0.0.0';
const rateLimit = await checkCdnRateLimit(
c.env.KV_CACHE,
clientIp,
RATE_LIMIT_CDN_MAX,
RATE_LIMIT_WINDOW_MS
);Analytics rate limit (analytics.ts:30-39):
const rateLimit = await checkRateLimit(
c.env.KV_CACHE,
`analytics:${auth.owner}`,
100,
60000
);Not using Cloudflare's rate limiting
This is a custom implementation. Cloudflare offers built-in rate limiting as a product, but it requires an Enterprise plan and costs extra. By implementing rate limiting in Workers code with KV storage, Pixflare keeps costs down while maintaining control over the logic.
Test coverage
Rate limiting has comprehensive test coverage in packages/api/src/__tests__/lib/rate-limit.test.ts. Tests cover:
- Basic rate limiting (allow under limit, deny over limit)
- Window expiration and reset
- Remaining count calculations
- TTL calculations
- Error handling for KV failures
If you modify the rate limiting logic, run the tests to ensure everything still works:
cd packages/api
pnpm test rate-limit