A high-performance Cloudflare bypass proxy server written in Go. This is a drop-in replacement for the original Python FlareSolverr with significant performance improvements.
This project is fully API-compatible with the original FlareSolverr. You can replace your existing FlareSolverr instance without changing any client code:
- Same API endpoints (
/and/v1) - Same request/response format
- Same command names (
request.get,request.post,sessions.create, etc.) plus newsessions.keepalive - Same default port (8191)
Just swap the Docker image or binary and you're done.
- Browser Pooling - Reuses browser instances instead of spawning new ones per request (150-250MB vs 400-700MB)
- Direct CDP Protocol - Uses Chrome DevTools Protocol directly, bypassing Selenium overhead
- Go Concurrency - Native goroutines for better concurrency than Python's GIL
- Memory Management - Active memory monitoring with automatic browser recycling
- Session Support - TTL-based session management with per-request TTL override, keepalive command, and automatic cleanup
- Cloudflare Bypass - Solves JavaScript challenges and Turnstile CAPTCHAs
- Anti-Fingerprinting - The same browser fingerprint on every request, stateless or session, with a per-request timezone and User-Agent
- Per-Session Chrome Flags - Custom window size, language, timezone, and Chrome flags per session with dedicated browser instances
- Human-Like Behavior - Bezier curve mouse movements, randomized timing, and natural scroll patterns
- Two-Phase CDP Bypass - Bypasses Cloudflare's managed challenge loop by launching a clean Chrome without CDP for challenge resolution
- External CAPTCHA Fallback - Pluggable provider registry with 2Captcha, CapSolver, and anti-captcha.com for Turnstile and hCaptcha
- hCaptcha Support - Detects hCaptcha challenges, extracts sitekeys, and solves via external providers with token injection
- Hot-Reload Selectors - Update challenge selectors via file watching or remote URL without restarts
- Adaptive Solving - Per-domain tracking of which solving methods work best
- Custom JS Execution - Run arbitrary JavaScript on pages after challenge solving
- Binary Downloads - Download files as base64 through the browser (bypasses TLS fingerprinting)
- Prometheus Metrics -
/metricsendpoint compatible with Prometheus/Grafana - OpenAPI Docs -
/docsendpoint serves the full API specification - CLI Dashboard - Optional split-screen TUI showing live requests and server stats (
DASHBOARD_ENABLED=true) - Multi-Architecture - Docker images for amd64, arm64, and armv7
- Docker Support - Production-ready Docker image with Xvfb
docker run -d -p 8191:8191 --name flaresolverr rorqualx/flaresolverr-go:latestThe image declares /tmp/rod as a VOLUME, so Chromium's per-browser user-data
dirs land in an anonymous Docker volume (cleaned up by docker rm -v) instead
of the container's writable layer. These dirs are removed after every browser
close, so they do not accumulate on disk.
⚠️ Do not mount a tmpfs at/tmp/rod(or/tmp). It is a common suggestion for "putting browser scratch space in RAM", but it breaks Chromium launch (GitHub issue #10):
- tmpfs defaults to
noexec, so Chromium's launch helper cannot exec →permission denied.- A nested tmpfs at
/tmp/rodbreaks Chrome's process-singleton lock →Failed to get the debug url: Opening in existing browser session. This is the failure most commonly hit on Unraid.The container works correctly with no tmpfs flag — just use the default anonymous volume. If you really want browser scratch space in RAM, mount a tmpfs with the
execoption at a path that is not nested under/tmp/rod.
go build -o flaresolverr ./cmd/flaresolverr
./flaresolverr./flaresolverr --versionThe API accepts POST requests with JSON body at both / and /v1 endpoints. All commands return the same response format.
| Endpoint | Method | Description |
|---|---|---|
/ |
POST | Main API endpoint (legacy) |
/v1 |
POST | Main API endpoint (recommended) |
/health |
GET | Health check with pool counters (no API key needed) |
/stats |
GET | Health fields plus per-domain statistics and selector reload state (API key required when enabled) |
/metrics |
GET | Prometheus-compatible metrics |
/docs |
GET | OpenAPI 3.0 specification (YAML) |
Navigates to a URL, solves any Cloudflare challenges, and returns the page content.
curl -X POST http://localhost:8191/v1 \
-H "Content-Type: application/json" \
-d '{
"cmd": "request.get",
"url": "https://example.com",
"maxTimeout": 60000
}'Navigates to a URL with POST data.
curl -X POST http://localhost:8191/v1 \
-H "Content-Type: application/json" \
-d '{
"cmd": "request.post",
"url": "https://example.com/login",
"postData": "username=user&password=pass",
"maxTimeout": 60000
}'Creates a session that persists cookies and browser state across requests.
curl -X POST http://localhost:8191/v1 \
-H "Content-Type: application/json" \
-d '{
"cmd": "sessions.create",
"session": "my-session-id",
"session_ttl_minutes": 60
}'The optional session_ttl_minutes parameter overrides the global SESSION_TTL for this session (1-1440 minutes). If omitted, the server default is used.
curl -X POST http://localhost:8191/v1 \
-H "Content-Type: application/json" \
-d '{
"cmd": "sessions.list"
}'Touches the session to prevent expiration. Optionally extends the TTL.
curl -X POST http://localhost:8191/v1 \
-H "Content-Type: application/json" \
-d '{
"cmd": "sessions.keepalive",
"session": "my-session-id",
"keepaliveTtl": 120
}'If keepaliveTtl is provided (in minutes), the session's TTL is updated to that value. If omitted, the session is simply touched to reset its inactivity timer.
curl -X POST http://localhost:8191/v1 \
-H "Content-Type: application/json" \
-d '{
"cmd": "sessions.destroy",
"session": "my-session-id"
}'| Parameter | Type | Required | Description |
|---|---|---|---|
cmd |
string | Yes | Command to execute |
url |
string | For request.* | Target URL to navigate to |
session |
string | No | Session ID for persistent sessions |
session_ttl_minutes |
int | No | Per-session TTL override in minutes (1-1440, default: server SESSION_TTL) |
maxTimeout |
int | No | Maximum timeout in milliseconds (default: 60000) |
cookies |
array | No | Cookies to set before navigation |
proxy |
object | No | Proxy configuration for this request |
postData |
string | For request.post | URL-encoded POST data |
returnOnlyCookies |
bool | No | Return only cookies, not HTML |
returnScreenshot |
bool | No | Return base64 PNG screenshot |
disableMedia |
bool | No | Block images, CSS, fonts to speed up loading |
waitInSeconds |
int | No | Wait N seconds before returning response |
contentType |
string | No | POST content type: application/json or application/x-www-form-urlencoded |
headers |
object | No | Custom HTTP headers (max 50) |
tabsTillVerify |
int | No | Tab presses for Turnstile keyboard navigation (0-50) |
download |
bool | No | Download URL as binary, return base64 in response field |
followRedirects |
bool | No | Follow HTTP redirects (default: true) |
userAgent |
string | No | Override User-Agent for this request |
returnRawHtml |
bool | No | Return raw HTML before JavaScript renders |
executeJs |
string | No | Custom JavaScript to execute after solving |
captchaSolver |
string | No | External captcha provider for this request: 2captcha, capsolver, anticaptcha or 9kw (one of the server's configured providers, unless captchaApiKey is given), or none to forbid external solving for this request |
captchaApiKey |
string | No | Your own API key for captchaSolver. Used for this request only, for at most one paid solve, even when the server-side fallback is disabled. Requires captchaSolver. Never logged or stored |
keepaliveTtl |
int | No | New TTL in minutes for sessions.keepalive (0 = just touch, max 1440) |
cookieExtractDelay |
int | No | Seconds to wait before extracting cookies (0-30). Captures late-set JS cookies |
browserFlags |
object | No | Per-session Chrome flag overrides (sessions.create only). See below |
fingerprint |
object | No | Only overrides.timezone is honored; fingerprint profiles have been removed. See below |
{
"name": "session_id",
"value": "abc123",
"domain": ".example.com",
"path": "/",
"secure": true,
"httpOnly": true
}{
"url": "http://proxy.example.com:8080",
"username": "user",
"password": "pass"
}Supported proxy schemes: http, https, socks4, socks5
Per-session Chrome flag overrides. Sessions created with custom flags get a dedicated browser (not from the pool) that is closed when the session is destroyed.
{
"windowSize": "1280,720",
"language": "fr-FR",
"timezone": "Europe/Paris",
"headless": true,
"disableGpu": false,
"extraArgs": ["--disable-extensions"]
}| Field | Type | Description |
|---|---|---|
windowSize |
string | Window dimensions as "width,height" (e.g. "1280,720") |
language |
string | Accept-Language override (e.g. "fr-FR", "de-DE") |
timezone |
string | Timezone for stealth patches (e.g. "Europe/Paris") |
headless |
bool | Override global headless setting |
disableGpu |
bool | Force software rendering |
extraArgs |
array | Additional Chrome flags (validated against security whitelist) |
Security: extraArgs are validated against an allowed whitelist. Dangerous flags like --disable-web-security and --remote-debugging-port are blocked.
Example: Create a French-language session with a smaller viewport:
curl -X POST http://localhost:8191/v1 \
-H "Content-Type: application/json" \
-d '{
"cmd": "sessions.create",
"session": "my-french-session-01",
"browserFlags": {
"windowSize": "1280,720",
"language": "fr-FR",
"timezone": "Europe/Paris"
}
}'Fingerprint profiles have been removed. They never took effect on stateless requests, and every request now presents the same browser whether or not it uses a session.
The fingerprint field is still accepted, so existing clients keep working, but only one option in it does anything:
{
"overrides": { "timezone": "Asia/Tokyo" }
}| Field | Type | Description |
|---|---|---|
overrides.timezone |
string | IANA timezone ID for this request, applied through the browser's own timezone emulation. Takes priority over TZ and a session's browserFlags.timezone |
profile, disablePatches and every other override are ignored; the server logs one warning per request naming them. To change the User-Agent, use the top-level userAgent field: the client hints and navigator.platform follow it.
{
"status": "ok",
"message": "Challenge solved successfully",
"startTimestamp": 1704067200000,
"endTimestamp": 1704067205000,
"version": "1.0.0",
"solution": {
"url": "https://example.com/",
"status": 200,
"response": "<html>...</html>",
"cookies": [
{
"name": "cf_clearance",
"value": "...",
"domain": ".example.com",
"path": "/",
"expires": 1704153600,
"httpOnly": true,
"secure": true,
"sameSite": "None"
}
],
"userAgent": "Mozilla/5.0...",
"screenshot": "base64...",
"turnstile_token": "..."
}
}| Field | Type | Description |
|---|---|---|
status |
string | "ok" or "error" |
message |
string | Human-readable status message |
startTimestamp |
int | Request start time (Unix ms) |
endTimestamp |
int | Request end time (Unix ms) |
version |
string | FlareSolverr version |
solution |
object | Solution data (on success) |
sessions |
array | List of session IDs (for sessions.list) |
| Field | Type | Description |
|---|---|---|
url |
string | Final URL after redirects |
status |
int | HTTP status code |
response |
string | Page HTML content |
cookies |
array | All cookies from the page |
userAgent |
string | Browser user agent |
screenshot |
string | Base64 PNG (if requested) |
turnstile_token |
string | Cloudflare Turnstile token (if present) |
localStorage |
object | All localStorage key-value pairs (for debugging) |
sessionStorage |
object | All sessionStorage key-value pairs (for debugging) |
responseHeaders |
object | Extracted response metadata (cf-ray, etc.) |
responseTruncated |
bool | true if HTML was truncated due to 10MB size limit (optional) |
cookieError |
string | Error message if cookies could not be retrieved (optional) |
responseEncoding |
string | "base64" when download=true (optional) |
executeJsResult |
string | Result of executeJs custom JavaScript (optional) |
browserVersion |
string | Chrome major version for TLS profile matching (optional) |
rateLimited |
bool | true if rate limiting detected (optional) |
suggestedDelayMs |
int | Recommended delay before retry in ms (optional) |
errorCode |
string | Specific error code like CF_1015 (optional) |
errorCategory |
string | Error category: rate_limit, access_denied, captcha, geo_blocked (optional) |
When the target site returns a rate limiting or access denied response, additional fields are included in the solution:
{
"solution": {
"url": "https://example.com/",
"status": 403,
"response": "<html>Access denied...</html>",
"rateLimited": true,
"suggestedDelayMs": 5000,
"errorCode": "ACCESS_DENIED",
"errorCategory": "access_denied"
}
}Supported Error Codes:
| Code | Category | Description | Suggested Delay |
|---|---|---|---|
CF_1015 |
rate_limit | Cloudflare rate limit | 60s |
CF_1020 |
access_denied | Cloudflare suspicious request | 30s |
CF_1006-1008 |
access_denied | Cloudflare access denied | 30s |
CF_1009 |
geo_blocked | Cloudflare geo-restriction | N/A |
CF_1010 |
access_denied | Browser signature rejected | 30s |
ACCESS_DENIED |
access_denied | Generic access denied | 5s |
RATE_LIMITED |
rate_limit | Generic rate limit | 10s |
TOO_MANY_REQUESTS |
rate_limit | Too many requests | 10s |
HTTP_429 |
rate_limit | HTTP 429 status | 60s |
HTTP_503 |
rate_limit | Service unavailable | 30s |
CAPTCHA_REQUIRED |
captcha | CAPTCHA challenge | N/A |
The following headers are included in responses for domain-level statistics:
| Header | Description |
|---|---|
X-Domain-Suggested-Delay |
Recommended delay in ms based on domain history |
X-Domain-Error-Rate |
Error rate (0.0-1.0) for this domain |
X-Domain-Request-Count |
Total requests tracked for this domain |
All configuration is done via environment variables.
| Variable | Default | Description |
|---|---|---|
HOST |
0.0.0.0 |
Server bind address |
PORT |
8191 |
Server port |
| Variable | Default | Description |
|---|---|---|
HEADLESS |
true |
Run browser in headless mode |
BROWSER_PATH |
(auto) | Path to Chrome/Chromium executable |
BROWSER_POOL_SIZE |
3 |
Number of browser instances in pool |
BROWSER_POOL_TIMEOUT |
30s |
Timeout for acquiring a browser |
MAX_MEMORY_MB |
4096 |
Memory used by the whole container before idle browsers are recycled. If the container has a memory limit below this, 90% of that limit is used instead |
| Variable | Default | Description |
|---|---|---|
SESSION_TTL |
30m |
Session time-to-live |
SESSION_CLEANUP_INTERVAL |
1m |
Cleanup interval for expired sessions |
MAX_SESSIONS |
20 |
Maximum concurrent sessions (about 150MB each while a page is open) |
| Variable | Default | Description |
|---|---|---|
DEFAULT_TIMEOUT |
60s |
Default request timeout |
MAX_TIMEOUT |
300s |
Maximum allowed timeout |
| Variable | Default | Description |
|---|---|---|
PROXY_URL |
(none) | Default proxy URL for all requests |
PROXY_USERNAME |
(none) | Default proxy username |
PROXY_PASSWORD |
(none) | Default proxy password |
PROXY_LIST |
(none) | Pool of egress proxies (comma/newline-separated, embedded user:pass@ ok). Enables clean-egress routing |
PROXY_STRATEGY |
sticky-domain |
Egress selection: sticky-domain (same exit IP per site — keeps cf_clearance valid), round-robin, or per-request |
CLEARANCE_CACHE_ENABLED |
true |
Reuse a minted cf_clearance (+UA) per domain/egress to skip repeat solves |
CLEARANCE_TTL |
25m |
Max lifetime of a cached cf_clearance |
| Variable | Default | Description |
|---|---|---|
RATE_LIMIT_ENABLED |
true |
Enable rate limiting |
RATE_LIMIT_RPM |
60 |
Requests per minute per IP |
TRUST_PROXY |
false |
Use X-Forwarded-For / X-Real-IP for rate limiting. Only enable behind a reverse proxy, and set TRUSTED_PROXIES |
TRUSTED_PROXIES |
(none) | Comma-separated CIDRs (or IPs) of your reverse proxies. With this set, forwarding headers are believed only from these addresses and X-Forwarded-For is read right to left. Without it, TRUST_PROXY believes the header from any client, which lets a client evade rate limiting |
ALLOWED_HOSTS |
(none) | Extra hostnames accepted in the Host header, comma-separated (.example.com matches subdomains, * disables the check). IP addresses, localhost, the machine's hostname and single-label names such as a Compose service name are always accepted. See Browser-borne request protection |
CORS_ALLOWED_ORIGINS |
(none) | Comma-separated web origins allowed to call the API from a browser. Requests carrying any other Origin header are rejected with 403 |
STRICT_CONTENT_TYPE |
true |
Require Content-Type: application/json on API requests (415 otherwise) |
ALLOW_LOCAL_PROXIES |
false |
Allow a proxy in an API request to point at localhost or a private address. Does not apply to PROXY_URL, which is trusted configuration |
IGNORE_CERT_ERRORS |
false |
Ignore TLS certificate errors |
DNS_REBINDING_PROTECTION |
true |
Pin response URL to the request-time IP. Set false for sites serving identical content across multiple TLDs/CDN IPs (SSRF protection stays on) |
EGRESS_GUARD |
false |
Route every browser connection through an in-process proxy that refuses private, loopback and cloud-metadata destinations. See Egress guard |
API_KEY_ENABLED |
false |
Enable API key authentication |
API_KEY |
(none) | Required API key (use 16+ chars) |
BROWSER_SANDBOX |
false |
Run Chromium with its own sandbox instead of --no-sandbox. Needs extra container permissions; see below |
By default Chromium runs with --no-sandbox, because the default Docker runtime does not allow what the sandbox needs. That means a browser exploit in a page you ask FlareSolverr to load is contained only by the container itself, so keep the container unprivileged (the bundled docker-compose.yml drops all capabilities and sets no-new-privileges) and do not point it at URLs from untrusted callers without API_KEY_ENABLED.
BROWSER_SANDBOX=true turns Chromium's sandbox back on. It uses unprivileged user namespaces, which Docker's default seccomp profile blocks, so with the default runtime the server exits at startup with No usable sandbox!. One of these must also be set:
| Docker option | Effect |
|---|---|
--security-opt seccomp=<profile>.json |
A seccomp profile that allows user namespaces. seccomp=unconfined also works but removes all syscall filtering |
--cap-add SYS_ADMIN |
Works, but grants a broad capability; prefer a seccomp profile |
Whether user namespaces are available also depends on the host (kernel settings and AppArmor), so test on the machine that will run it. Leave the setting off if the container does not start.
FlareSolverr is normally run without an API key on an address only local software can reach. A web page open in a browser on that machine or network can reach it too, so the server refuses requests that a web page caused:
- Host check (DNS rebinding). The
Hostheader must be an IP address,localhost, the machine's own hostname, a single-label name (for example the Compose service name inhttp://flaresolverr:8191), or a name listed inALLOWED_HOSTS. Anything else gets403, and the log line names the exactALLOWED_HOSTS=...value that would allow it. If you reach FlareSolverr through a dotted name (a reverse proxy atsolver.example.com, a Kubernetes name likeflaresolverr.default.svc), setALLOWED_HOSTS. - Origin check (cross-site requests). A request with an
Originheader not listed inCORS_ALLOWED_ORIGINSgets403. Programs such as Prowlarr, Jackett and curl do not sendOrigin. - JSON only.
POSTrequests must sendContent-Type: application/json, or they get415. If you usecurl -dby hand, add-H "Content-Type: application/json". SetSTRICT_CONTENT_TYPE=falseto turn this off.
When enabled, all requests (except /health) require authentication:
# Via header (recommended)
curl -X POST http://localhost:8191/v1 \
-H "X-API-Key: your-secret-key" \
-H "Content-Type: application/json" \
-d '{"cmd": "sessions.list"}'
# Via query parameter
curl -X POST "http://localhost:8191/v1?api_key=your-secret-key" \
-H "Content-Type: application/json" \
-d '{"cmd": "sessions.list"}'Enable in Docker:
environment:
- API_KEY_ENABLED=true
- API_KEY=your-secret-key-at-least-16-charsExternal CAPTCHA solver fallback for Turnstile and hCaptcha challenges that native solving cannot handle.
Nothing is ever bought unless you opt in. With CAPTCHA_FALLBACK_ENABLED unset or false, FlareSolverr never contacts a paid provider, whatever keys are configured. The only other way a provider is contacted is a request that brings its own captchaSolver + captchaApiKey.
| Variable | Default | Description |
|---|---|---|
CAPTCHA_NATIVE_ATTEMPTS |
3 |
Native solve attempts before external fallback (1-10) |
CAPTCHA_FALLBACK_ENABLED |
false |
Enable external CAPTCHA solver fallback with the server's keys |
CAPTCHA_MAX_EXTERNAL_SOLVES |
1 |
Paid solves one request may buy with the server's keys (0-3). 0 buys nothing even when the fallback is enabled |
TWOCAPTCHA_API_KEY |
(none) | 2Captcha API key |
CAPSOLVER_API_KEY |
(none) | CapSolver API key (standalone Turnstile widgets and hCaptcha; not Cloudflare managed challenges) |
ANTICAPTCHA_API_KEY |
(none) | anti-captcha.com API key |
NINEKW_API_KEY |
(none) | 9kw.eu API key (hCaptcha/reCAPTCHA only — does not solve Cloudflare Turnstile) |
CAPTCHA_PRIMARY_PROVIDER |
2captcha |
Primary provider: 2captcha, capsolver, anticaptcha, or 9kw |
CAPTCHA_SOLVER_TIMEOUT |
120s |
How long to wait for a provider to solve one task (30s-300s) |
TWOCAPTCHA_BASE_URL |
(none) | Override the 2Captcha API base URL. For testing and self-hosted gateways |
CAPSOLVER_BASE_URL |
(none) | Override the CapSolver API base URL. For testing and self-hosted gateways |
ANTICAPTCHA_BASE_URL |
(none) | Override the anti-captcha.com API base URL. For testing and self-hosted gateways |
NINEKW_BASE_URL |
(none) | Override the 9kw.eu endpoint URL. For testing and self-hosted gateways |
The *_BASE_URL overrides must be http or https URLs without credentials or a query string. API keys for that provider — including a request's own captchaApiKey — are sent to the address you configure, so only point them at a gateway you control. A malformed override is ignored and disables the server-side fallback, so a typo cannot send your keys to the provider's real API.
Supported CAPTCHA types:
- Turnstile — Cloudflare's challenge widget (native + external solving)
- hCaptcha — Detected automatically, solved via external provider
How it works:
- FlareSolverr attempts native Turnstile solving first (click methods, keyboard, etc.)
- If native solving fails after
CAPTCHA_NATIVE_ATTEMPTS, it falls back to the external solver - For hCaptcha, external solving is used directly (no native solving available)
- External solver extracts the sitekey, submits to the provider, and injects the token
- Providers are tried in order:
CAPTCHA_PRIMARY_PROVIDERfirst, then the others alphabetically
What one request can cost:
- A request buys at most
CAPTCHA_MAX_EXTERNAL_SOLVESsolves (default 1), counted in tasks created at a provider, across Turnstile and hCaptcha and across every pass of the solve loop. If the challenge is still there after the bought token was injected, FlareSolverr goes back to native solving and does not buy again. - A provider that refuses the task outright (unsupported captcha type, rejected key, empty balance) costs nothing; the next provider is tried and the refusal is remembered for the rest of the request.
- Once a task was created and then timed out, nothing more is bought for that request, from any provider.
- No task is created when fewer than 10 seconds of the request's
maxTimeoutremain: the solution could not arrive in time. Native attempts run first and take time, so give requests that should fall back to a provider a generousmaxTimeout. - If it is unclear whether a provider accepted a task (the create call was sent but answered with a 5xx, an error page, or nothing), the solve counts as spent. A create call that never left the server (DNS or connection failure) does not.
- A flaky response while waiting for a solution (HTTP 429/5xx, an HTML error page) does not abandon the paid task: polling backs off and retries, giving up only after several failures in a row.
- CapSolver is not sent Cloudflare managed challenges (the "Just a moment..." interstitial): its proxyless Turnstile task cannot carry the challenge-page data, so the solve would be paid for and rejected. With CapSolver as the only provider, managed challenges are not solved externally.
Per-request control (see the captchaSolver and captchaApiKey request fields):
| Request | Effect |
|---|---|
| neither field | The server's providers, if CAPTCHA_FALLBACK_ENABLED=true |
"captchaSolver": "none" |
No external solving for this request |
"captchaSolver": "anticaptcha" |
Only that provider, with the server's key (must be configured and the fallback enabled) |
"captchaSolver": "2captcha", "captchaApiKey": "..." |
That provider with your key, for this request only and at most one solve — works even when the server-side fallback is disabled. CAPTCHA_MAX_EXTERNAL_SOLVES does not apply |
captchaApiKey without captchaSolver |
Rejected with a validation error |
A per-request key is never written to the logs, stored, or returned in a response or error message.
Metrics: /metrics exposes per-provider counters once a provider has been used: flaresolverr_captcha_attempts_total, flaresolverr_captcha_tasks_total (billable tasks created), flaresolverr_captcha_successes_total, flaresolverr_captcha_failures_total, flaresolverr_captcha_cost_usd_total and flaresolverr_captcha_solve_seconds_total, each labelled provider="...".
Example configuration:
environment:
- CAPTCHA_FALLBACK_ENABLED=true
- TWOCAPTCHA_API_KEY=your-2captcha-api-key
- CAPTCHA_NATIVE_ATTEMPTS=2
- CAPTCHA_MAX_EXTERNAL_SOLVES=1Hot-reload capable selectors for adapting to Cloudflare changes without restarts. Supports local files and remote URLs.
| Variable | Default | Description |
|---|---|---|
SELECTORS_PATH |
(none) | Path to external selectors.yaml override file |
SELECTORS_HOT_RELOAD |
false |
Enable file watching for automatic reload |
SELECTORS_REMOTE_URL |
(none) | HTTP(S) URL to fetch selectors from |
SELECTORS_REMOTE_REFRESH |
1h |
Refresh interval for remote selectors (5m-24h) |
When SELECTORS_HOT_RELOAD is enabled, changes to the selectors file are automatically detected and applied without restarting the service.
When SELECTORS_REMOTE_URL is configured, selectors are fetched periodically from the remote URL. File selectors take priority over remote selectors if both are configured.
FlareSolverr checks the URL you ask for and refuses private, loopback and cloud-metadata addresses. That check cannot see what the browser does next. A page can redirect to http://10.0.0.5/, load an image or an iframe from it, fetch() it, open a WebSocket to it, or sit behind a hostname that resolves to a public address when it is checked and to a private one when Chromium connects (DNS rebinding). Without the guard those requests are made, from inside your network, before the final URL is examined.
EGRESS_GUARD=true closes that gap. Every browser and browser context is pointed at a small forward proxy inside the FlareSolverr process, listening on 127.0.0.1 only. For each connection the guard resolves the name once, requires every address it resolves to to be public, and connects to the address it checked. This applies to pooled browsers, sessions, browserFlags sessions and the reconnect bypass, and to loopback as well: with the guard on, a page cannot reach 127.0.0.1 or localhost on the machine running FlareSolverr (which includes FlareSolverr's own API).
What it does not change:
- TLS and HTTP/2 fingerprints. HTTPS is tunnelled byte for byte; the guard never terminates TLS. The JA4 and HTTP/2 fingerprints are identical with the guard on and off.
- Request headers. Plain HTTP is forwarded with its headers unchanged and in order. No
ViaorX-Forwarded-Foris added.
What it does change:
- No HTTP/3. Chromium does not use HTTP/3 (QUIC) through a proxy, so sites that would have moved to HTTP/3 stay on HTTP/2. Cloudflare can see that. The solves tested for this release were unaffected, but a managed-challenge target was not available to measure, which is why the guard is off by default.
- Refused destinations look like errors. A blocked HTTPS destination fails with
net::ERR_TUNNEL_CONNECTION_FAILED; a blocked plain-HTTP destination returns a403page from the guard. - Proxies. With the guard on, a proxy from a request, a session,
PROXY_LISTorPROXY_URLis reached through the guard, which logs in to it itself (http,httpsandsocks5, with or without credentials). A per-request proxy then runs in a browser context on a pooled browser instead of a browser of its own, the reconnect bypass works with proxies that need credentials, andPROXY_USERNAME/PROXY_PASSWORDare applied toPROXY_URL.socks4proxies are rejected. Wrong proxy credentials are reported asthe proxy rejected the supplied credentials. - DNS. Names are resolved by FlareSolverr with the system resolver rather than by Chromium.
Limit when a proxy is in use: the upstream proxy resolves hostnames, so the guard cannot check the address a name leads to. It still refuses what it can recognise without resolving (private and special-purpose IP addresses in any notation, localhost names, cloud-metadata names), but a hostname that the proxy resolves to a private address is not caught. Without an upstream proxy there is no such gap.
The guard exports counters on /metrics (flaresolverr_egress_guard_*), including the number of destinations it refused.
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
info |
Log level (trace, debug, info, warn, error). Health-check request logs and periodic Server stats are emitted at debug, so set LOG_LEVEL=debug to see them. |
LOG_HTML |
false |
Log HTML responses (verbose) |
LOG_FILE |
(none) | Path to log file (in addition to stdout) |
TZ |
(none) | Browser timezone (e.g., America/New_York) |
LANG |
(none) | Browser language (e.g., en_GB) |
TEST_URL |
https://www.google.com |
URL to verify browser works on startup |
DASHBOARD_ENABLED |
true |
TUI dashboard (auto-disables without TTY) |
PPROF_ENABLED |
false |
Enable pprof profiling |
PPROF_PORT |
6060 |
pprof server port |
PPROF_BIND_ADDR |
127.0.0.1 |
pprof bind address |
FlareSolverr launches a split-screen terminal dashboard by default when running in an interactive terminal:
┌──────── Incoming Requests ────────┬──────────── Server Stats ──────────┐
│ 16:03:22 POST /v1 example.com 200 │ Server │
│ 16:03:21 POST /v1 site.org 200 │ Uptime: 2h 14m Req/s: 2.1 │
│ 16:03:19 POST /v1 example.com 200 │ Total: 1,247 Goroutines: 42 │
│ 16:03:15 POST /v1 blocked.io 403 │ Memory: 312 MB │
│ │ Pool │
│ │ 2/3 available │
│ │ Sessions │
│ │ 4 active │
│ │ Domains (3) │
│ │ example.com 842 req 96% ok │
│ │ site.org 231 req 89% ok │
└────────────────────────────────────┴────────────────────────────────────┘
Left pane: Live request log with method, path, status code, and latency. Right pane: Server stats including uptime, request rate, memory, browser pool state, active sessions, and top domains by request count.
Press q or ctrl+c to exit. Logging is suppressed while the dashboard is active.
The dashboard auto-disables when stdout is not a terminal (e.g., Docker logs, piped output). To explicitly disable it: DASHBOARD_ENABLED=false.
version: "3.8"
services:
flaresolverr:
image: rorqualx/flaresolverr-go:latest
container_name: flaresolverr
ports:
- "8191:8191"
environment:
- LOG_LEVEL=info
- BROWSER_POOL_SIZE=3
- MAX_TIMEOUT=300s
restart: unless-stopped
security_opt:
- no-new-privileges:true
cap_drop:
- ALL- Stop your existing FlareSolverr container
- Update your docker-compose.yml or run command to use this image
- Start the new container
- Your existing clients will work without any changes
No code changes required in your applications.
There is one minor difference in cookie field naming:
| Field | Python FlareSolverr | Go Edition |
|---|---|---|
| Expiration | expiry |
expires |
Both values are Unix timestamps (seconds since epoch). If your client code explicitly checks for cookie expiration, use this pattern to handle both:
JavaScript/TypeScript:
const expiry = cookie.expiry ?? cookie.expires;Python:
expiry = cookie.get('expiry') or cookie.get('expires')Go:
expiry := cookie.Expiry
if expiry == 0 {
expiry = cookie.Expires
}The Go edition includes extra fields not in Python FlareSolverr:
| Field | Type | Description |
|---|---|---|
size |
int | Cookie size in bytes |
session |
bool | Whether it's a session cookie |
These are optional and can be safely ignored if not needed.
| Metric | Python FlareSolverr | Go Edition |
|---|---|---|
| Memory per session | 400-700 MB | 150-250 MB |
| Startup time | 5-10s | <1s |
| Request latency | Higher (Selenium) | Lower (CDP) |
| Concurrent requests | Limited (GIL) | Native goroutines |
curl http://localhost:8191/healthReturns:
{
"status": "ok",
"message": "FlareSolverr is ready",
"version": "1.0.0",
"pool": {
"size": 3,
"available": 2,
"acquired": 150,
"released": 148,
"recycled": 5,
"errors": 2
}
}/health needs no API key, so it only reports liveness and pool counters. Per-domain statistics are on /stats, which requires the API key when one is enabled:
curl http://localhost:8191/statsReturns the /health fields plus:
{
"domainStats": {
"example.com": {
"requestCount": 45,
"successCount": 42,
"errorCount": 3,
"rateLimitCount": 2,
"avgLatencyMs": 2340,
"lastRequestTime": "2025-01-15T10:35:00Z",
"lastRateLimited": "2025-01-15T10:30:00Z",
"suggestedDelayMs": 5000
}
},
"defaults": {
"minDelayMs": 1000,
"maxDelayMs": 30000
}
}| Field | Description |
|---|---|
size |
Configured pool size (number of browser instances) |
available |
Browsers currently idle and ready for requests |
acquired |
Total browsers acquired from pool |
released |
Total browsers returned to pool |
recycled |
Browsers recycled due to memory or errors |
errors |
Total browser operation errors |
When requests have been made, the health endpoint includes per-domain statistics:
| Field | Description |
|---|---|
requestCount |
Total requests to this domain |
successCount |
Successful requests (2xx/3xx, no rate limiting) |
errorCount |
Failed requests |
rateLimitCount |
Requests that were rate limited |
avgLatencyMs |
Average response time |
lastRateLimited |
Timestamp of last rate limit event |
suggestedDelayMs |
Recommended delay between requests |
The suggestedDelayMs is calculated using an algorithm inspired by Scrapy's AutoThrottle, considering latency, error rates, and recent rate limiting events.
FlareSolverr uses a browser pool to handle concurrent requests efficiently:
BROWSER_POOL_SIZEcontrols how many requests can be processed simultaneouslyRATE_LIMIT_RPMcontrols the maximum requests per minute per IP
Example behavior with defaults (BROWSER_POOL_SIZE=3, RATE_LIMIT_RPM=60):
- 3 requests can be processed in parallel
- Additional requests queue until a browser becomes available
- Rate limiting kicks in at 60 requests/minute per client IP
| Scenario | Recommended Settings |
|---|---|
| Low volume, fast response | BROWSER_POOL_SIZE=2, RATE_LIMIT_RPM=30 |
| Medium volume | BROWSER_POOL_SIZE=3, RATE_LIMIT_RPM=60 (defaults) |
| High volume, more memory | BROWSER_POOL_SIZE=5, RATE_LIMIT_RPM=120 |
| Single client, max throughput | BROWSER_POOL_SIZE=5, RATE_LIMIT_ENABLED=false |
Each browser instance consumes approximately:
- 100-150MB base memory
- +50-100MB per active page (during request processing)
For a pool size of 3 with 5 active pages, expect 500-700MB total memory usage.
Use MAX_MEMORY_MB to set a memory ceiling. When exceeded, browsers are automatically recycled.
- IP blocked: The target site may have blocked your IP. Try using a proxy.
- Bot detection: Some sites have aggressive bot detection. Session reuse can help maintain cookies.
- Rate limiting: Reduce request frequency or use
RATE_LIMIT_RPMto self-throttle.
- Sessions auto-expire after
SESSION_TTL(default: 30 minutes) - Use
sessions.keepaliveto refresh a session's TTL without making a full request - Use
keepaliveTtlparameter to extend the TTL (e.g.,"keepaliveTtl": 120for 2 hours) - Always check if session exists with
sessions.listbefore using - Destroy and recreate sessions if they become stale
- Reduce
BROWSER_POOL_SIZE(each browser uses 100-150MB) - Enable
disableMedia: truein requests to skip images/CSS - Set
MAX_MEMORY_MBto trigger automatic browser recycling
- Increase
BROWSER_POOL_SIZEfor higher concurrency - Increase
BROWSER_POOL_TIMEOUTif requests timeout waiting for browsers - Check
/healthendpoint to monitor pool stats
- Ensure adequate memory (minimum 512MB recommended)
- Check Docker logs for OOM kills
- Reduce
BROWSER_POOL_SIZEif memory constrained
Enable debug logging to see detailed information:
docker run -e LOG_LEVEL=debug -p 8191:8191 rorqualx/flaresolverr-go:latest- GitHub Issues - Bug reports and feature requests
- Check
/healthendpoint for pool statistics - Review container logs for error messages
See CHANGELOG.md for version history and release notes.
Built with Claude Code by Anthropic
MIT License - See LICENSE for details.