Every few weeks a new headless browser lands on GitHub promising the same thing: a clean fingerprint, a tiny footprint, and anti-bot bypasses for free. Most of them are patched Chromium or patched Firefox.
Obscura is not. It is a browser engine written from scratch in Rust that embeds V8 for JavaScript, ships no Chromium and no Node.js, and claims to run in about 30 MB of RAM. I already put its fingerprint on the bench once, in THE LAB #111, where it scored well in a controlled probe. A lab score is one thing. What I wanted this time was to point it at a real anti-bot on a real site and see what a featherweight engine actually buys you.
Give your AI a web data layer – Decodo’s Web Scraping API turns any site into clean, structured data your models can actually use.
The site is Radisson. Their booking pages hide an availability API that returns every room rate for a hotel and date range, and that API sits behind Akamai Bot Manager. If you can talk to it, you can do something useful with it. You can track a single hotel’s nightly price over the next few months and find the cheapest and most expensive days to go. I picked Munich, because Oktoberfest 2026 runs from September 19 to October 4, and nothing moves a city’s hotel prices like a festival that pulls millions of visitors into one place.
So there are two questions stacked on top of each other. Can Obscura clear Akamai at all? And if it can, is a 30 MB engine a sensible way to mint the session that a fast HTTP client then reuses? That second pattern is not new. In THE LAB #100 I built exactly this hybrid, one browser login feeding thousands of curl_cffi requests, but with Camoufox, a real Firefox. So this is really a hands-on review of Obscura for one concrete job. Can the cheap engine take Camoufox’s place, and where it cannot, is that the tool’s fault or the target’s? That second question turned out to be the interesting one.
The tools on the table
Three pieces do the work.
Obscura is the from-scratch Rust engine. Because it implements its own DOM and Web APIs around V8 rather than shipping a real browser, it is small and fast, and that same design is the source of its quirks. It exposes a Chrome DevTools Protocol server, so we can drive it with Playwright over connect_over_cdp exactly like a headless Chrome. Its --stealth flag turns on a consistent spoofed fingerprint, which is the feature under test.
curl_cffi is the replay layer. It is a Python HTTP client that impersonates a real browser’s TLS fingerprint (JA3/JA4, HTTP/2 settings, header order) without running a browser at all. When TLS fingerprinting is the wall, it walks through it in milliseconds. When JavaScript execution is the wall, it cannot help. Knowing which wall is which is most of the job.
The third piece is the fingerprint bench from #111, which reads a browser’s own fingerprint surface from a local probe page and scores it out of 100. I reused it here to get a fresh, reproducible number for Obscura before sending it into the fight.
Two more browsers turn up later as controls, both from that same bench and both scoring a clean 100. CloakBrowser is a Chromium patched at the C++ level, and Camoufox is a Firefox fork with a coherent injected fingerprint. I ran the same proxy test through them to separate a tool problem from a proxy problem, and all three browsers gave the identical result.
How Radisson is actually protected
Before running anything, it pays to model the target. Radisson does not protect everything the same way, and the whole strategy depends on that split.
The HTML pages, like the room-display page you land on after picking a hotel and dates, come back fine to a good TLS impersonation. A plain curl gets nothing, HTTP 000, because its TLS handshake does not look like a browser. curl_cffi with a Chrome profile gets HTTP 200 and 380 KB of real HTML. So the first tier is TLS fingerprinting, and curl_cffi alone handles it.
The XHR APIs are a different story. Two matter here. content-api/hotels/{code}/rooms returns a hotel’s bookable rooms, and availability-api/stays-by-rooms returns the rates for a set of rooms on given dates. Both sit behind Akamai Bot Manager. A request with no cookies gets HTTP 403 with a 421-byte body. Akamai wants its cookie set first: _abck, bm_sz, ak_bmsc, bm_sv, bm_so, bm_s. Those cookies are minted by the Akamai sensor script, a chunk of obfuscated JavaScript that runs on page load, collects a pile of signals, and posts them back. Only after that handshake does _abck reach a validated state and the API start answering.
There is a third layer that shows up when you push too hard. Load the wrong path, or hit the site too often from one address, and Akamai serves a small interstitial instead of the page. It is the sec-cpt behavioral challenge, with a sec-if-cpt-container div and a friendly “Powered and protected by Akamai” line. That page runs its own script and expects the client to solve it before moving on.
So the layer we target is clear. curl_cffi covers the TLS tier and the raw HTTP replay. The one thing it cannot do is run the Akamai sensor to mint _abck. That is the job we hand to Obscura, and it is the exact point where a from-scratch engine either earns its place or does not.
Testing the fingerprint first
There is no reason to send a browser at Akamai if its fingerprint falls over on the easy tests, so I scored it first.
We ran Obscura 0.1.10 headless with --stealth, driven through Playwright over CDP, against the #111 local probe and then against the public test pages people actually cite. The probe reads the browser’s own surface, and here Obscura looks the part. navigator.webdriver is false, on both the prototype and the instance. The user agent is Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/145.0.0.0 Safari/537.36, a plausible Windows Chrome with no HeadlessChrome token. window.chrome is complete, with runtime, csi, loadTimes, and app all present. navigator.userAgentData reports Chrome 145 and Chromium 145. The WebGL unmasked renderer is a spoofed ANGLE (NVIDIA, NVIDIA GeForce RTX 30...). There are five plugins, a Europe/Berlin timezone, and no automation residue at all: no leaked CDP globals, no driver keys.
The score came out at 94 out of 100. That puts Obscura above every Chromium CDP tool in the bench, which sit at 70 because their user agent still carries a headless token, and well above vanilla Playwright and Selenium at 35. The one thing that costs it points is the native-code check. When you ask whether a Web API function is really native by inspecting its toString, a few of Obscura’s functions give themselves away as patched. That is the single tell, and the bench labels it “patched fns”.
The public suites are where it gets more interesting, and more honest. On Sannysoft, Obscura passes cleanly, 21 checks green and none red. But CreepJS never finishes. The page loads to about 8 KB and then stops, with no trust score ever computed. Pixelscan renders its landing chrome but never produces an automation verdict. iphey renders too, but the verdict banner it normally paints, trustworthy or suspicious, never populates. The two oracles from #111, BrowserScan and deviceandbrowserinfo, were inconclusive for the same reason. browserleaks is the exception that proves the point: its canvas and WebGL pages are light enough that Obscura runs them, and the WebGL page confirms the same spoofed NVIDIA GPU the probe reported.
The split is the interesting part. Obscura sails through the boolean checks, the ones that ask a single yes-or-no question about a property. It stalls on the heavy suites, the ones that run thousands of lines of fingerprinting JavaScript to completion. Those suites cannot flag it, but only because they never finish running inside the minimal engine. Hold that thought, because Akamai’s sensor is exactly that kind of heavy script.
Building the hybrid, Obscura mints and curl_cffi replays
The plan follows #100. Use the browser only to mint a valid Akamai session, then hand the cookies to curl_cffi and let it hammer the availability API for every date we care about. The code lives in our repository under obscura_radisson_deals.
The session layer is session.py. It launches Obscura as a CDP server with --stealth, connects Playwright, and loads the room-display page for the target hotel. Then it harvests the cookies and checks them by calling the content-api rooms endpoint. If that call returns 200, the session is good and we also get the room ids we need for scanning. If not, it reloads the page and tries again. This retry loop is not decoration. It is there because of what we found next.
def mint(self, attempts=6, settle_s=8.0):
for attempt in range(attempts):
if attempt == 0:
self._page.goto(self.page_url, wait_until="load", timeout=45000)
else:
self._page.reload(wait_until="load", timeout=45000)
time.sleep(settle_s)
self.cookies = self._harvest()
probe = resolve_rooms(self.cookies, self.hotel_code, self.currency, self.proxy)
if probe["ok"]:
self.room_ids = probe["room_ids"]
return True
return FalseFrom a clean residential IP it works, and it works fast. On a good load, with bm_sv and bm_mi present in the jar, we took the harvested cookies straight to curl_cffi with a Chrome 131 TLS profile. The availability-api/stays-by-rooms endpoint answered HTTP 200 with hundreds of kilobytes of real rate JSON. Against the Munich airport hotel the first load minted a valid session and resolved its rooms on attempt zero. That is the whole thesis in one response. The 30 MB engine minted a session a fast HTTP client could reuse.
It is not always the first load. Sometimes the handshake does not complete, _abck stays in its unsolved state with a -1 in the second segment, and the API answers 403. Obscura harvests nine cookies on those loads too, but without bm_sv and bm_mi the session is not valid, so mint() reloads until one lands. On a trusted IP a reload or two is enough. The reason it can miss at all is the same trait we saw on CreepJS: the minimal engine runs the Akamai sensor part of the way and then not. Camoufox, a real Firefox, does not have that wobble.
There is a second signal worth recording. When we called the API from inside Obscura’s own page context instead of replaying externally, it did not return 403. It returned 500, an application error, which means the request cleared the Akamai wall and reached the backend. A 500 is not a bot block. So the in-browser path can get through too. It just needs the request shaped exactly the way the site’s own code shapes it, and on the room-display page the SPA kept navigating and tearing down our injected fetch before it settled.
You can find the full code used in this article in The Lab repository, available only to paying readers, in the folder “113.Obscura”.



