Rate Limiting and Abuse Protection for Public Image APIs
Understand image api rate limiting strategies, abuse vectors for public placeholder services, and how CDN caching and Cloudflare rate limiting protect generation infrastructure.
Public image generation APIs are exposed to the full range of internet traffic: benign crawlers, link preview fetchers, bot traffic, and occasional intentional abuse. Without image api rate limiting and CDN caching, a small percentage of abusive traffic can exhaust compute resources and degrade response times for legitimate users.
This guide explains the main abuse vectors for placeholder image APIs, how CDN caching eliminates most compute-level risk by serving cached responses, how Cloudflare rate limiting handles the remaining uncached attack surface, and what self-hosted operators need to implement to match the protection level of managed services.
Abuse vectors
How public image APIs get abused
The most common abuse pattern is high-volume enumeration: an automated client generates thousands of unique dimension combinations per second to bypass CDN caching and hit the origin. Because placeholder APIs accept arbitrary dimensions, every unique combination is a cache miss. An attacker generating random dimensions can manufacture unbounded origin load.
A second pattern is extremely large dimension requests. Generating a 4096x4096 image requires more SVG computation and produces a larger response than a 100x100 image. Bounding dimensions to a reasonable maximum is the first line of defense against compute exhaustion.
Text injection through query parameters is a third concern. An image API that renders user-supplied text into SVG must sanitize that text to prevent XSS in SVG consumers, escape XML special characters, and limit text length. Unsanitized text in an SVG response can be a vector for content injection if the SVG is rendered inline.
CDN caching
How immutable CDN caching eliminates most abuse risk
The most effective defense against high-volume enumeration is immutable CDN caching. When a placeholder URL is cached at the CDN edge, subsequent requests for the same URL are served from cache without touching the origin Worker. The origin only computes the image once per unique URL per Cloudflare PoP.
fallback.pics returns Cache-Control: public, max-age=31536000, immutable on all responses. The Cloudflare CDN layer absorbs requests for previously cached URLs at the CDN layer, not at the origin. A single PoP serving 10,000 requests for the same 400x300 placeholder computes it once and serves the remaining 9,999 from cache.
Cache-based protection works when attackers use the same URLs repeatedly. For enumeration attacks that intentionally vary parameters to avoid caching, rate limiting at the origin edge is necessary.
# Correct cache-control headers for public placeholder APIs
Cache-Control: public, max-age=31536000, immutable
CDN-Cache-Control: max-age=31536000
Cloudflare-CDN-Cache-Control: max-age=31536000
# These headers tell Cloudflare to cache for 1 year and treat
# the resource as immutable (no revalidation needed) Rate limiting
Cloudflare rate limiting for uncached image generation requests
For requests that miss the CDN cache — new dimension combinations, first requests to a PoP, or intentionally varied parameters — rate limiting at the Cloudflare Workers layer or Cloudflare Firewall provides a second line of defense.
Cloudflare's rate limiting can apply per IP, per ASN, or per JA3 fingerprint. For a public placeholder API where no authentication is required, per-IP rate limiting at a threshold like 100 cache-miss requests per minute is a reasonable starting point. Legitimate use cases — developers using a new URL from their laptop, crawlers indexing a page with several unique placeholder URLs — rarely exceed this threshold.
The rate limit should apply only to origin-hitting requests, not to CDN-cached responses. Cloudflare's rate limiting configuration supports this distinction through the rule action and cache status conditions.
// Cloudflare Worker: dimension bounds and text sanitization
const MAX_DIMENSION = 4096;
const MAX_TEXT_LENGTH = 100;
function sanitizeText(raw: string): string {
return raw
.slice(0, MAX_TEXT_LENGTH)
.replace(/[<>&"']/g, (c) => ({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' }[c]!));
}
function parseDimensions(path: string): [number, number] | null {
const match = path.match(/^/(d{1,4})x(d{1,4})/);
if (!match) return null;
const w = parseInt(match[1], 10);
const h = parseInt(match[2], 10);
if (w < 1 || h < 1 || w > MAX_DIMENSION || h > MAX_DIMENSION) return null;
return [w, h];
} Self-hosted
Implementing rate limiting in a self-hosted placeholder service
If you are self-hosting a placeholder Worker, you need to implement rate limiting yourself. Cloudflare's Rate Limiting product can be added to any Workers route. Define a rule that matches your placeholder path and set a threshold of requests per period per IP.
For a Node.js-based service behind nginx or a reverse proxy, use nginx's limit_req module or a Redis-backed rate limiter in your application code. The bucket size and refill rate depend on your expected legitimate traffic patterns; start conservative and increase based on observed usage.
Regardless of platform, always set a maximum dimension, sanitize text parameters, and log 429 responses so you can tune the rate limit threshold over time.
# nginx rate limiting for a self-hosted placeholder service
http {
limit_req_zone $binary_remote_addr zone=placeholder:10m rate=60r/m;
server {
location /api/v1/ {
limit_req zone=placeholder burst=20 nodelay;
limit_req_status 429;
proxy_pass http://placeholder-origin;
}
}
} Input validation
Preventing text injection and oversized dimension attacks
Every public-facing parameter must be validated and bounded. For image generation APIs, the critical parameters are: dimensions (maximum 4096, minimum 1, integers only), color parameters (valid hex format, exactly 6 characters after stripping the # prefix), and text (maximum length, XML-entity-escaped before rendering into SVG).
Test your validation with adversarial inputs: dimensions of 0, -1, NaN, Infinity, and 99999; colors with script injections like 'fill=red onload=alert(1)'; text with angle brackets and ampersands. Your SVG output should contain none of these characters unescaped.
// Comprehensive input validation for placeholder API
function validateHex(raw: string): string {
const clean = raw.replace(/^#/, '').toUpperCase();
if (!/^[0-9A-F]{6}$/.test(clean)) return 'E4E4E7'; // default gray
return clean;
}
function buildSafeUrl(
w: number, h: number, bg: string, fg: string, text: string,
): string | null {
if (!Number.isInteger(w) || !Number.isInteger(h)) return null;
if (w < 1 || h < 1 || w > 4096 || h > 4096) return null;
return `https://fallback.pics/api/v1/${w}x${h}/${validateHex(bg)}/${validateHex(fg)}?text=${encodeURIComponent(text.slice(0, 100))}`;
} Summary
Defense in depth for public image API rate limiting
The defense stack for a public image API is: maximum dimension bounds to prevent compute exhaustion, text sanitization to prevent injection, immutable CDN caching to eliminate compute load from repeat requests, and rate limiting at the edge for novel uncached requests. Each layer handles a different part of the threat surface.
Managed services like fallback.pics have all four layers in place by default. Self-hosted operators must implement them explicitly, which is one of the non-trivial operational costs of owning the infrastructure.
https://fallback.pics/docs/
https://fallback.pics/placeholder-image-api/
https://fallback.pics/blog/self-hosted-vs-managed-placeholder-api/
https://fallback.pics/blog/csp-img-src-placeholder-apis/ Key takeaways
What to standardize before shipping
- Dimension enumeration attacks bypass CDN caching by varying parameters; max-dimension bounds and rate limiting are necessary defenses.
- Immutable CDN caching is the most effective protection: the origin only computes each unique URL once per PoP.
- Text parameters in SVG generators must be XML-entity-escaped to prevent content injection in SVG consumers.
- Per-IP rate limiting at ~60 requests/minute blocks enumeration attacks without affecting legitimate developer usage.
- Self-hosted operators must implement all four defenses (bounds, sanitization, caching, rate limiting) that managed services provide by default.
Production fallback layer
Use fallback.pics anywhere an image URL is accepted.
Start with one deterministic URL and standardize fallback behavior across your design system.