Skip to content

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:

EndpointLimitWindowIdentified ByPurpose
POST /v1/images10 requests1 minuteUser ownerPrevent upload spam
CDN variant generation1000 requests1 minuteIP addressPrevent variant abuse
GET /v1/analytics/*100 requests1 minuteUser ownerProtect analytics queries
POST /v1/images/:id/ai/analyze1 request28 daysPer imagePrevent 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

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "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:

typescript
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 cleanup

To allow 20 uploads per minute, change RATE_LIMIT_UPLOAD_BURST to 20.

CDN variant generation limits

Edit packages/config/src/security.ts:

typescript
export const RATE_LIMIT_WINDOW_MS = 60 * 1000;          // 1 minute
export const RATE_LIMIT_CDN_MAX = 1000;                 // Max variant generations per minute

To 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:

typescript
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):

typescript
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:

bash
# Rebuild config package
cd packages/config
pnpm build

# Rebuild API
cd ../api
pnpm build

# Deploy
pnpm deploy:prod

No 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

mermaid
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
    end

The algorithm

Pixflare uses a fixed window counter:

  1. Define a time window (e.g., 1 minute starting at 10:00:00)
  2. Count all requests within that window
  3. If count exceeds the limit, reject the request
  4. 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:

json
{
  "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

typescript
async function checkRateLimit(
  kv: KVNamespace,
  key: string,
  limit: number,
  windowMs: number
): Promise<RateLimitResult>

Returns an object with:

  • allowed: boolean - Whether the request should be allowed
  • limit: number - The maximum requests allowed
  • remaining: number - How many requests are left in the window
  • resetAt: number - Unix timestamp (ms) when the window resets

How counters work

On each request:

  1. Read current counter from KV
  2. Check if the window has expired (resetAt < now)
  3. If expired, create a new window with count=1
  4. If within window, increment the count
  5. If count > limit, return allowed=false (don't update KV)
  6. Otherwise, write updated count to KV with TTL
  7. 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):

typescript
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):

typescript
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):

typescript
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:

bash
cd packages/api
pnpm test rate-limit

Released under the MIT License.