SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

SearXNG 429错误:速率限制与IP封锁排查

SearXNG 出现 429 可能是本地限制器返回,也可能是 Google 等上游封锁服务器 IP。查看日志中的限制器、引擎和超时信息,约 1 分钟判断原因并修复。

SearXNG 返回 429 错误的原因

自托管 SearXNG 实例返回 429 错误通常有两个互不相关的原因,而需要修复的速率限制通常不是您以为的那个。第一个原因发生在本地:SearXNG 自身的限制器判断请求来自机器人,并以状态码 429 返回 Too Many Requests。第二个原因发生在上游:某个搜索引擎拒绝了您服务器的 IP 地址,用户看到的结果页会缺少部分内容,而不是收到 429。

这两种情况没有共同的修复方法。限制器由您控制,因此可以修改。上游封锁发生在 Google 一侧,因此您在 settings.yml 中进行任何修改都无法解除封锁。通过日志,您通常可以在约 1 分钟内确认属于哪种情况,因此应从日志开始排查。

本指南假定您使用的是 在自己的 VPS 上部署自托管 SearXNG 实例 中介绍的容器安装方式。以下每个设置名称都来自当前上游文档和源代码,并已在 August 2026 核对。

先查看日志,再修改设置

打开日志窗口后重现问题。

cd ./searxng/
docker compose logs -f searxng-core

限制器消息来自名为 searx.limiter 的日志记录器,并会标明 IP 地址。命中阻止列表时,日志内容为 BLOCK 203.0.113.10: matched BLOCKLIST;命中允许列表时,日志内容为 PASS 203.0.113.10: matched PASSLIST。如果限制器无法访问计数器存储,日志会显示 The limiter requires Valkey, please consult the documentation,这表示根本没有记录任何计数。

每次单独的机器人检查都会以 debug 级别记录,因此默认情况下看不到。将 settings.yml 中的 debug 级别临时开启,执行一次测试:

general:
  debug: true

随后,日志会在客户端网络旁添加类似 NOT OK (http_accept_language) 的行,并标明失败的检查。测试完成后立即关闭 debug,因为上游明确要求已部署的实例不得启用 debug 运行。

引擎故障的表现完全不同。日志会标明引擎,而不是 IP;最常见的情况是超时:

HTTP requests timeout (search duration : 3.1 s, timeout: 3.0 s)

此问题还对应一个页面。将 enable_metrics 保持默认值 true 时,实例会在 /stats/errors 记录引擎错误;/preferences 会列出当前正在响应的引擎。如果 /stats/errors 已满,而日志中没有任何 searx.limiter 行,则问题不在限制器。

调试前先固定版本

上游容器配置由两个文件组成。

mkdir -p ./searxng/core-config/
cd ./searxng/

curl -fsSL \
    -O https://raw.githubusercontent.com/searxng/searxng/master/container/docker-compose.yml \
    -O https://raw.githubusercontent.com/searxng/searxng/master/container/.env.example

cp -i .env.example .env

Compose 文件会拉取 docker.io/searxng/searxng:${SEARXNG_VERSION:-latest}。未设置的变量表示 latest,而 latest 表示实例会在下一次 docker compose pull 时发生变化。因此,上周可用的设置可能不再匹配读取该设置的代码。SearXNG 标签包含日期和提交。以 2026 年 8 月为准,上游 .env.example 中的示例标签是 2026.3.25-541c6c3cb,因此请在 .env 中设置一个明确的版本:

SEARXNG_VERSION=2026.3.25-541c6c3cb

检查已发布的标签,并固定到你实际测试过的版本,然后针对固定目标进行调试。同一个 .env 文件还保存密钥,因此在将该目录提交到任何位置前,请阅读 Docker Compose 中环境文件和密钥的工作方式

限流器需要 Valkey,否则无法运行

限流器按客户端统计请求次数,并且这些计数必须在多个工作进程之间共享。用于存储这些计数的是 Valkey,它是 Redis 的维护分支。旧版 SearXNG 指南将此设置称为 redis:。当前版本读取 valkey:,因此应从当前文档复制键名,不要使用旧文章中的名称。

use_default_settings: true
server:
  secret_key: "change-this-value"
  limiter: true
  public_instance: false
valkey:
  url: valkey://searxng-valkey:6379/0

上游 compose 文件已经通过 searxng-valkey 服务运行 docker.io/valkey/valkey:9-alpine 镜像,因此该主机名可在 compose 网络中解析。同一个值也可以通过 SEARXNG_VALKEY_URL 环境变量设置。如果 SearXNG 和 Valkey 共用一台主机,也可以使用 Unix socket URL(unix:///path/to/socket.sock?db=0)。

存储服务缺失时的行为取决于另一个键。使用 public_instance: false 时,限流器会记录 Valkey 错误并退出,因此实例会继续提供服务,但完全不进行速率限制。使用 public_instance: true 时,进程会改为调用 sys.exit(1),因为开放且机器人防护失效的实例会在一天内从每个引擎收集 CAPTCHA(完全自动化的公共图灵测试,用于区分计算机和人类)。如果您刚设置 public_instance: true 后容器就循环重启,原因就是这个问题;每次退出前的最后一行日志会指出 Valkey。

限制器实际统计的内容

ChartSearXNG limiter: requests allowed per client IP, defaults in ip_limit.py
The data behind this chart
[
  {
    "label": "Burst, normal client",
    "max_requests": 15,
    "window": "20 seconds"
  },
  {
    "label": "Burst, flagged client",
    "max_requests": 2,
    "window": "20 seconds"
  },
  {
    "label": "Sustained, normal client",
    "max_requests": 150,
    "window": "10 minutes"
  },
  {
    "label": "Sustained, flagged client",
    "max_requests": 10,
    "window": "10 minutes"
  },
  {
    "label": "Any non-HTML format",
    "max_requests": 4,
    "window": "1 hour"
  },
  {
    "label": "Flagged requests before block",
    "max_requests": 3,
    "window": "30 days"
  }
]

普通客户端在 20 秒突发窗口内可发起 15 个请求,在 10 分钟窗口内可发起 150 个请求。一旦请求被标记为可疑,同一客户端在每个突发窗口内只能发起 2 个请求。最后一行的限制最严格:如果某个地址在 30 天窗口内产生 3 个被标记的请求,该地址会被重定向到起始页,而不是执行搜索,日志会记录 BLOCK: too many request from ... in SUSPICIOUS_IP_WINDOW (redirect to /)

这些数字是 searx/botdetection/ip_limit.py 中的常量。它们不是配置项,limiter.toml 也不会公开这些值,因此修改它们需要编辑源代码。/etc/searxng/limiter.toml 实际控制的是用于对客户端分组的地址前缀、受信任代理列表、可选的链接令牌检查,以及允许列表和阻止列表。

请求会通过请求头检查被标记为可疑。每项检查都有一个名称,您会在调试日志中看到这些名称:

  • http_acceptAccept 请求头不包含 text/html
  • http_accept_encodingAccept-Encoding 请求头既未指定 gzip,也未指定 deflate
  • http_accept_language:不存在 Accept-Language 请求头。
  • http_connectionConnection 请求头被设置为 close
  • http_user_agent:缺少 User-Agent,或其值匹配已知的 bot 模式。
  • http_sec_fetchSec-Fetch-ModeSec-Fetch-Dest 请求头的内容与浏览器发送的内容不一致。

浏览器会发送上述所有请求头。普通的 curl 调用几乎不会发送其中任何一个,因此手动编写的测试请求第一次尝试就会被标记,而相同的搜索在浏览器标签页中却能正常执行。这就是“浏览器中可以正常工作,但脚本收到 429”通常出现的原因,而不是异常现象。

反向代理后限流器会同时阻止所有人

这是破坏正常运行实例最常见的方式。SearXNG 会从 X-Forwarded-For 中第一个不受信任的 IP 获取客户端地址;如果无法获取,则回退到 X-Real-IP;如果仍无法获取,则使用建立连接的地址。是否信任这些请求头,由 trusted_proxieslimiter.toml 中决定。

如果代理地址不在该列表中,系统会忽略这些请求头,所有访客都会以代理地址访问。这样他们共用一个计数器;当总请求数在 10 分钟内超过 150 时,整个站点都会被同时阻止。一个用户多次刷新结果页,就可能导致所有人都无法访问。

过度信任的风险更大。如果列出了公网地址段,任何访客都可以发送自己的 X-Forwarded-For 请求头,并为每个请求选择新的身份。知道这一点的用户即可绕过限流。这里只列出您自己的代理发起连接时使用的地址。在 Docker 中,这通常是 172.16.0.0/12 内部的桥接网络;该配置行默认被注释掉。

[botdetection]
ipv4_prefix = 32
ipv6_prefix = 48

trusted_proxies = [
  '127.0.0.0/8',
  '::1',
  '172.16.0.0/12',
]

代理还必须发送这些请求头。Nginx 不会自动添加其中任何一个:

location / {
    proxy_pass http://127.0.0.1:8080;

    proxy_set_header Host              $host;
    proxy_set_header Connection        $http_connection;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
}

Caddy 和 Traefik 会为您设置转发请求头,因此使用这两个代理时,只需完成 trusted_proxies 部分。相关权衡见为自托管服务选择反向代理。要验证任一配置,请启用 debug,通过移动数据在手机上执行一次搜索,并确认日志行中的网络地址是您手机的地址,而不是代理的地址。

您的代理每小时只能发起 4 个 API 请求

默认禁用 JSON 输出,因此需要为代理启用该选项:

search:
  formats:
    - html
    - json

现在再次查看图表中的这一行。任何请求 HTML 以外格式的请求都会在独立窗口中计数:每个地址在每个 1 hour 内最多 4 个请求。研究代理在一个任务中就会耗尽配额,之后的每次调用都会返回 429。无法通过提高限制来解决,因为该数值写在源代码中。

正确的解决方法是告诉限流器,该客户端不是未知来源。将其地址添加到 limiter.toml 中的放行列表:

[botdetection.ip_lists]
block_ip = []

pass_ip = [
  '10.8.0.0/24',
]

pass_searxng_org = true

pass_ip 的优先级高于其他所有方法,因此加入放行列表的客户端也会跳过请求头检查,直接调用 curl 即可正常工作。放行范围应尽可能小,并优先使用 VPN 子网或容器网络,避免使用任何可路由网络。另一个正确的解决方法是让代理完全避开公网路径:将其指向内部网络上的容器地址。这样,代理及其限流器就看不到这些流量。配置方法请参阅为 AI 代理提供 SearXNG 搜索技能

应避免将代理指向他人运行的公共实例。这是导致志愿者的 IP 地址被上游搜索引擎封禁的最快方式,也是 JSON 格式默认禁用的原因。

引擎反过来阻止您时

ChartHow long SearXNG suspends an engine, search.suspended_times defaults
The data behind this chart
[
  {
    "label": "SearxEngineTooManyRequests",
    "suspended_seconds": 3600,
    "roughly": "1 hour"
  },
  {
    "label": "SearxEngineAccessDenied",
    "suspended_seconds": 86400,
    "roughly": "1 day"
  },
  {
    "label": "SearxEngineCaptcha",
    "suspended_seconds": 86400,
    "roughly": "1 day"
  },
  {
    "label": "recaptcha_SearxEngineCaptcha",
    "suspended_seconds": 604800,
    "roughly": "7 days"
  },
  {
    "label": "cf_SearxEngineCaptcha",
    "suspended_seconds": 1296000,
    "roughly": "15 days"
  }
]

当引擎返回自身的 429 响应或 CAPTCHA 页面时,SearXNG 会引发一个命名异常,并在一段时间内停止向该引擎发送请求。429(请求过多)响应会将其暂停 3600 秒。普通 CAPTCHA 或访问被拒绝的响应会将其暂停 1 day。通过 Cloudflare 提供的 CAPTCHA 会将其暂停 15 days,这是列表中的最长默认时长,因为这表示阻断发生在边缘层,重试不会有帮助。

普通故障使用不同的设置。超时或解析错误会根据 search.ban_time_on_fail 计算出一个较短的暂停时间。该值默认为 5 秒,并由 search.max_ban_time_on_fail 限制为最多 120 秒。因此,响应缓慢的引擎会在几分钟内自动恢复,而被阻断的引擎可能会停用数小时。这解释了一个常见的“随机”现象:开始时结果正常,随后某个引擎的结果在整个下午都消失。

在归咎于其他问题之前,应先处理超时。默认的 request_timeout 为 2.0 秒。对于距离引擎最近边缘服务器较远的小型 VPS,这个值可能过于严格。

outgoing:
  request_timeout: 3.0
  max_request_timeout: 10.0
engines:
  - name: bing
    timeout: 5.0

request_timeout 是所有引擎的默认值,max_request_timeout 是上限,单个引擎还可以设置自己的 timeout。提高这些值会增加页面延迟,但可减少失败。因此应以半秒为步长调整,并监控 /stats/errors,不要直接跳到 10。

如果某个引擎确实在阻止您的地址,请将其移除。每次搜索都要等待最慢的引擎,因此保留一个永久暂停的引擎只会增加延迟,而且不会返回结果。

use_default_settings:
  engines:
    remove:
      - google

使用 docker compose restart searxng-core 应用更改,然后执行几次搜索并重新加载 /stats/errors。实际使用 5 分钟后页面仍为空,说明更改已生效。

数据中心 IP 会被视为机器人

您的 VPS 地址属于托管服务网段,大型搜索引擎通常会将这些网段判定为自动化来源。其中一些引擎会对来自此类地址的每个请求提供 CAPTCHA,无论请求头设置得多规范、请求速率多慢。settings.yml 中的任何设置都无法改变这一判断。

您可以改变的是请求哪些引擎,以及是否公开列出您的实例。供一个家庭使用的私有实例通常不会触发限制。使用托管服务 IP 的公开实例会在限制最严格的引擎上不断遭到暂停,这属于软件的正常状态,而不是配置故障。SearXNG 可以通过 outgoing.proxiesoutgoing.using_tor_proxy 将引擎请求经由代理转发,从而改用其他 IP 地址。出口节点和廉价代理池的评分通常比托管服务网段更低,因此改用代理可能导致结果变差。

首先监控实例,及时发现问题

即使所有引擎都已暂停,SearXNG 仍会在其端口上响应。因此,只监控状态码的运行时间检查会继续显示正常,但实例实际不会返回内容。应检查响应内容:发起一次真实搜索,并匹配响应正文中预期出现的词语。Uptime Kuma 关键词监控无需额外工具即可完成此操作。每次版本升级后也要监控 /stats/errors,因为引擎的 HTML 会发生变化,解析器可能因此失效,而这与速率限制无关。

FAQ

Why does SearXNG return 429 to every visitor after I put it behind a reverse proxy?

Because the limiter is counting the proxy as the client. SearXNG only reads X-Forwarded-For when the connecting address is listed in trusted_proxies in /etc/searxng/limiter.toml. If it is not listed, every visitor shares one counter and they all cross the 150 requests per 10 minutes line together. Add the address your proxy connects from, which in Docker is usually the bridge range 172.16.0.0/12, and make sure the proxy sends X-Real-IP and X-Forwarded-For. Never list a range you do not control, because a trusted network lets any visitor set that header and choose a new identity for every request.

How many API requests per hour does the SearXNG limiter allow?

Four per IP address per hour. Any request asking for a format other than HTML counts in a separate one hour window, and that limit is set in searx/botdetection/ip_limit.py rather than in limiter.toml, so it cannot be raised from config. An agent or a script passes it in one task. Add the client's address to pass_ip in limiter.toml, or reach the instance over an internal network where the limiter never sees the request.

Why do my search results come back empty with no 429 error?

The engines are refusing your server, not your users. Open /stats/errors on your own instance: it names each engine that failed and why, and a CAPTCHA or access-denied entry means that engine blocked your server's IP address. SearXNG then suspends the engine, for an hour after a too-many-requests answer and for a day after a CAPTCHA. No local setting lifts an upstream block, so remove the engines that block your address and keep the ones that answer.

Should I enable the limiter on a private instance?

If nothing reaches the instance except you, leave limiter: false. It adds a Valkey dependency and it blocks your own scripts, and it protects against traffic you do not have. Enable it the moment the instance gets a public address, together with public_instance: true. That pair is deliberate: with public_instance: true and no working Valkey, the process exits with status 1 instead of running unprotected.