A Serverless Proxy for a Background Removal API on Cloudflare Workers and Vercel Edge
Static sites, mobile apps and browser extensions all hit the same wall: the background-removal API needs a key, and the client is the one place a key must not live. A serverless edge function is the cheapest fix — no server to run, a free tier that covers most side projects, and a global endpoint that adds a few milliseconds. This post builds the proxy on Cloudflare Workers, repeats it as a Vercel Edge route, and covers the two things that bite in production: request body limits and abuse.
In this guide
Cloudflare Worker: forward the upload, stream the result
// src/index.js — wrangler secret put BGCLEAR_API_KEY ; wrangler secret put PROXY_TOKEN
export default {
async fetch(request, env) {
if (request.method !== "POST") return new Response("POST only", { status: 405 });
if (request.headers.get("X-Proxy-Token") !== env.PROXY_TOKEN) {
return new Response("unauthorized", { status: 401 }); // your app's shared token
}
const inbound = await request.formData();
const file = inbound.get("image");
if (!(file instanceof File)) return Response.json({ error: "image missing" }, { status: 400 });
if (file.size > 25 * 1024 * 1024) return Response.json({ error: "max 25 MB" }, { status: 413 });
const size = new URL(request.url).searchParams.get("size") === "full" ? "full" : "preview"; // preview is free
const form = new FormData();
form.append("image_file", file, file.name);
form.append("size", size);
form.append("format", "png");
const upstream = await fetch("https://www.bgclear.ai/api/v1/remove", {
method: "POST",
headers: {
Authorization: `Bearer ${env.BGCLEAR_API_KEY}`,
"Idempotency-Key": `${size}-${file.size}-${file.name}`, // a client retry never double-charges
},
body: form,
});
if (!upstream.ok) return new Response(upstream.body, { status: upstream.status, headers: { "Content-Type": "application/json" } });
return new Response(upstream.body, { // stream, do not buffer
headers: {
"Content-Type": upstream.headers.get("content-type") ?? "image/png",
"X-Credits-Remaining": upstream.headers.get("x-credits-remaining") ?? "",
"Cache-Control": "no-store",
},
});
},
};# wrangler.toml
name = "cutout-proxy"
main = "src/index.js"
compatibility_date = "2026-09-01"Workers count CPU time, not time spent waiting on fetch, so a five-second removal costs almost nothing against the limit. Secrets set with wrangler secret put never appear in the bundle or the dashboard logs.
Vercel Edge route: the same proxy
// app/api/cutout/route.ts
export const runtime = "edge";
export async function POST(req: Request) {
if (req.headers.get("x-proxy-token") !== process.env.PROXY_TOKEN) {
return new Response("unauthorized", { status: 401 });
}
const inbound = await req.formData();
const file = inbound.get("image");
if (!(file instanceof File)) return Response.json({ error: "image missing" }, { status: 400 });
const size = new URL(req.url).searchParams.get("size") === "full" ? "full" : "preview";
const form = new FormData();
form.append("image_file", file, file.name);
form.append("size", size);
form.append("format", "png");
const upstream = await fetch("https://www.bgclear.ai/api/v1/remove", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.BGCLEAR_API_KEY}`, "Idempotency-Key": `${size}-${file.size}-${file.name}` },
body: form,
});
return new Response(upstream.body, {
status: upstream.status,
headers: { "Content-Type": upstream.headers.get("content-type") ?? "application/json", "Cache-Control": "no-store" },
});
}Vercel's request body limit is the constraint here (4.5 MB for serverless functions; check the current limit for the Edge runtime in Vercel's docs). For phone photos that exceed it, do not forward bytes at all: upload the file to Vercel Blob, R2 or S3 from the client with a signed URL, then have the edge route send image_url in a JSON body with Accept: application/json — the API fetches the image itself and returns a hosted result URL. That pattern has no body limit and is what the Next.js guide recommends for large uploads.
Abuse control: a cooldown without a database
A public endpoint that spends your credits needs more than a shared token if the token ships in a browser bundle. On Workers, the Rate Limiting binding or a KV counter keyed by client IP is enough for a per-minute cap; on Vercel, Vercel KV or Upstash serves the same purpose. The cheapest version keeps size=preview (free at the API) for anonymous traffic and requires your own session token for size=full, so the worst an abuser can do is burn free previews.
// Workers: per-IP cooldown with the Rate Limiting binding (wrangler.toml: [[unsafe.bindings]] … type = "ratelimit")
const { success } = await env.LIMITER.limit({ key: request.headers.get("CF-Connecting-IP") ?? "anon" });
if (!success) return new Response("slow down", { status: 429, headers: { "Retry-After": "60" } });The API itself allows 60 requests a minute per key and answers 429 with an X-RateLimit-Reset header; pass that through to the client so it backs off.
Cost and compatibility
Both platforms' free tiers comfortably cover a side project; the API cost is one credit per full-resolution image — $9 for 100, $39 for 500, $129 for 2,000 — with unlimited free previews and no subscription (pricing). The request fields are remove.bg's, so a proxy that used to front api.remove.bg changes the URL and the header before that API closes on 1 December 2026 — mapping here. For a full server rather than an edge function, see the Node and Python proxy guides.
Frequently asked questions
Why not call the API from the browser with the key in an environment variable?
Front-end environment variables are compiled into the bundle and visible to anyone. The proxy keeps the key on the edge and lets you add auth and a cooldown.
Does streaming matter?
Yes on edge runtimes with small memory limits: returning upstream.body pipes the PNG through without holding a 10 MB file in memory.
What about images larger than the platform's body limit?
Upload to object storage with a signed URL first, then send image_url to the API from the edge route; the result comes back as a hosted URL valid for 24 hours.
How much does a full-resolution image cost through the proxy?
The same one credit ($0.065–0.09 depending on pack); the proxy adds nothing. Previews are free.