Self-host vs managed CORS proxy: which one for which job?
You can use corsproxy.dev, the hosted API we run, or self-host our MIT-licensed open-source Go relay. They are two different programs with different behaviour, not one proxy in two packages. This post compares them and helps you pick.
The two-line summary
- Self-host when you care about who sees your URLs, when you need methods beyond GET and POST, or when you want to change the code yourself.
- Managed when you'd rather not run infrastructure, and you want API keys, quotas, header stripping and server-side upstream secrets out of the box.
For most "I just need to ship this prototype" situations, managed wins on time-to-first-request. For security-sensitive cases where requests must stay inside your own perimeter, self-hosting wins on control, but you take on its security defaults (below).
How the two actually differ
The hosted service is a TypeScript Cloudflare Worker. The open-source proxy is a separate, simpler Go program. They share the idea of passing a target URL as a query parameter and adding CORS headers, and not much else:
| Managed (corsproxy.dev) | Self-hosted Go relay | |
|---|---|---|
| Program | TypeScript Cloudflare Worker | Go binary (main.go), standard library only |
| Endpoint | /proxy?url=… (or /v1/proxy) | /?url=… |
| Methods | GET and POST (plus OPTIONS preflight) | Any method |
| Auth to the proxy | API key (?key= or X-API-Key) | None |
| Limits | Daily request and bandwidth quota per account | Optional per-IP per-minute limit, off by default |
Browser Cookie / Authorization | Stripped before the upstream call | Forwarded to the upstream |
Upstream Set-Cookie | Stripped | Passed back |
| Upstream authentication | Managed upstream headers, or reqHeaders | Whatever headers the browser sends |
| Private-network targets | Blocked (literal IPs plus a DNS check that fails open) | Blocked (DNS lookup that fails closed, re-checked on each redirect) |
| Redirects | Not followed; the 3xx is returned | Followed, up to 10 |
| Caching | Pro: ttl edge caching | None |
| Oversized responses | 10 MB cap, fails with 502 RESPONSE_TOO_LARGE | 10 MB default, silently truncated |
The header row matters most. The Go relay is a transparent pass-through: whatever cookies or tokens the browser attaches go to the upstream, and cookies the upstream sets come back. That is useful when you control both ends, and risky on a public deployment.
Self-hosted: what you get and what it costs
The OSS repo (github.com/melihbirim/corsproxy) is a single Go program with no dependencies outside the standard library, MIT licensed. Run it with:
git clone https://github.com/melihbirim/corsproxy
cd corsproxy
go run main.go
The README also covers Docker and one-click deploys for Railway, Render, Fly.io and Koyeb.
You get:
- Control over allowed origins (
ALLOWED_ORIGINS), allowed and blocked hosts (substring matches), a per-IP rate limit, request size, timeouts and redirect count, all via environment variables. - Your requests never touch our infrastructure. We hold no logs for it.
- The Go relay accepts any HTTP method and passes headers through in both directions.
- The freedom to fork the code and add what it doesn't have, such as caching or keys.
You take on:
- Rebuilding when Go security advisories land.
- Locking it down.
ALLOWED_ORIGINSdefaults to*and the rate limit is off by default. With specific origins set, it also sendsAccess-Control-Allow-Credentials: true, so think about what cookies it will carry. - Building your own API keys or quotas if you need them; there are none.
- Observability. There's a
/healthendpoint; alerting and dashboards are up to you. - TLS and DNS for the host you run it on.
- Watching response sizes: a response over the size limit is truncated without an error, under the upstream's status code.
Managed: what you get and what it costs
The hosted API at api.corsproxy.dev is its own program with accounts, quotas and a dashboard built in.
You get:
- An API key from the dashboard. Pass it as
?key=in the browser orX-API-Keyfrom a server. - A per-account daily quota enforced atomically in a Cloudflare Durable Object, so concurrent requests can't overshoot it.
- Managed upstream headers that keep third-party secrets out of the browser, per-request header overrides (
reqHeaders/resHeaders), andttledge caching on Pro. - Browser
CookieandAuthorizationheaders stripped before the upstream call, andSet-Cookiestripped from responses. - Usage and request logs in the dashboard, and an email when you hit your daily limit.
- Data export and account deletion from the dashboard; request logs are kept for 30 days.
- An SLA on the Enterprise plan. Free and Pro have no SLA.
You take on:
- Trusting a third party with the URLs you proxy. Request logs include the target URL and are kept for 30 days. Bodies aren't logged, but with
ttlset, Pro responses are cached at Cloudflare's edge. - GET and POST only. PUT, PATCH and DELETE aren't supported.
- A daily quota: 500 requests/day on Free, 20,000 on Pro.
- A bill above the free tier: Pro is $5/month through self-serve checkout; Enterprise is arranged with sales.
The decision in four questions
- Is what I'm proxying sensitive enough that I want it inside my own perimeter? Yes → self-host. No → managed is fine.
- Do I need PUT, PATCH or DELETE, or cookies passed through? Yes → self-host. No → either works.
- Do I want to build API keys, quotas, and a usage dashboard myself? Yes → self-host. No → managed.
- Is "time to first proxied request" more important than long-term operational control? Yes → managed. No → either works; pick the one whose constraints match how your team already operates.
Switching between them
Switching is not a drop-in URL swap. The managed path is /proxy?url= and needs a key; the self-hosted path is /?url= with no key. Behaviour differs too, as the table shows, so test cookie, auth and method handling after you switch. Environment variables keep the difference in one place:
// Managed: https://api.corsproxy.dev/proxy?key=...&url=...
// Self-hosted: https://your-host/?url=...
const key = process.env.PROXY_KEY;
const proxied = key
? `https://api.corsproxy.dev/proxy?key=${encodeURIComponent(key)}&url=${encodeURIComponent(targetUrl)}`
: `${process.env.PROXY_URL}/?url=${encodeURIComponent(targetUrl)}`;
fetch(proxied);
Try either in five minutes
Hosted account on the Free tier: create an account. Self-host: clone the repo.