Client: a real browser called like httpx¶
botonomus.Client is a lightweight layer over one shared Chrome. You call it like
httpx, and every request is loaded by a real browser tab, so cookies, scripts and
anti-bot checks behave as they would for a person. It is an addition:
Botonomus, bot.open(), profiles and personas are unchanged and remain the right tool
when you need a long-lived browser per account.
import botonomus
response = await botonomus.get("https://example.com")
print(response.status, response.via, response.text[:80])
A module-level call opens a temporary client for one request, like httpx.get. Keep a
Client open to reuse the browser, its cookies and the HTTP fast path:
from botonomus import BrowserConfig, Client
async with Client(config=BrowserConfig(headless=False), max_tabs="auto") as client:
page = await client.get("https://example.com/products", params={"page": 2})
login = await client.post("https://example.com/login", data={"user": "me", "pw": "..."})
api = await client.post("https://example.com/api/cart", json={"sku": 42})
print(api.json())
Why it is lighter¶
Botonomus.open |
Client |
|
|---|---|---|
| Browser processes | One Chrome per session | One Chrome for the client |
| Per concurrent request | Browser, GPU and network processes, renderer, profile on disk | One renderer; in-memory context, nothing on disk |
| Cookies | In the profile directory | In the context (or an identity file) |
Every request runs in an in-memory browser context: its own cookies, storage and cache, isolated like a separate browser, while the browser, GPU and network processes are shared. Chrome never shares a renderer between contexts.
Measured on the development machine (Windows 11, stock Chrome, a local page, 6 held-open
tabs, botonomus benchmark --levels 1,3,6): about 224 MiB of host memory per session
with --mode sessions against about 133 MiB per context with --mode contexts, and half
the startup time. The saving grows with the number of tabs because the shared processes
are paid once. Measure your own server before sizing:
The HTTP fast path¶
With mode="auto" (the default) and the optional extra installed, a host that a tab has
loaded without a challenge is served by an HTTP client for later requests:
- The fast path uses
curl_cffi, which reproduces Chrome's TLS ClientHello and HTTP/2 settings. It impersonates the newest Chrome target not newer than your browser (at most 12 majors older), sends the User-Agent, client hints and Accept-Language that your browser actually sent, and uses the context's cookies. Cookies the server sets are written back to the browser context. - If a fast-path response looks like an anti-bot challenge (Cloudflare, DataDome, PerimeterX markers, or 429), the request is sent again from a tab. After two such fallbacks the host stays on tabs for the rest of the client's life.
- A response that came from a tab has
response.via == "browser"and the rendered DOM inresponse.html; a fast-path response hasvia == "http"andhtml is None. - Without the extra, or without a matching target, every request uses a tab.
mode="browser"forces that.
Some sites check every request with JavaScript. The fast path cannot pass those checks, so those requests fall back to tabs automatically.
Pages: full interaction¶
When you need clicks, typing or scrolling, open a tab in the same context:
async with client.page("https://example.com/login") as page:
await page.locator("#user").fill("me")
await page.get_by_role("button", name="Sign in").click()
With BrowserConfig(humanize=True) the page is a HumanPage. Cookies and
localStorage stay in the context after the tab closes.
Identities: accounts without a profile directory¶
async with client.identity("acct-01", proxy="http://user:pass@proxy:8080") as me:
await me.post("https://example.com/login", data={...})
async with me.page("https://example.com/account") as page:
...
An identity is a context whose state survives between uses: cookies, localStorage per
origin and its proxy are saved to identities/acct-01/state.json (next to the profile
root) when the block exits. They are restored into a fresh context next time.
localStorage is restored locally without contacting the site. The file is a few
kilobytes instead of a profile directory. Like profiles, an identity can be open in only
one place at a time, across processes.
- The file contains session cookies and proxy credentials. It is written readable by the owner only (mode 0600 on Linux and macOS; on Windows the identity directory's inherited permissions are replaced with full control for the current user) and is never logged. Treat it like a password.
- IndexedDB, Cache Storage, service workers and the HTTP cache are not carried. For sites
that keep login state there, use
client.identity(name, backend="profile"), which opens the full Chrome profilename(the same onebot.open(profile=name)uses) in its own browser, through the same API.
Options¶
| Option | Default | Meaning |
|---|---|---|
config |
BrowserConfig() |
Executable, persona, proxy, headless, humanize. driver must be native. |
proxy |
None |
Proxy for the client's contexts (credentials allowed); None uses config.proxy. |
mode |
"auto" |
"auto": tab first, then the fast path per host. "browser": always a tab. |
max_tabs |
"auto" |
Concurrent tabs; "auto" sizes from free memory (about 150 MB per tab after 300 MB for the browser) and CPUs (4 per CPU). |
block |
() |
Skip image, font, media or stylesheet loads. Saves bandwidth and CPU; some sites notice. |
fresh_context |
False |
A new, empty context per request instead of one shared context. |
timeout |
30 |
Seconds per request. |
wait |
"load" |
"load" or "domcontentloaded". |
identity_root |
<profile_root>/../identities |
Where identity files live. |
Per request: params, headers, data (form mapping, str or bytes), json,
timeout, wait and settle (extra seconds for scripts to run before the page is
read). Non-GET methods, headers and bodies are applied to the tab's navigation request
itself, as a form submission would send them.
Response¶
url (after redirects), status, headers (case-insensitive), content (raw bytes),
text, json(), html (rendered DOM, tabs only), cookies (names and values for this
URL), elapsed, via, ok and raise_for_status(), which raises
botonomus.HTTPStatusError.
Notes and limits¶
- Contexts are Chrome's off-the-record kind. Some detectors can tell incognito windows
apart (for example a smaller storage quota). Check your target with
botonomus detectandbotonomus consistency. - A persona applies to the whole shared browser, so every context of a client presents
the same hardware. Use separate clients (or
Botonomus.open) for separate personas. - If the browser process dies, the client relaunches it on the next request. It
recreates each context with its last known cookies and
localStorage, and retries the interrupted request once. - The shared browser keeps its own profile (
client-browsers/client-N, next to your profile root) between runs, like a person's browser. Components it downloads once, such as the Widevine CDM that streaming sites need (about 90 seconds after the very first start), stay installed. Requests run in separate contexts, so no site data is kept there. Concurrent clients each take the first freeclient-N. - Client browsers always set the profile preference
webrtc.ip_handling_policytodisable_non_proxied_udp, which every context inherits, so a per-context proxy cannot leak the real address over WebRTC.