--- title: "SearXNG API not working? Fixes for 403, 429 and empty results, and the alternatives" category: "guides" url: https://keirolabs.cloud/blog/searxng-api-alternatives --- ## The 60-second version **SearXNG is a free, self-hosted metasearch engine. It has no index of its own: every query you send is forwarded to Google, Bing, DuckDuckGo and other engines from your server's IP, and the results are merged.** That design is why it's free and private, and also why it fails under agent traffic. Almost every "SearXNG API not working" problem is one of three things: - **`403 Forbidden` on `format=json`.** The JSON output format is off by default. Add `json` to `search.formats` in `settings.yml` and restart. - **`429 Too Many Requests`.** SearXNG's own bot limiter is blocking your app. Turn it off on a private instance (`server.limiter: false`), or allow your app's IP. - **`200 OK` but empty or thin results.** Google, Bing or DuckDuckGo are blocking your server with CAPTCHAs or rate limits, and SearXNG has suspended them. You'll see them listed in `unresponsive_engines`. This one can't be fully fixed with settings, because the cause is that SearXNG scrapes engines that block scrapers. The first two are one-line fixes. For the third, the reliable answer is a search API that runs its own index, so there's no upstream engine to block it. Keirolabs `/search/lite` costs $0.10 per 1,000 searches on the Essential plan ($0.03 on Startup), comes with 750 free credits, and the drop-in function below returns SearXNG's exact field names. You can also keep SearXNG first and only fall back when it comes back empty. ## How does the SearXNG JSON API work? You call your instance's `/search` endpoint with a query and `format=json`: ```bash curl "http://localhost:8080/search?q=rust+async+runtime&format=json" ``` A healthy response has a `results` array. Each result carries `url`, `title`, `content` (the snippet), `engine`, `engines` (every engine that returned it), `score` and `category`. Alongside it you get `answers`, `suggestions`, `infoboxes`, `number_of_results`, and the field that matters most when something is wrong: `unresponsive_engines`, a list of `[engine, reason]` pairs for every engine that failed on this query. The useful parameters are `q`, `format`, `categories` (for example `general`, `news`, `it`), `engines` (a comma-separated list to force specific engines), `language`, `time_range` (`day`, `month`, `year`), `pageno` and `safesearch`. ## Why does SearXNG return 403 Forbidden for format=json? New instances serve HTML only. Any request with `format=json` is refused with 403 until you enable the format. This is the single most common reason Open WebUI, LangChain or a custom agent "can't connect" to a freshly installed SearXNG. Open `settings.yml` (in the official Docker setup it sits in the mounted `searxng` config folder) and add `json` to the formats list: ```yaml search: formats: - html - json ``` Then restart the container: ```bash docker compose restart searxng ``` Test it with the `curl` call above. If you get JSON back, the format is on. If you still get 403, check that you edited the file the container actually mounts. A settings file in the wrong folder is the usual cause. ## Why does SearXNG return 429 Too Many Requests? SearXNG ships with bot protection called the limiter. It exists to protect public instances from scrapers, and it can't tell your agent from one: agents send many requests in quick bursts, often without browser headers, from a single IP. So the limiter answers with 429. On a private instance that only your own apps can reach, switch it off: ```yaml server: limiter: false ``` If the instance is reachable from the internet, keep the limiter on and add your app's IP to the pass list in the limiter configuration instead. A public SearXNG instance with the limiter off gets found and abused quickly. ## Why does SearXNG return empty results? This is the failure that actually hurts, because the request succeeds. You get a `200 OK` with two results instead of twenty, or none at all, and nothing in your logs looks like an error. The cause is upstream. When SearXNG forwards your query to Google, the request comes from your server's IP. A person searching a few dozen times a day looks normal. An agent sending hundreds of queries an hour, often several in parallel, looks exactly like what it is, and the engines respond with CAPTCHAs, "unusual traffic" pages and rate limits. SearXNG handles each of these by suspending the engine for a cooldown period, set by `search.suspended_times` in `settings.yml`. By default a CAPTCHA suspends an engine for a full day, and a "too many requests" response for an hour. While an engine is suspended, every query is answered by the engines that are left. If Google, Bing and DuckDuckGo are all suspended, you're left with Wikipedia and a few small engines, and your agent starts saying "I couldn't find anything about that." ## How do you check which SearXNG engines are blocked? Read `unresponsive_engines` on a test query: ```python import requests r = requests.get( "http://localhost:8080/search", params={"q": "rust async runtime comparison", "format": "json"}, timeout=15, ).json() print(len(r["results"]), "results") for engine, reason in r.get("unresponsive_engines", []): print(f" {engine}: {reason}") ``` Typical output on a blocked instance: ``` 2 results google: CAPTCHA duckduckgo: too many requests bing: timeout ``` The admin preferences page of your instance also shows engine statistics, including error rates and response times, which is a quick way to see which engines have been failing over the last hours. If you run SearXNG in production, log `unresponsive_engines` on every call and alert when it stays non-empty. That turns a silent failure into a visible one. ## What helps when SearXNG engines get blocked? Some changes genuinely help: - **Enable more engines.** If Google is suspended but Brave, Mojeek, Qwant, Startpage and Wikipedia still answer, you get thinner results instead of none. Engines are switched on in the `engines:` section of `settings.yml`. - **Slow the agent down.** Space out searches and avoid firing ten parallel queries per agent step. Block rates rise sharply with burstiness. - **Retry once with a rephrased query.** Different engines, or a cached result, may answer the second form. - **Cache repeat queries** in your app, so the same question doesn't hit the engines twice. Other fixes don't last: - **Rotating IPs or adding proxies.** It works for a while. But you're now running a scraping operation against anti-bot systems that commercial providers spend heavily to get past, and you're paying for residential proxies at around $1 to $8 per GB. At that point a hosted API is cheaper. - **Public instances.** They block automated traffic, often disable JSON, and change without notice. Never point an agent at one. ## Why does SearXNG seem to fail more in 2025 and 2026? Because the engines it depends on got harder to scrape. Since January 2025 Google requires JavaScript to show search results, which broke or weakened plain-HTTP scrapers across the industry, and SearXNG's Google engine is one of them. Bot detection on the big engines keeps tightening, and the official alternatives are going away: Microsoft retired the Bing Search API in August 2025, and Google's Custom Search JSON API is closed to new customers and shuts down on January 1, 2027. Every tool that sits on top of someone else's search engine is under more pressure than it was two years ago. ## How do you fix SearXNG in Open WebUI? Open WebUI calls SearXNG's JSON API, so all three problems above show up there as "no results." Check them in order: 1. In **Admin Settings → Web Search**, pick SearXNG and set the query URL with `` where the search terms go, for example `http://searxng:8080/search?q=`. Use the container name when both run in the same Docker network, and the host and port otherwise. The same value can be set with the `SEARXNG_QUERY_URL` environment variable. 2. Make sure `json` is in `search.formats`. Without it every search returns 403. 3. Turn the limiter off, or allow Open WebUI's IP, so its searches don't get 429s. 4. If results are still empty, run the `unresponsive_engines` check above. The problem has moved upstream. If you'd rather not run SearXNG at all, Open WebUI also supports hosted search providers in the same settings screen, and it can call external tools over MCP. Keirolabs' MCP server lives at `https://api.keirolabs.cloud/mcp`. ## How do you fix SearXNG in LangChain? LangChain's `SearxSearchWrapper(searx_host="http://localhost:8080")` calls the same JSON API, so it hits the same three errors. Fix 403 and 429 on the instance as above. To protect the agent from empty results, replace the wrapper with a tool around the fallback function at the end of this post. It returns the same fields whichever backend answered. ## When should you stop fixing SearXNG? SearXNG is excellent software for what it was built for: private search for a person or a small team, with no tracking and no bill. It's the wrong foundation when: - empty or thin results are affecting your agent's answers often enough that users notice; - you spend more time tuning engines and chasing blocks than working on your product; - you need predictable throughput, such as hundreds of searches a minute; - your product's quality depends on search working every time, not most of the time. ## What are the best SearXNG alternatives? The alternatives that fix SearXNG's core problem are the ones that don't scrape someone else's search engine. - **Keirolabs `/search/lite`** returns titles, URLs and snippets from Keirolabs' own index, at $0.10 per 1,000 searches on the $29.99 Essential plan, about $0.06 on Pro and $0.03 on Startup. New accounts get 750 free credits with no card. Nothing upstream can block it, because there's no upstream. When you need page text as well, `/search/content` returns results with clean markdown for each page. One catch: the plan rates need a subscription. Free credits and one-time packs bill lite search at 1 credit each. - **Brave Search API** runs one of the few large independent indexes, at $5 per 1,000 searches. It dropped its free plan in February 2026, so a card is required, with a $5 monthly credit. - **Exa** runs a neural index, strongest at "find pages like this one" style discovery, at $7 per 1,000 searches for up to ten results. - **Serper** returns Google's own results for $1 per 1,000, with 2,500 free queries once. It still scrapes Google, but blocks and proxies are its problem, not yours. Choose it when you specifically need Google's ranking. - **Tavily** is common in LangChain tutorials, with 1,000 free credits a month and basic search at $8 per 1,000. For an agent, the deciding question is whether it needs Google's exact results (pick Serper) or reliable, relevant results (pick an own-index API). Most agents need the second. ## Drop-in replacement that returns SearXNG's format This function returns the same shape as SearXNG's JSON API (`results` with `url`, `title`, `content`, `engine` and `score`, plus an empty `unresponsive_engines`), so code that parses SearXNG keeps working without changes: ```python import os import requests def searxng_compatible_search(query: str, count: int = 10) -> dict: r = requests.post( "https://api.keirolabs.cloud/api/v2/search/lite", headers={"Authorization": f"Bearer {os.environ['KEIRO_API_KEY']}"}, json={"query": query, "count": count}, timeout=30, ) r.raise_for_status() hits = r.json()["results"] return { "query": query, "number_of_results": len(hits), "results": [ { "url": h["url"], "title": h["title"], "content": h.get("snippet", ""), "engine": "keirolabs", "engines": ["keirolabs"], "score": 1 / h.get("position", 1), } for h in hits ], "unresponsive_engines": [], } if __name__ == "__main__": for x in searxng_compatible_search("rust async runtime comparison")["results"][:5]: print(x["title"], "-", x["url"]) ``` `count` accepts 1 to 30. The score is rank-based (1, 0.5, 0.33 and so on), which keeps any "sort by score" logic working. ## Keep SearXNG and fall back only when it fails If SearXNG works for most of your queries, keep it as the first call and use the hosted API only when results come back thin or SearXNG errors out: ```python import os import requests SEARX_URL = os.environ.get("SEARX_URL", "http://localhost:8080") MIN_RESULTS = 3 def searxng(query: str) -> list[dict]: r = requests.get(f"{SEARX_URL}/search", params={"q": query, "format": "json"}, timeout=15) r.raise_for_status() return [ {"url": x["url"], "title": x["title"], "content": x.get("content", "")} for x in r.json().get("results", []) ] def keirolabs(query: str, count: int = 10) -> list[dict]: r = requests.post( "https://api.keirolabs.cloud/api/v2/search/lite", headers={"Authorization": f"Bearer {os.environ['KEIRO_API_KEY']}"}, json={"query": query, "count": count}, timeout=30, ) r.raise_for_status() return [ {"url": x["url"], "title": x["title"], "content": x.get("snippet", "")} for x in r.json()["results"] ] def web_search(query: str) -> list[dict]: try: results = searxng(query) if len(results) >= MIN_RESULTS: return results except requests.RequestException: pass return keirolabs(query) # thin, empty or failed ``` Count how often the fallback fires. If it fires on most queries, SearXNG isn't saving you money any more, and calling the API directly is simpler and faster. If it rarely fires, you have free search with a safety net. ## How we checked this The settings, response fields and error behavior come from SearXNG's documentation and default `settings.yml`. Provider prices come from each vendor's pricing page as of November 2026. Keirolabs prices are our published plan rates.