Image proxy + cache service running on Cloudflare Workers with R2 persistent storage.
  • TypeScript 100%
Find a file
2026-06-21 17:21:41 +08:00
.vscode working app 2026-06-21 17:21:41 +08:00
src working app 2026-06-21 17:21:41 +08:00
test working app 2026-06-21 17:21:41 +08:00
.editorconfig working app 2026-06-21 17:21:41 +08:00
.gitignore working app 2026-06-21 17:21:41 +08:00
.prettierrc working app 2026-06-21 17:21:41 +08:00
AGENTS.md working app 2026-06-21 17:21:41 +08:00
bun.lock working app 2026-06-21 17:21:41 +08:00
LICENSE working app 2026-06-21 17:21:41 +08:00
package.json working app 2026-06-21 17:21:41 +08:00
README.md working app 2026-06-21 17:21:41 +08:00
tsconfig.json working app 2026-06-21 17:21:41 +08:00
vitest.config.mts working app 2026-06-21 17:21:41 +08:00
worker-configuration.d.ts working app 2026-06-21 17:21:41 +08:00
wrangler.jsonc working app 2026-06-21 17:21:41 +08:00

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.app must be an active DNS zone in your Cloudflare account
  • The R2 bucket img-proxy-cache is 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 (binding IMAGES)
  • Custom domain: cf-img-proxy.tbdh.app
  • Size limit: 10 MB (defined in src/index.ts as MAX_IMAGE_BYTES)

Tech stack