preloader
post-thumb

Last Update: May 18, 2026


BYauthor-thumberic

|Loading...

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 CONNECT method. GFE also blocks CONNECT entirely.

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:

bash
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):

bash
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:

bash
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:

python
# 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:

python
# 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
Your email won't be published. We'll only use it to notify you of replies to your comment.
Loading comments...
Previous Article
post-thumb

Oct 03, 2021

Setting up Ingress for a Web Service in a Kubernetes Cluster with NGINX Ingress Controller

A simple tutorial that helps configure ingress for a web service inside a kubernetes cluster using NGINX Ingress Controller

Next Article
post-thumb

May 12, 2026

Cloud Run Domain Mappings with Cloudflare: Why Your SSL is Stuck

Cloud Run domain mapping can stay stuck on certificate provisioning when Cloudflare proxying blocks Google's SSL validation.

agico

We transform visions into reality. We specializes in crafting digital experiences that captivate, engage, and innovate. With a fusion of creativity and expertise, we bring your ideas to life, one pixel at a time. Let's build the future together.

Copyright ©  2026  TYO Lab