Flask-Limiter 初始化必须显式指定 storage,否则多进程下静默失效;rate string 须用整数+标准单位缩写;反代场景需自定义 key_func 提取真实 IP;@limiter.exempt 不跳过全局规则。

Flask-Limiter 初始化时必须指定 storage,否则 404 或 500 静默失败
很多人在第一次用 Flask-Limiter 时,只写了 Limiter(app) 就以为完事了,结果请求完全没被限流,甚至某些路由返回 404(尤其是用了 @limiter.exempt 后反而挂掉)。根本原因是:默认 storage 是 memory://,但它在多进程/多线程下不可靠;而如果你用 gunicorn/uwsgi 启动,又没显式配置 storage,它会 fallback 到一个无效后端,导致限流逻辑直接跳过。
实操建议:
- 开发环境可先用
redis://localhost:6379(需装 redis-server 和redisPython 包) - 没 Redis?至少显式写上
storage_uri="memory://",并接受「仅单 worker 有效」的限制 - 别信文档里「自动选择」的说法——
Limiter(app, key_func=get_remote_address)这种写法不指定 storage,大概率失效 - 验证是否生效:在视图里加一行
print(limiter._storage.check("test", 1, 1)),看是否返回True
@limiter.limit 的 rate string 格式不能省略单位,且数字必须是整数
写成 @limiter.limit("5/minute") 没问题,但 @limiter.limit("5/m")、@limiter.limit("5 per minute") 或 @limiter.limit("5.5/minute") 全部报错,错误信息是:ValueError: Invalid rate limit string。这个解析器非常严格,只认固定单位缩写,且不支持浮点。
常见单位缩写:
-
/second、/minute、/hour、/day - 别用中文、空格、“per”、“每”、“次”等任何非标准字符
- 想实现“10 次/2 分钟”?只能写
"10/120seconds"(注意是seconds,不是second) - 多个规则叠加?用分号分隔:
"5/minute;100/hour"
基于 IP 限流必须重写 key_func,默认的 get_remote_address 在反代后失效
本地跑没问题,一上 Nginx 就发现所有请求都算成同一个 IP(比如全是 127.0.0.1),因为 Flask 默认读 request.remote_addr,而反向代理会把真实 IP 放在 X-Forwarded-For 头里。不处理,限流就变成「对服务器限流」而不是「对用户限流」。
实操建议:
- 在初始化 Limiter 前,先定义安全的 IP 提取函数:
def get_real_ip():
if request.headers.get("X-Forwarded-For"):
return request.headers["X-Forwarded-For"].split(",")[0].strip()
return request.remote_addr
- 初始化时传进去:
Limiter(app, key_func=get_real_ip, storage_uri="redis://...") - 别忘了 Nginx 配置里加:
proxy_set_header X-Forwarded-For $remote_addr;,否则头根本传不过来 - 如果用了 Cloudflare,还得额外检查
Cf-Connecting-Ip头,否则可能拿到 Cloudflare 节点 IP
全局限流和路由级限流混用时,@limiter.exempt 不会跳过已声明的全局规则
很多人以为给某个视图加了 @limiter.exempt 就万事大吉,结果发现它还是被卡在全局 @app.before_request 或 limiter.limit(..., key_func=...) 里。这是因为 @limiter.exempt 只影响「直接装饰该视图的限流规则」,对通过 limiter.limit() 手动绑定或全局应用的规则无效。
正确做法:
- 全局规则尽量用
limiter.limit("...", per_method=False)显式控制范围 - 要豁免某路由?优先用
@limiter.exempt+ 确保没其他地方重复调用limiter.limit - 更稳妥的方式:把 exempt 逻辑下沉到
key_func里,比如返回"exempt_" + str(uuid.uuid4())让 key 失效 - 调试时打印
limiter.current_limit和limiter.get_limits()结果,确认哪些规则实际生效了
IP 限流真正难的不是写几行代码,而是 storage 选型、反代头透传、key 冲突这三处——任一漏掉,看起来跑通了,上线后全白搭。










