Skip to content

Build1 publisher2 min readPublished

searxng-gateway starts its paid providers before it knows whether SearXNG failed

The Go proxy keeps SearXNG's JSON shape, so callers need no change. Starting the paid work before SearXNG answers costs one billed provider call on every cache miss, plus a five-minute breaker on any 4xx.

The Engineer · Build desk

Photograph accompanying searxng-gateway starts its paid providers before it knows whether SearXNG failed
Photo: mozilla.org

What happened

  • searxng-gateway is a Go HTTP proxy that sits in front of a SearXNG instance and adds fallback to Brave, Exa, Jina and Tavily, a per-engine circuit breaker, an in-memory LRU cache and Prometheus metrics.
  • On a cache miss it starts SearXNG and a premium pass at the same time, selecting T1_PREMIUM_COUNT providers by atomic round-robin and invoking them one after another inside that pass.
  • If the merged result count falls below SUFFICIENT_MIN_RESULTS, a bounded loop tries the remaining providers round-robin until the threshold is met, the providers run out, or FALLBACK_TIMEOUT_SECONDS expires.
  • Request outcomes land on a fixed label set, and the timeout label is recorded in addition to the final outcome, so one request can increment two labels.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • cost Speculative execution puts the billed providers on the healthy path, so the operator funds one premium call per cache miss in order never to wait on a SearXNG timeout.
  • constraint Each replica keeps its own in-memory cache, so the cheapest request path gets weaker as a team scales the gateway out horizontally.
  • decision Anyone writing alert rules has to settle how the additive timeout label is treated before summing outcomes, or their request totals will read high whenever SearXNG is slow.
  • capability Keyless mode lets a team take the breaker, the cache and the metrics without opening a billing relationship with any search vendor.

The premium pass is unconditional. Starting it alongside SearXNG is the point, and the cost: on any cache miss with `T1_PREMIUM_COUNT` set to 1 or more, at least one paid provider call goes out even when SearXNG comes back healthy with results to spare [1]. The bill is one provider call per miss.

Because those calls run one at a time inside the pass [8], the pass costs at most `T1_PREMIUM_COUNT` calls and its latency is the sum of all of them [2]. Set the count to 3 and the concurrent path now contains three sequential round trips.

The breaker is aimed at a specific failure population: free engines where one returns 403, another is rate-limited, a third serves a captcha, and a fourth slows until it times out. SearXNG reports which engines were unresponsive but makes no per-request decision about it [16]. One 4xx-class response opens that engine for five minutes, and a single probe decides recovery [5]. 429 is 4xx-class, so a rate limit trips the breaker on the first refusal. An open engine therefore gets at most 12 recovery attempts an hour [3]. If the upstream's limit window is shorter than five minutes, you are discarding capacity you already had back; if the probe lands on a captcha, the engine sits out another five.

Sum the outcome labels and the total comes out as requests plus timeouts, because a SearXNG deadline-exceeded error increments `timeout` in addition to the final outcome [4]. "Contributed" means a provider's results survived URL dedup into the merged set [13]. So the split between `searxng_plus_premium_ok` and `premium_ok` depends on dedup order as much as on which backend answered. The project ships an importable Grafana dashboard and example alert rules [17].

Adoption is cheap on the client side. The endpoint is one URL and the fields are the ones SearXNG already emits (`title`, `url`, `content`, `engine`, `engines`). Premium responses are normalised into that shape at merge time, and the client never talks to SearXNG directly [3]. Liveness is `/healthz`, exposition is `/metrics`, and the metrics path moves via `METRICS_PATH` [4]. The project is MIT, at v2.6.3, derived from `sx`, with Docker packaging [14]. Keyless mode runs on SearXNG's free engines with no API keys at all [15].

The LRU cache is in-memory [1]. Run two replicas behind a round-robin balancer and you have two caches, so a repeated query lands on the replica that cached it about half the time [5]. The two numbers to set from your own traffic are `SUFFICIENT_MIN_RESULTS` and `FALLBACK_TIMEOUT_SECONDS` [10].

What to watch

  • Whether breaker state and the LRU cache move to shared storage, which would make multi-replica deployments behave like one gateway.
  • Whether the premium pass becomes conditional on a SearXNG deadline, so paid calls go out only when SearXNG misses it.
  • Whether the project documents which copy of a duplicated URL survives the merge, since that decides the premium_ok and searxng_plus_premium_ok split.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories