Ghilteras/searxng-gateway

Decision proxy in front of SearXNG with Brave Search API fallback

Go

7

31 commits

updated Sep 17, 2026

See the code
ai-agents
circuit-breaker
go
openclaw
opencode
proxy
search
searxng
self-hosted-search

See what people are saying (1)

SourceMessageScoreDate

Couldn’t find a FOSS resilience layer for SearXNG, so I built one (r/opensource)

I wanted a more reliable self-hosted search setup using SearXNG, but free search engines can fail silently or become rate-limited. That can leave a query with only a few results even though more providers are available. I tried addressing this through SearXNG configuration, but I couldn’t find an…

2

Sep 17, 2026

README

searxng-gateway

Decision proxy in front of SearXNG: speculative execution — starts SearXNG and the configured premium-provider pass concurrently, selects providers via round-robin, invokes premium providers serially within that pass, merges results with URL dedup, and loops through remaining providers until a configurable threshold is met or timeout expires. Same JSON shape as SearXNG, Prometheus /metrics, in-memory LRU cache.

🚀 Works with zero API keys in keyless mode. See docs/keyless.md.

Originally derived from sx; adds HTTP server, per-engine circuit breaker, Prometheus metrics, cache, and Docker packaging.

Quick Start

# 1. Clone
git clone https://github.com/Ghilteras/searxng-gateway.git
cd searxng-gateway

# 2. (Optional) Create .env with API keys — or skip for keyless mode
echo 'BRAVE_API_KEY=your_key' > .env
echo 'SERPER_API_KEY=your_key' >> .env

# 3. Start
docker compose -f docker-compose.example.yml up -d

# 4. Test
curl 'http://localhost:8080/search?q=hello+world&format=json'
curl 'http://localhost:8080/metrics'

Architecture

Client ───▶ searxng-gateway (:8080) ───▶ SearXNG (Tier 1 free engines)
                    │                        │
                    │  ┌─────────────────────┤
                    │  │ Speculative execution│
                    │  │ (parallel round-robin)│
                    │  └─────────────────────┤
                    │                        ├── Serper (Google via API)
                    │                        ├── Bing, Wikipedia, GitHub...
                    │                        └── Circuit breaker per engine
                    │
                    └──▶ Tier 2 hot path (T1_PREMIUM_COUNT providers)
                    │    ├── Brave ──┐
                    │    ├── Exa     ├── round-robin alongside SearXNG;
                    │    ├── Jina    │   with SearXNG. Dedup URL.
                    │    └── Tavily ─┘
                    │
                    └──▶ Fallback loop (if merged < SUFFICIENT_MIN_RESULTS)
                         Round-robin through remaining Tier 2 providers
                         until threshold, exhaustion, or FALLBACK_TIMEOUT

See docs/architecture.md for the full design.

Supported fallback providers

Set FALLBACK_PROVIDERS to a comma-separated list of premium backend names. Each needs its _API_KEY env var. T1_PREMIUM_COUNT controls how many of them run alongside SearXNG in every request (round-robin selection); the rest are used in the fallback loop when the merged result count is below SUFFICIENT_MIN_RESULTS.

ProviderEnv varFree tierProduction
BraveBRAVE_API_KEY$5 credit (1,000/mo)✅ Yes
ExaEXA_API_KEY$20 + $10/mo (~2,800 searches)✅ Yes
JinaJINA_API_KEY10M tokens, 500 RPM✅ Yes
TavilyTAVILY_API_KEY1,000 credits/mo✅ Yes

Example:

FALLBACK_PROVIDERS=brave,exa,jina
BRAVE_API_KEY=xxx
EXA_API_KEY=xxx
JINA_API_KEY=xxx

Keyless mode (no API keys) works out of the box using SearXNG's free engines (Bing, Wikipedia, GitHub, etc.). Premium providers require their respective API keys.

Features

  • Speculative executionT1_PREMIUM_COUNT premium providers are selected via atomic round-robin and invoked in the hot path while SearXNG runs concurrently; premium calls are serial within the pass. Results are merged and deduplicated by URL.
  • Bounded fallback loop — if merged results < SUFFICIENT_MIN_RESULTS, remaining Tier 2 providers are tried via round-robin until threshold, exhaustion, or FALLBACK_TIMEOUT_SECONDS.
  • Circuit breaker per engine — 4xx on an engine opens the circuit for 5 min; auto-recovers
  • Exponential backoff retry — 3 retries with 1s/2s/4s backoff on 5xx/timeout
  • Prometheus /metrics — 15+ gauges and counters prefixed searxng_gateway_
  • LRU cache — 1000 entries, 1h TTL, in-memory
  • SearXNG config tuning — reference examples/searxng/ with engine selection, suspended_times tuning, custom User-Agent, and custom Python engines (Serper, Mojeek)
  • Fallback billing alertengine_results_total tracks premium API usage so you can alert before hitting quota limits

Observability

The gateway exposes Prometheus metrics at :8080/metrics.

Grafana dashboard

Full dashboard

A reference dashboard is included at examples/grafana/searxng-gateway-dashboard.json. Import it into Grafana, select your Prometheus datasource, and you'll see:

Circuit Breaker State (per engine)

CB State Timeline

Each engine gets its own row. 🟢 Closed → 🟡 Half-Open → 🔴 Open. When an engine returns 4xx, the circuit opens for 5 minutes, then auto-recovers.

Circuit Breaker Trips (cumulative)

CB Trips

Each trip means the gateway caught a 4xx and opened the circuit before the engine could degrade further queries. Colored by reason: rate_limited, access_denied, captcha.

Circuit Breaker Recoveries (auto-healing)

CB Recoveries

After 5 minutes of cooldown, the gateway probes the engine. If it responds, the circuit closes and a recovery is recorded.

Cache hit rate & Cache size

Cache panels

Repeated queries are served from the in-memory LRU cache (1000 entries, 1h TTL). "Cache hit rate" is the percentage of requests answered from cache (100 * rate(cache_hit) / rate(total)); "Cache size" tracks the current entry count. A low hit rate means every query is hitting SearXNG and premium providers — tune CACHE_SIZE/CACHE_TTL_SECONDS accordingly.

Alerting

Example Prometheus alert rules (vmalert/Mimir compatible):

groups:
  - name: searxng-gateway
    rules:
      - alert: SearxngCBStuckOpen
        expr: searxng_gateway_circuit_breaker_state == 2
        for: 5m
        annotations:
          summary: "Circuit breaker stuck open for engine {{ $labels.engine }}"
          
      - alert: SearxngRetryExhausted
        expr: rate(searxng_gateway_retry_exhausted_total[5m]) > 0.01
        for: 5m
        annotations:
          summary: "Retry exhaustion rate elevated"
          
      - alert: FallbackBillSpike
        expr: rate(searxng_gateway_engine_results_total[1h]) * 3600 > 100
        for: 10m
        annotations:
          summary: "Fallback API usage > 100 calls/hour — check billing"

Endpoints

  • GET /search?q=<query>&format=json — proxy endpoint
  • GET /healthz — liveness
  • GET /metrics — Prometheus exposition

Env vars

VarDefaultRequiredDescription
LISTEN_ADDR:8080noHTTP listen address
SEARXNG_BACKEND_URLhttp://searxng-primary:8080noSearXNG instance URL
FALLBACK_PROVIDERSbravenoComma-separated list of premium provider names
BRAVE_API_KEYnoBrave Search API key
EXA_API_KEYnoExa Search API key
JINA_API_KEYnoJina Search API key
TAVILY_API_KEYnoTavily Search API key
SUFFICIENT_MIN_RESULTS1noTarget merged result count; loop stops when reached (recommend 10 with premiums)
T1_PREMIUM_COUNT0noNumber of premium providers to call in the hot path while SearXNG runs (0 = none; premium calls are serial within the pass)
FALLBACK_TIMEOUT_SECONDS30noMaximum time for speculative execution + fallback loop
SEARXNG_TIMEOUT_SECONDS25noPer-request timeout for SearXNG
SEARXNG_FAIL_THRESHOLD6noConsecutive SearXNG failures before cooldown
SEARXNG_FAIL_COOLDOWN_SECONDS180noCooldown duration for SearXNG (seconds)
BRAVE_FAIL_THRESHOLD3noConsecutive Brave failures before cooldown
BRAVE_FAIL_COOLDOWN_SECONDS300noCooldown duration for Brave (seconds)
BRAVE_TIMEOUT_SECONDS15noPer-request timeout for Brave API
CACHE_SIZE1000noLRU cache entries (in-memory)
CACHE_TTL_SECONDS3600noCache entry TTL (seconds)
LOG_LEVELinfonoLog level (debug, info, warn, error)
METRICS_PATH/metricsnoPrometheus metrics endpoint path

Adding a new provider

Implement the SearchBackend interface in backends/:

type MyProvider struct { APIKey string; Timeout time.Duration }

func (m *MyProvider) Name() string { return "myprovider" }
func (m *MyProvider) IsAvailable() bool { return m.APIKey != "" }
func (m *MyProvider) Search(opts SearchOptions) ([]SearchResult, error) { /* ... */ }

Then add a case to backends/factory.go and set MYPROVIDER_API_KEY in the environment. The rest (registry, fallback chain, circuit breaker) is automatic.

Reference deployment

  • docker-compose.example.yml — SearXNG + gateway, one command
  • examples/searxng/ — reference SearXNG config with multi-tier engine posture
  • examples/searxng-engines/ — custom SearXNG engines (Serper, Mojeek API)
  • docs/architecture.md — design decisions, circuit breaker, metrics
  • docs/keyless.md — how to run without any API keys

Build

The image is built and pushed automatically by GitHub Actions on every push to main and on v* tags: see .github/workflows/build.yml (docker/build-push-action@v6, platforms linux/amd64,linux/arm64, gha cache, push to GHCR). No local multi-arch build needed — the local multiarch buildx builder was removed from the homelab (2026-08-04).

License

MIT — see LICENSE.

Contributors

WismutHansen

31 commits

Ghilteras/searxng-gateway

Decision proxy in front of SearXNG with Brave Search API fallback

Go

7

31 commits

updated Sep 17, 2026

See the code
ai-agents
circuit-breaker
go
openclaw
opencode
proxy
search
searxng
self-hosted-search

See what people are saying (1)

SourceMessageScoreDate

Couldn’t find a FOSS resilience layer for SearXNG, so I built one (r/opensource)

I wanted a more reliable self-hosted search setup using SearXNG, but free search engines can fail silently or become rate-limited. That can leave a query with only a few results even though more providers are available. I tried addressing this through SearXNG configuration, but I couldn’t find an…

2

Sep 17, 2026

README

searxng-gateway

Decision proxy in front of SearXNG: speculative execution — starts SearXNG and the configured premium-provider pass concurrently, selects providers via round-robin, invokes premium providers serially within that pass, merges results with URL dedup, and loops through remaining providers until a configurable threshold is met or timeout expires. Same JSON shape as SearXNG, Prometheus /metrics, in-memory LRU cache.

🚀 Works with zero API keys in keyless mode. See docs/keyless.md.

Originally derived from sx; adds HTTP server, per-engine circuit breaker, Prometheus metrics, cache, and Docker packaging.

Quick Start

# 1. Clone
git clone https://github.com/Ghilteras/searxng-gateway.git
cd searxng-gateway

# 2. (Optional) Create .env with API keys — or skip for keyless mode
echo 'BRAVE_API_KEY=your_key' > .env
echo 'SERPER_API_KEY=your_key' >> .env

# 3. Start
docker compose -f docker-compose.example.yml up -d

# 4. Test
curl 'http://localhost:8080/search?q=hello+world&format=json'
curl 'http://localhost:8080/metrics'

Architecture

Client ───▶ searxng-gateway (:8080) ───▶ SearXNG (Tier 1 free engines)
                    │                        │
                    │  ┌─────────────────────┤
                    │  │ Speculative execution│
                    │  │ (parallel round-robin)│
                    │  └─────────────────────┤
                    │                        ├── Serper (Google via API)
                    │                        ├── Bing, Wikipedia, GitHub...
                    │                        └── Circuit breaker per engine
                    │
                    └──▶ Tier 2 hot path (T1_PREMIUM_COUNT providers)
                    │    ├── Brave ──┐
                    │    ├── Exa     ├── round-robin alongside SearXNG;
                    │    ├── Jina    │   with SearXNG. Dedup URL.
                    │    └── Tavily ─┘
                    │
                    └──▶ Fallback loop (if merged < SUFFICIENT_MIN_RESULTS)
                         Round-robin through remaining Tier 2 providers
                         until threshold, exhaustion, or FALLBACK_TIMEOUT

See docs/architecture.md for the full design.

Supported fallback providers

Set FALLBACK_PROVIDERS to a comma-separated list of premium backend names. Each needs its _API_KEY env var. T1_PREMIUM_COUNT controls how many of them run alongside SearXNG in every request (round-robin selection); the rest are used in the fallback loop when the merged result count is below SUFFICIENT_MIN_RESULTS.

ProviderEnv varFree tierProduction
BraveBRAVE_API_KEY$5 credit (1,000/mo)✅ Yes
ExaEXA_API_KEY$20 + $10/mo (~2,800 searches)✅ Yes
JinaJINA_API_KEY10M tokens, 500 RPM✅ Yes
TavilyTAVILY_API_KEY1,000 credits/mo✅ Yes

Example:

FALLBACK_PROVIDERS=brave,exa,jina
BRAVE_API_KEY=xxx
EXA_API_KEY=xxx
JINA_API_KEY=xxx

Keyless mode (no API keys) works out of the box using SearXNG's free engines (Bing, Wikipedia, GitHub, etc.). Premium providers require their respective API keys.

Features

  • Speculative executionT1_PREMIUM_COUNT premium providers are selected via atomic round-robin and invoked in the hot path while SearXNG runs concurrently; premium calls are serial within the pass. Results are merged and deduplicated by URL.
  • Bounded fallback loop — if merged results < SUFFICIENT_MIN_RESULTS, remaining Tier 2 providers are tried via round-robin until threshold, exhaustion, or FALLBACK_TIMEOUT_SECONDS.
  • Circuit breaker per engine — 4xx on an engine opens the circuit for 5 min; auto-recovers
  • Exponential backoff retry — 3 retries with 1s/2s/4s backoff on 5xx/timeout
  • Prometheus /metrics — 15+ gauges and counters prefixed searxng_gateway_
  • LRU cache — 1000 entries, 1h TTL, in-memory
  • SearXNG config tuning — reference examples/searxng/ with engine selection, suspended_times tuning, custom User-Agent, and custom Python engines (Serper, Mojeek)
  • Fallback billing alertengine_results_total tracks premium API usage so you can alert before hitting quota limits

Observability

The gateway exposes Prometheus metrics at :8080/metrics.

Grafana dashboard

Full dashboard

A reference dashboard is included at examples/grafana/searxng-gateway-dashboard.json. Import it into Grafana, select your Prometheus datasource, and you'll see:

Circuit Breaker State (per engine)

CB State Timeline

Each engine gets its own row. 🟢 Closed → 🟡 Half-Open → 🔴 Open. When an engine returns 4xx, the circuit opens for 5 minutes, then auto-recovers.

Circuit Breaker Trips (cumulative)

CB Trips

Each trip means the gateway caught a 4xx and opened the circuit before the engine could degrade further queries. Colored by reason: rate_limited, access_denied, captcha.

Circuit Breaker Recoveries (auto-healing)

CB Recoveries

After 5 minutes of cooldown, the gateway probes the engine. If it responds, the circuit closes and a recovery is recorded.

Cache hit rate & Cache size

Cache panels

Repeated queries are served from the in-memory LRU cache (1000 entries, 1h TTL). "Cache hit rate" is the percentage of requests answered from cache (100 * rate(cache_hit) / rate(total)); "Cache size" tracks the current entry count. A low hit rate means every query is hitting SearXNG and premium providers — tune CACHE_SIZE/CACHE_TTL_SECONDS accordingly.

Alerting

Example Prometheus alert rules (vmalert/Mimir compatible):

groups:
  - name: searxng-gateway
    rules:
      - alert: SearxngCBStuckOpen
        expr: searxng_gateway_circuit_breaker_state == 2
        for: 5m
        annotations:
          summary: "Circuit breaker stuck open for engine {{ $labels.engine }}"
          
      - alert: SearxngRetryExhausted
        expr: rate(searxng_gateway_retry_exhausted_total[5m]) > 0.01
        for: 5m
        annotations:
          summary: "Retry exhaustion rate elevated"
          
      - alert: FallbackBillSpike
        expr: rate(searxng_gateway_engine_results_total[1h]) * 3600 > 100
        for: 10m
        annotations:
          summary: "Fallback API usage > 100 calls/hour — check billing"

Endpoints

  • GET /search?q=<query>&format=json — proxy endpoint
  • GET /healthz — liveness
  • GET /metrics — Prometheus exposition

Env vars

VarDefaultRequiredDescription
LISTEN_ADDR:8080noHTTP listen address
SEARXNG_BACKEND_URLhttp://searxng-primary:8080noSearXNG instance URL
FALLBACK_PROVIDERSbravenoComma-separated list of premium provider names
BRAVE_API_KEYnoBrave Search API key
EXA_API_KEYnoExa Search API key
JINA_API_KEYnoJina Search API key
TAVILY_API_KEYnoTavily Search API key
SUFFICIENT_MIN_RESULTS1noTarget merged result count; loop stops when reached (recommend 10 with premiums)
T1_PREMIUM_COUNT0noNumber of premium providers to call in the hot path while SearXNG runs (0 = none; premium calls are serial within the pass)
FALLBACK_TIMEOUT_SECONDS30noMaximum time for speculative execution + fallback loop
SEARXNG_TIMEOUT_SECONDS25noPer-request timeout for SearXNG
SEARXNG_FAIL_THRESHOLD6noConsecutive SearXNG failures before cooldown
SEARXNG_FAIL_COOLDOWN_SECONDS180noCooldown duration for SearXNG (seconds)
BRAVE_FAIL_THRESHOLD3noConsecutive Brave failures before cooldown
BRAVE_FAIL_COOLDOWN_SECONDS300noCooldown duration for Brave (seconds)
BRAVE_TIMEOUT_SECONDS15noPer-request timeout for Brave API
CACHE_SIZE1000noLRU cache entries (in-memory)
CACHE_TTL_SECONDS3600noCache entry TTL (seconds)
LOG_LEVELinfonoLog level (debug, info, warn, error)
METRICS_PATH/metricsnoPrometheus metrics endpoint path

Adding a new provider

Implement the SearchBackend interface in backends/:

type MyProvider struct { APIKey string; Timeout time.Duration }

func (m *MyProvider) Name() string { return "myprovider" }
func (m *MyProvider) IsAvailable() bool { return m.APIKey != "" }
func (m *MyProvider) Search(opts SearchOptions) ([]SearchResult, error) { /* ... */ }

Then add a case to backends/factory.go and set MYPROVIDER_API_KEY in the environment. The rest (registry, fallback chain, circuit breaker) is automatic.

Reference deployment

  • docker-compose.example.yml — SearXNG + gateway, one command
  • examples/searxng/ — reference SearXNG config with multi-tier engine posture
  • examples/searxng-engines/ — custom SearXNG engines (Serper, Mojeek API)
  • docs/architecture.md — design decisions, circuit breaker, metrics
  • docs/keyless.md — how to run without any API keys

Build

The image is built and pushed automatically by GitHub Actions on every push to main and on v* tags: see .github/workflows/build.yml (docker/build-push-action@v6, platforms linux/amd64,linux/arm64, gha cache, push to GHCR). No local multi-arch build needed — the local multiarch buildx builder was removed from the homelab (2026-08-04).

License

MIT — see LICENSE.

Contributors

WismutHansen

31 commits

Languages

Go

98.8%