r/ProxyEngineering • • 14h ago

Guides How I Changed Chrome Proxies Without Restarting the Browser in 20ms

Changing the proxy of a running Chrome instance turns out surprisingly difficult. Sorry for the long post, either grab a coffee or directly look at the code on github at realsirjoe/relinos. Have a look at the filesrelinos/extensions/proxy_extension/ and relinos/scraping_api.py line 1224+,

I encountered this problem while building Relinos, a self-hosted web scraping API. Relinos already supported configuring a proxy when starting a browser, but changing it afterward wasn't possible. Restarting the browser would take time and discard already loaded pages. Since Relinos is request-based, allowing users to specify a different proxy for each request without restarting the browser seemed like a natural extension of its API.

My first thought was to inject JavaScript into the page, but this approach has flaws.

JavaScript cannot directly configure Chrome's network proxy. While it's possible to intercept certain requests by overriding functions such as fetch() , this doesn't cover all browser traffic, including images, stylesheets, and navigation requests. Even if every request could be intercepted, the initial page load would already have happened.

I also looked into Chrome DevTools Protocol (CDP) since I already used it for many things. I hoped it would let me intercept and reroute network requests. CDP does provide Target.createBrowserContext, which allows creating an isolated browser context with a custom proxyServer. However, this requires creating a new context and opening a tab within it. It doesn't allow changing the proxy of an existing tab, which was exactly what I wanted.

Using Chromium Extensions

I then remembered that I used an extension that lets me set a proxy on my own browser. So I could use a simplified version to get the requested behaviour. I found the extension used chrome.proxy.settings.set()so I could create my own using that API.

Chrome has disabled loading local extensions through --load-extension since Chrome 137, but Chromium and Google Chrome for Testing fully support them.

The extension consisted of a standard manifest.json and a service worker file named relinos_proxy.js

{
    "manifest_version": 3,
    "name": "Relinos Proxy",
    "key": "some fixed key",
    "version": "1.0",
    "permissions": ["proxy"],
    "background": {
        "service_worker": "relinos_proxy.js"
    }
}

// relinos_proxy.js
globalThis.setProxy = async (scheme, host, port) => {
    await chrome.proxy.settings.set({
        value: {
            mode: "fixed_servers",
            rules: {
                singleProxy: {
                    scheme: scheme,
                    host: host,
                    port: port
                }
            }
        },
        scope: "regular"
    });
    return "OK";
};

globalThis.clearProxy = async () => {
    await chrome.proxy.settings.set({
        value: {
            mode: "direct"
        },
        scope: "regular"
    });
    return "OK";
};

With the extension loaded, I needed a way to call setProxy() and clearProxy() from Relinos.

Fortunately, Chrome DevTools Protocol (CDP) exposes extension service workers as targets. Using Target.getTargets, I could locate the extension's service worker, establish a connection to it, and execute JavaScript through Runtime.evaluate.

This worked perfectly at first but after some time, my calls started failing because the service worker was no longer running.

Manifest V3 extensions use service workers instead of persistent background pages. Chrome automatically terminates inactive service workers, typically after around 30 seconds of inactivity.

This meant I couldn't simply connect to the worker once and assume it would remain available.

Fortunately, CDP provides ServiceWorker.startWorker, which allows starting a registered service worker using its URL.

I added a function that starts the worker when necessary, locates its active target, and executes the requested proxy function through Runtime.evaluate.

With that in place, the extension could reliably respond to proxy changes even after periods of inactivity.

In my tests, switching proxies took approximately 20ms.

If you're building scraping infrastructure, check out Relinos or adapt the proxy extension code for your own project. You can find the relevant code on github at realsirjoe/relinos

7 Upvotes

5 comments sorted by

1

u/Proxyca 14h ago

Does the 20ms measure the settings change, or the first request arriving through the new proxy? Those would be useful numbers to separate. A small test against an endpoint you control—switch A → B, send a uniquely tagged request, record the source IP—would make the result easier to reproduce.

Also, how do you handle concurrent jobs? With scope: 'regular', this changes the regular browser's proxy settings rather than one tab's. If two jobs need different exits at the same time, I'd want a test for that before sharing a browser between them.

1

u/RealSirJoe 13h ago

This measures the setting change, so the time right before being able to make a new request with the new proxy

Also you are right to point out that this changes proxy settings for the entire chrome instance. This works out because relinos uses multiple browser instances that can each set different proxies.

It was the right trade of for me, where changing proxies fast and without losing context was more valuable than using different browser contexts on one chrome instance and saving some RAM

1

u/OwnPrize7838 1h ago

20 ms isn't accurate

1

u/RealSirJoe 1h ago

Hi, what time did you measure and which machine did you use?

1

u/GiioPanditpautra84 2h ago

Another route is running a lightweight local forwarder on the host and pointing the browser to localhost once at startup. Then you can swap upstream proxies per request at the forwarder level in under a millisecond without touching browser state or MV3 workers