- TypeScript 100%
| .vscode | ||
| src | ||
| test | ||
| .editorconfig | ||
| .gitignore | ||
| .prettierrc | ||
| AGENTS.md | ||
| bun.lock | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vitest.config.mts | ||
| worker-configuration.d.ts | ||
| wrangler.jsonc | ||
cf-img-proxy
Image proxy + cache service running on Cloudflare Workers with R2 persistent storage.
Note: This is a simple proxy and cache. It does not optimize, resize, compress, or transform images. If you need on-the-fly image optimization (resizing, format conversion, quality tuning, responsive images, etc.), use a purpose-built service like Cloudflare Images, Imgix, Cloudinary, or Bunny Optimizer.
Given an image URL, the Worker downloads the image, stores it in R2 keyed by a SHA-256 hash of the URL, and serves it back. Repeated requests for the same URL skip the source fetch and serve directly from R2.
How it works
GET https://cf-img-proxy.tbdh.app/?url=https://example.com/cat.jpg
│
▼
SHA-256("https://example.com/cat.jpg") → cache key
│
▼
R2.head(key)
│ │
found missing
│ │
▼ ▼
serve fetch source URL
from R2 │
▼
Content-Type = image/* ?
│ │
yes no → 400 error
│
▼
store in R2 → serve
Development
# Install dependencies
bun install
# Start local dev server (uses local R2 simulation)
bun run dev
# Run tests
bun test
# Type-check
bunx tsc --noEmit
Deployment
bun run deploy
The Worker deploys to Cloudflare and becomes available at:
https://cf-img-proxy.tbdh.app/
Prerequisites for deployment
tbdh.appmust be an active DNS zone in your Cloudflare account- The R2 bucket
img-proxy-cacheis auto-provisioned on first deploy (already created)
API
GET /?url=<image-url>
| Parameter | Required | Description |
|---|---|---|
url |
Yes | Absolute URL to an image (must return image/* Content-Type) |
Response
| Status | Meaning |
|---|---|
200 |
Image returned successfully |
400 |
Missing/invalid url parameter, or URL doesn't return an image |
405 |
HTTP method not allowed (only GET) |
413 |
Image exceeds 10 MB size limit |
502 |
Failed to fetch the source image |
Examples
# Proxy a JPEG
curl -o img.jpg "https://cf-img-proxy.tbdh.app/?url=https://example.com/photo.jpg"
# Proxy an SVG
curl -o graphic.svg "https://cf-img-proxy.tbdh.app/?url=https://example.com/icon.svg"
# Use in an <img> tag
<img src="https://cf-img-proxy.tbdh.app/?url=https://example.com/banner.png" />
Caching behavior
| Layer | Cache duration |
|---|---|
| R2 object | Persistent (never expires) |
Response Cache-Control |
public, max-age=31536000, immutable |
| Cloudflare CDN edge cache | 1 year (via Cache-Control header) |
Images are content-addressed by SHA-256 of the full URL, so the same source URL always produces the same cache key. Updating the source image requires changing the URL.
Supported image types
Any Content-Type starting with image/ is accepted:
image/jpeg · image/png · image/gif · image/webp · image/svg+xml · image/avif · image/bmp · image/tiff · image/heic · image/heif
Configuration
All configuration is in wrangler.jsonc:
- R2 bucket:
img-proxy-cache(bindingIMAGES) - Custom domain:
cf-img-proxy.tbdh.app - Size limit: 10 MB (defined in
src/index.tsasMAX_IMAGE_BYTES)
Tech stack
- Cloudflare Workers — serverless runtime
- Cloudflare R2 — object storage
- TypeScript
- Wrangler — CLI
- Vitest — testing