Echo 的 rate limiter 默认采用滑动窗口而非令牌桶,因其底层 Store 仅支持计数器增减,无法表达令牌桶所需的多状态(lastTick/tokens/limit)及原子操作;若需令牌桶,须手动集成 golang.org/x/time/rate。

为什么 echo.RateLimiter 默认不是令牌桶?
Echo 官方 middleware.RateLimiter 默认用的是滑动窗口(基于内存 map + 时间戳),不是令牌桶。它不支持平滑放行、突发流量透传、长期匀速消耗等典型令牌桶行为。如果你看到限流“突然卡住又全放行”,大概率是滑动窗口在作祟,而不是配置错了。
要真正用上令牌桶,得自己集成或换中间件。官方没内置 TokenBucketRateLimiter,也没封装 golang.org/x/time/rate.Limiter 的适配器。
用 golang.org/x/time/rate 手写令牌桶中间件
最轻量、可控性最强的做法:直接包装 rate.Limiter 为 Echo 中间件。注意它线程安全,但每个路由/用户需独立实例(否则共享桶会互相干扰)。
- 按 IP 限流:用
c.RealIP()做 key,维护map[string]*rate.Limiter+ 读写锁(sync.RWMutex) - 令牌生成速率设为
rate.Every(1 * time.Second / 10)表示每秒 10 个令牌(别用rate.Limit(10)直接除,浮点精度可能出错) - 桶容量建议 ≥ 2× 峰值期望并发,比如允许最多突发 20 次请求,就设
burst: 20 - 调用
limiter.Wait(c.Request().Context())阻塞等待令牌;若不想阻塞,改用limiter.AllowN(time.Now(), 1)判断
func TokenBucketMiddleware(burst int, avgRate float64) echo.MiddlewareFunc {
limiterMap := sync.Map{}
r := rate.Every(time.Second / time.Duration(avgRate))
return func(next echo.Handler) echo.Handler {
return echo.HandlerFunc(func(c echo.Context) error {
ip := c.RealIP()
if limiter, ok := limiterMap.Load(ip); ok {
if !limiter.(*rate.Limiter).Allow() {
return echo.NewHTTPError(http.StatusTooManyRequests, "rate limit exceeded")
}
return next.ServeHTTP(c)
}
newLimiter := rate.NewLimiter(r, burst)
limiterMap.Store(ip, newLimiter)
if !newLimiter.Allow() {
return echo.NewHTTPError(http.StatusTooManyRequests, "rate limit exceeded")
}
return next.ServeHTTP(c)
})
}
}为什么不能直接复用 echo.RateLimiter 的存储后端?
因为它的 Store 接口(如 memory.NewStore)只提供 Get/Set 计数器,不支持“取令牌”“重置冷却时间”“查询剩余令牌数”等令牌桶语义。底层结构是 map[key]struct{ Count int; ExpiresAt time.Time },和令牌桶需要的 lastTick、tokens、limit 三个状态完全不兼容。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
强行魔改 Store 会导致逻辑错乱:比如多次 Set 覆盖时间戳,丢失令牌累积逻辑;或者并发下 Get+Set 非原子,出现超发。
生产环境必须考虑的三个细节
本地内存 map 方案(如上面示例)只适合单机部署。集群下必须换存储:
- Redis + Lua 脚本实现原子令牌桶(推荐使用
github.com/bsm/redislock或手写 EVAL),注意网络延迟会影响精度 - 避免用
time.Now()做桶时间基准——不同机器时钟漂移会导致令牌生成不均,应以 Redis 服务端时间(TIME命令)或单调时钟(runtime.nanotime())对齐 - 客户端未收到响应就断连时,令牌其实已扣除但请求没执行,这是令牌桶固有缺陷;可通过前置校验(如
AllowN(now, 1))+ 幂等 key 减少影响
令牌桶真正的复杂点不在实现,而在于 burst 和 rate 的业务意义是否对齐:比如“每分钟最多 60 次,允许瞬间打满”和“匀速每秒 1 次,最多积压 30 秒”是两种完全不同的体验,选错就等于限流策略失效。

















