
Last Update: May 18, 2026
BY
eric
Keywords
We all need freedom of information access and privacy protection. It's just a fact of modern digital life. When direct access isn't available, we need alternative routes. The go-to solution? VPNs. But what if I told you there's a lighter, faster way?
The VPN Struggle Is Real
People visiting certain countries absolutely need VPNs—they open up the world of communication. Sometimes folks use them just to pretend they're residents of a particular country. Look, I won't judge your reasons.
Traditional VPN setups are often quite complicated. Old legacy protocols aren't supported on newer systems anymore. And there are so many protocols to choose from it'll make your head spin.
For my own practice, I initially used WireGuard. The configuration is really easy—if you're having trouble setting it up, you can simply ask any AI agent. They'll be able to set it up for you without breaking a sweat.
The VPN Downside Nobody Talks About
Here's the thing though—the entire VPN setup is heavy. You need to buy or rent a VPS. We've got wide selections to choose from: Google Cloud, AWS, Microsoft Azure, Digital Ocean, Hetzner, Linode, Liquid Web, OVHcloud. The list goes on.
The big three cloud providers offer pay-as-you-go pricing. They charge based on hours you use, which is good. You can leave the VPN server running 24/7, so whenever you need your VPN service, you have it straightaway. Multi-platform support is there too.
But here's the kicker—for those who don't really need the VPN server running 24/7, the VPN approach isn't particularly ideal. The speed for access isn't great either. You're routing everything through another server, after all.
What If There Was a Better Way?
Do we have a better choice? Something that's light and simple? Something that runs only when we need it? We don't have to start the VPN server, stop the server, and we certainly don't have to maintain another server, let alone deal with all those configurations.
Turns out, yes. Google Cloud Run is advertised as:
Run frontend and backend services, batch jobs, host LLMs, and queue processing workloads without the need to manage infrastructure.
The sweet part? You get two million requests free per month.
Enter the Serverless Web Proxy
Here's what caught my attention: You can write code using your favorite language, framework, and libraries. Package it up as a container, run gcloud run deploy, and your app will be live—provided with everything it needs to run in production.
Building a container is completely optional too. If you're using Go, Node.js, Python, Java, .NET Core, or Ruby, you can use the source-based deployment option that builds the container for you.
So I tested running a web proxy with Cloud Run. And it works. But here's the amazing part—the website viewing speed felt faster with Cloud Run than with a traditional VPN.
The Technical Setup
I put all the necessary code on GitHub: https://github.com/e-tang/web-proxy-with-google-cloud-run
You can just deploy and use it. If you have issues running it, just ask an AI agent to help—Gemini, Codex, Claude, they've all got your back.
The Catch Nobody Warns You About
Here's where it gets interesting. My first instinct was to run Squid—a classic, battle-tested forward proxy—inside a Cloud Run container. Simple, right? Wrong.
Cloud Run sits behind Google's Front End (GFE), a global load balancer that terminates all TLS connections before traffic reaches your container. And GFE is strict: it does not forward standard proxy protocols to your container.
Specifically:
- HTTP forward proxy requests use an absolute URL in the request line (e.g.
GET http://example.com/ HTTP/1.1). GFE sees these and returns 404 before Squid ever gets a look in. - HTTPS tunneling requires a
CONNECTmethod. GFE also blocksCONNECTentirely.
So a traditional proxy container on Cloud Run is dead on arrival. The GFE is doing its job—it's just not the job we needed.
The Workaround: A Fetch Service
The solution is to stop pretending Cloud Run is a proxy server, and instead treat it as a fetch service. Instead of sending proxy-style requests, the client sends a normal POST / with a custom header:
X-Proxy-Url: https://the-actual-destination.com/path
GFE happily forwards this to the container. The container fetches the real URL using its own outbound connection and streams the response back. Simple, and it actually works.
Two-Part Architecture
This means you need two pieces: the Cloud Run service, and something local to translate your browser's normal proxy requests into those fetch-service calls.
That local piece is mitmproxy. It runs on your machine, intercepts browser traffic, decrypts HTTPS, and redirects each request to Cloud Run—all transparently. Your browser just thinks it's talking to a normal HTTP proxy on localhost:8082.
Quick Deployment Steps
First, clone the repository:
git clone https://github.com/e-tang/web-proxy-with-google-cloud-run.git
cd web-proxy-with-google-cloud-run
Then deploy to Cloud Run (swap in your own project and region):
cd proxy
gcloud builds submit \
--tag REGION-docker.pkg.dev/PROJECT_ID/REPO/IMAGE:latest \
--gcs-source-staging-dir gs://YOUR_BUILD_BUCKET/source .
gcloud run deploy web-proxy \
--image REGION-docker.pkg.dev/PROJECT_ID/REPO/IMAGE:latest \
--region asia-southeast1 \
--timeout 3600 \
--concurrency 80
Start the local proxy:
pip install mitmproxy
mitmdump -p 8082 --set http2=false -s local/fetch_proxy_addon.py
That's it. Seriously.
Why This Approach Wins
Cost Efficiency
You're not paying for a server that's running 24/7. Cloud Run charges you only when requests are being processed. For occasional use, this is way more economical than keeping a VPS running.
Speed
The performance was genuinely surprising. Google's infrastructure is optimized for this kind of workload. The cold start times are minimal, and once it's warm, it's lightning fast.
Zero Maintenance
No server updates. No security patches. No monitoring disk space or memory usage. Google handles all of that infrastructure headache for you.
Scalability
If you suddenly need to handle more traffic, Cloud Run scales automatically. Try doing that with your single VPS.
Real-World Use Cases
Bypassing Geo-restrictions
Deploy your proxy in different Cloud Run regions to access content from various geographical locations. Much simpler than managing multiple VPS instances.
Development Testing
Need to test how your app behaves from different locations? Spin up a proxy in seconds rather than configuring a full VPN setup.
Privacy-Conscious Browsing
For occasional private browsing needs, this serverless approach gives you the privacy benefits without the overhead of a full VPN infrastructure.
The Code Behind the Magic
The Cloud Run service is a small Python/Flask app. It reads the X-Proxy-Url header, fetches the target, and streams the response back:
# proxy/main.py (Cloud Run)
import os, requests
from flask import Flask, request, Response, stream_with_context
app = Flask(__name__)
# Strip these to avoid encoding and connection issues
SKIP_REQ = frozenset(['host', 'x-proxy-url', 'accept-encoding', 'connection', ...])
SKIP_RESP = frozenset(['transfer-encoding', 'connection', 'content-encoding'])
@app.route('/', defaults={'path': ''}, methods=['GET','POST','PUT','DELETE','HEAD','OPTIONS','PATCH'])
@app.route('/<path:path>', methods=['GET','POST','PUT','DELETE','HEAD','OPTIONS','PATCH'])
def proxy(path=''):
target = request.headers.get('X-Proxy-Url')
if not target:
return 'Missing X-Proxy-Url header', 400
headers = {k: v for k, v in request.headers.items() if k.lower() not in SKIP_REQ}
up = requests.request(request.method, target, headers=headers,
data=request.get_data(), stream=True, timeout=60)
resp_headers = {k: v for k, v in up.headers.items() if k.lower() not in SKIP_RESP}
return Response(stream_with_context(up.iter_content(chunk_size=65536)),
status=up.status_code, headers=resp_headers)
The local mitmproxy addon intercepts browser requests and redirects them to Cloud Run. Critically, it sets flow.response.stream = True so responses pipe directly to the browser with no intermediate buffering:
# local/fetch_proxy_addon.py (runs on your machine)
from mitmproxy import http
CLOUD_RUN_HOST = "your-service.asia-southeast1.run.app"
class FetchProxy:
def request(self, flow: http.HTTPFlow):
original_url = flow.request.pretty_url # save before we mutate
flow.request.headers["X-Proxy-Url"] = original_url
flow.request.headers["Host"] = CLOUD_RUN_HOST
flow.request.host = CLOUD_RUN_HOST
flow.request.port = 443
flow.request.scheme = "https"
flow.request.path = "/"
def responseheaders(self, flow: http.HTTPFlow):
if flow.response:
flow.response.stream = True # no buffering — vital for video
addons = [FetchProxy()]
One non-obvious thing: Accept-Encoding must be stripped from outgoing requests. If you leave it in, servers may respond with Brotli-compressed content (br). Python's requests library only auto-decompresses gzip—Brotli bytes pass through raw, and since we also strip the Content-Encoding response header, the browser receives binary garbage with no idea it needs decompressing. Stripping Accept-Encoding sidesteps the whole mess by asking servers to respond uncompressed.
How Does It Handle Video?
This was my biggest concern. Browsing static pages is one thing—video is a different beast. Streaming video makes continuous byte-range requests, and any buffering or latency compounds badly.
The first version of the addon buffered every response completely before sending anything to the browser. For a page that returns 50 KB, that's imperceptible. For a video segment that might be several megabytes, that means a blank player until the entire segment has been downloaded to Cloud Run and transferred to your machine. Unacceptable.
The fix is the flow.response.stream = True line in the mitmproxy addon above. Once that's in place, mitmproxy stops collecting the response and starts piping bytes as they arrive. The browser sees a steady stream of data from the moment Cloud Run starts receiving it from the video server.
Measured throughput routing through Singapore (asia-southeast1):
File size : 10 MB
Transfer : 1.85 MB/s (≈ 15 Mbps)
Time : 5.7 seconds
For context, common video bitrates:
- 720p streaming: ~2.5 Mbps
- 1080p streaming: ~5–8 Mbps
- 4K streaming: ~15–25 Mbps
So 15 Mbps comfortably handles 1080p, and sits right at the threshold for 4K. In practice, video sites also use adaptive bitrate streaming—the player starts at a lower quality and ramps up as it builds a buffer—so the experience is smooth even if your connection varies.
Limitations to Consider
Don't get me wrong—this isn't a silver bullet. There are some trade-offs:
- Cold starts: Cloud Run scales to zero when idle. The first request after a period of inactivity takes an extra second or two while the container starts up. Subsequent requests are fast.
- HTTP/HTTPS only: This handles web traffic beautifully. It won't tunnel arbitrary protocols the way a VPN does—no SMTP, no game servers, no BitTorrent.
- Video site API calls: Some video platforms make signed POST requests from JavaScript (login state, recommendations, view tracking). These can return 403 because the request appears to originate from a Cloud Run IP rather than a residential browser. The actual video segments—which carry URL tokens—load fine. This is a minor annoyance rather than a showstopper.
- mitmproxy must be running locally: Unlike a VPN that runs at the OS level and captures all traffic, this requires mitmproxy to be active. It also requires trusting its CA certificate so it can decrypt HTTPS—a reasonable trade-off for what you get.
- Session persistence: You're not maintaining a persistent tunnel. Each request is independent, which is actually a feature for performance but means this doesn't behave like a VPN at the network layer.
When to Choose What
Use a traditional VPN when:
- You need persistent connection for all traffic
- You're doing heavy, continuous browsing
- You need support for non-HTTP protocols
Use the serverless proxy when:
- You need occasional access
- Cost is a primary concern
- You want zero maintenance overhead
- Speed and reliability are priorities
The serverless web proxy approach isn't trying to replace VPNs entirely. It's offering a different solution for a specific set of needs. And for those needs, it works brilliantly.
Sometimes the best solution isn't the most comprehensive one—it's the one that solves your actual problem with the least friction. For lightweight privacy and access needs, Cloud Run delivers exactly that.





Comments (0)
Leave a Comment