Node 后端实战 · Cloudflare Workers 限流总误伤?用内存固定窗口替代 KV 实战
Node 后端实战 · Cloudflare Workers 限流总误伤用内存固定窗口替代 KV 实战各位看官限流这件事放在单机或者常驻容器里基本就是个中间件的事装个express-rate-limit或者前端挂个 Nginxlimit_req完事。但我把这个系统搬上 Cloudflare Workers 之后发现「限流」这件小事在边缘架构下被彻底重写了——你以为自己在做「计数」其实是在跟「无状态」「最终一致性」「冷启动」这三个东西搏斗。这篇文章不讲概念只讲我在 Workers 上做限流时为什么最终放弃了 KV 中央计数改用 Worker 实例内存里的固定窗口计数以及这个选择背后真实的代价。代码都是生产里跑着的真东西。一、先说清楚边缘架构下为什么不能「中央计数」传统限流的核心是「一个可信的、唯一的计数器」所有请求打到它它累加、判断、放行。Redis 干这个最合适因为它是中心化的、强一致的。但 Workers 不是这么玩的你的代码不是跑在一台机器上而是跑在 Cloudflare 全球几百个数据中心的边缘节点上每个请求落在哪个节点、哪个实例官方叫 island你控制不了每个实例都是无状态、按需冷启动的请求结束实例可能被回收下次请求又是一个全新的实例唯一能让你跨实例共享状态的是 KV 或者 Durable Objects——但 KV 是最终一致性写入后别的节点可能几秒才看到Durable Objects 是单点强一致但有冷启动和额外成本。所以「在 Workers 上做精确全局限流」这件事从根上就不是免费的。三种方案的取舍我画一张表方案精确性额外延迟额外成本主要坑KV 中央计数差最终一致写入滞后导致计数偏旧每请求至少 1 次 KV 读/写KV 有写入次数配额高并发下先撞配额限流判断基于「旧值」要么放多了要么把正常用户误杀Durable Objects高单点强一致首次访问有冷启动~几百 ms按请求数计费贵复杂、要单独维护一个 DO 类小项目杀鸡用牛刀实例内存固定窗口不精确per-island非全局零纯内存零不占 KV/DO 配额同一客户端可能被多个实例各放行一次限流是「近似」的我的系统是一个多租户 SaaS限流的目的不是「精算到个位数」而是防刷、防失控、防单用户把实例打挂。在这种诉求下「per-island 的近似限流」完全够用而 KV 的写入配额和最终一致性反而是实打实的雷。所以我选了第三方案。二、核心实现固定窗口 globalThis 防丢失固定窗口是最朴素的算法把时间切成一段段的窗口比如 60 秒一个窗口内计数超过上限就拒窗口翻页就清零重数。它不完美窗口边界会有两倍突发但实现简单、内存友好对防刷足够。关键难点是Worker 实例会被回收普通模块级变量也会跟着没。所以计数器必须挂到globalThis上这样同一个 isolate实例内的多次请求复用同一份内存HMR 或多实例也不会把计数弄丢// middleware/ratelimit.tsimporttype{Context,MiddlewareHandler}fromhono;import{err}from/lib/errors;importtype{AppBindings}from/middleware/auth;/** 单窗口计数 */interfaceWindowCounter{count:number;// 当前窗口内已放行计数windowStart:number;// 当前窗口起点秒}// 跨请求持久化挂到 globalThis防止实例回收/HMR 丢失constgglobalThisasunknownas{__rateLimitStore?:Mapstring,WindowCounter};conststore:Mapstring,WindowCounterg.__rateLimitStore??newMap();g.__rateLimitStorestore;然后是限流工厂本身exportinterfaceRateLimitOptions{key:string|((c:ContextAppBindings)string);// 限流键按 IP / 用户动态生成limit:number;// 窗口内允许的最大请求数windowSec:number;// 窗口时长秒code?:string;// 超限错误码默认 RATE_LIMIT}exportconstrateLimit(opts:RateLimitOptions):MiddlewareHandlerAppBindingsasync(c,next){// dev 环境暂停限流本地联调避免刷新/轮询误伤自己if(c.env.ENVdev){awaitnext();return;}constkeytypeofopts.keyfunction?opts.key(c):opts.key;constnowSecMath.floor(Date.now()/1000);constwindowStartMath.floor(nowSec/opts.windowSec)*opts.windowSec;constcurstore.get(key);letcount:number;if(!cur||cur.windowStart!windowStart){// 没有记录或窗口已翻页 → 开新窗口计数从 1 起count1;store.set(key,{count,windowStart});}else{cur.count1;countcur.count;}maybeSweep(windowStart);if(countopts.limit)throwerr(opts.code??RATE_LIMIT,请求过于频繁请稍后再试);c.header(X-RateLimit-Limit,String(opts.limit));c.header(X-RateLimit-Remaining,String(Math.max(0,opts.limit-count)));awaitnext();};逻辑很简单算出现在属于哪个窗口没有就新建计数 1有就 1超了就抛RATE_LIMIT对应 HTTP 429。同时把X-RateLimit-Limit和X-RateLimit-Remaining写进响应头让前端知道「你还能发几条」。三、限流键与三档限额限流键决定了「按谁来限」。匿名端点登录、刷新 token按客户端 IP 限登录态接口按用户 ID 限——这样既防了「一个 IP 疯狂撞库」也防了「一个账号高频刷接口」// routes/auth.ts —— 登录与刷新按 IP 限rateLimit({key:(c)rl:login:${clientIp(c)},limit:RATE_LIMIT.LOGIN_PER_MIN_PER_IP,windowSec:60}),rateLimit({key:(c)rl:refresh:${clientIp(c)},limit:RATE_LIMIT.REFRESH_PER_MIN_PER_IP,windowSec:60}),// app.ts —— 通用 API按用户限未登录回落到 IPconstapiRateLimitrateLimit({key:(c){constuc.get(user);returnu?KvKeys.apiRate(u.id):rl:api:anon:${c.req.header(cf-connecting-ip)??unknown};},limit:RATE_LIMIT.API_PER_MIN_PER_USER,windowSec:60,});限额定义在constants.ts三档各司其职场景常量限额键目的登录LOGIN_PER_MIN_PER_IP10 次/分/IPrl:login:ip防撞库、防暴力破解刷新 tokenREFRESH_PER_MIN_PER_IP30 次/分/IPrl:refresh:iprefresh 比登录频繁放宽一档通用 APIAPI_PER_MIN_PER_USER120 次/分/用户rl:api:userId防单账号刷爆后端注意登录给得最紧10 次/分因为这是匿名端点最坏情况下攻击者可以拿它做撞库而正常用户一分钟登录十次已经是极端情况了不会误伤。四、dev 短路别在联调时把自己限死代码里第一件事是判断c.env.ENV dev就直接放行。这个不是偷懒——本地联调时前端可能一秒发好几个请求、HMR 疯狂刷新、轮询接口反复跑如果限流开着最先被挡的就是开发自己。线上靠环境变量区分dev 直接短路跳过省心。五、内存不是无限的防 Map 膨胀挂globalThis的内存 Map如果只进不出长期运行下去会越攒越大——尤其按 IP 限流时每个陌生 IP 都会占一个 key。所以加了一个清理机制constMAX_ENTRIES5000;// 仅当超过阈值时才全量扫描清掉「不在当前窗口」的旧 keyconstmaybeSweep(windowStart:number):void{if(store.sizeMAX_ENTRIES)return;for(const[k,v]ofstore){if(v.windowStart!windowStart)store.delete(k);}};两个设计点惰性清理只有当 Map 超过 5000 条才扫一遍平时零开销。对防刷场景绝大多数 key 活不过一个窗口60 秒自然会被下一次扫到清掉。全量扫描而非精准删除扫描成本 O(n)但只在阈值触发时跑一次且 n 上限被 MAX_ENTRIES 兜住不会无限增长。这是「用偶尔的一次 O(n) 换平时零成本」的取舍。六、per-island 的代价不精确是代价也是取舍这是内存方案最该讲清楚的地方。因为计数只活在「当前这个实例」的内存里不同边缘节点的实例各有各的计数器所以一个客户端如果请求被调度到 3 个不同实例理论上最多能被放行limit × 3次。这不是 bug是我主动接受的取舍。原因有三防刷、防失控只需要「量级正确」不需要「精确拦截第 N1 次」。攻击者想绕过得同时打穿多个实例且每个都卡在临界点成本远高于收益如果真要全局精确得上 Durable Objects 或者中心化计数带来的是延迟和额外成本对本系统不划算真正高价值的「精确防护」比如防撞库我是叠加在别处的登录除了 IP 限流还有账号级失败锁定失败 N 次锁账号见下文补充。所以限流方案的选择本质是「你要的是精确还是够用」。我的判断是边缘 API 的通用限流要够用账号安全的精确防护另走专用逻辑。七、取客户端 IP 的坑按 IP 限流第一步就是把「客户端真实 IP」取对。Cloudflare 边缘会注入cf-connecting-ip这是最可信的来源但万一没有比如本地或某些代理链回退到x-forwarded-for的第一段exportconstclientIp(c:ContextAppBindings):stringc.req.header(cf-connecting-ip)??c.req.header(x-forwarded-for)??unknown;这里有个隐性风险x-forwarded-for是客户端可以伪造的请求头。所以我把它作为「回退」而非「首选」——首选永远是 Cloudflare 自己填的cf-connecting-ip。如果只信x-forwarded-for攻击者随便改个头就能绕过 IP 限流。真实部署里因为请求一定经过 Cloudflare 边缘cf-connecting-ip几乎总是存在回退分支更多是兜底本地调试。八、把剩余额度告诉前端最后一行细节X-RateLimit-Limit和X-RateLimit-Remaining这两个响应头。它们不是装饰——前端拿到Remaining就能在用户快触顶时提前提示「操作太频繁稍后再试」而不是等返回 429 才一脸懵。符合 RFC 6585 的惯例很多前端限流库也认这两个头。小结限流的边界在边缘架构下做限流我最终的结论是不要用「中央存储」去追求一个虚假的精确而是用「实例内存的固定窗口」换零延迟和零配额消耗并接受它是 per-island 的近似。配合账号级失败锁定补上安全短板整体既防得住又不误伤正常用户。如果你的场景对精确性要求极高比如按量计费 API、要严格封顶那 Durable Objects 或者自建中心化计数才是正解——只是那时你要准备好为延迟和成本买单。选型之前先想清楚你到底要「精确」还是要「够用」。相关阅读Serverless 导出 CSV 总超时用 Queue R2 异步任务彻底解决Node 后端实战 · 多租户 SaaS 的数据隔离Node 后端实战 · JWT 双密钥轮转与 token 版本号Node 后端实战 · D1 那些坑Node 后端实战 · 架构决策全景Koa 实现 JWT 会话与鉴权前后端分离项目通用方案RSA 非对称加密在 Node 中的应用实战MySQL 生产环境备份与恢复完整方案Ubuntu 下 Nginx 反向代理与 HTTPS 配置实战本文由 FungLeo 主导Deepseek 优化校阅转发请注明首发地址谢谢大家

相关新闻

深度学习精度选择:FP32与BF16的权衡及混合精度训练实践

深度学习精度选择:FP32与BF16的权衡及混合精度训练实践

1. 从FP32到BF16:一次关于精度的权衡与选择 最近在社区里看到不少朋友在讨论模型训练和推理时,对FP32、BF16这些数据类型的选择感到困惑。尤其是在一些开源项目或者框架的配置文件中,常常需要手动指定 dtype ,面对 fp32 、 b…

2026/9/26 21:58:49 阅读更多 →
摆脱“网易云专属“限制:ncmdumpGUI 图形界面几步完成 NCM 音乐解密

摆脱“网易云专属“限制:ncmdumpGUI 图形界面几步完成 NCM 音乐解密

摆脱"网易云专属"限制:ncmdumpGUI 图形界面几步完成 NCM 音乐解密 【免费下载链接】ncmdumpGUI C#版本网易云音乐ncm文件格式转换,Windows图形界面版本 项目地址: https://gitcode.com/gh_mirrors/nc/ncmdumpGUI ncmdumpGUI 是一款面向…

2026/9/29 19:29:33 阅读更多 →
2022 年顶级机器学习算法和 Python 库

2022 年顶级机器学习算法和 Python 库

新的算法产生起来非常困难, 2022年搞不好也不会是另外的情况。可是呢, 依旧存在一些机器学习算法以及库在往后会更受大家青睐。这些算法之所以显得不一样, 是由于它们具备一些在其他算法当中并非普遍存在的优点, 我会在这篇文章里对这些优点展开详细的探讨。不管可不可以在你的…

2026/9/23 21:50:10 阅读更多 →

最新新闻

ICEEMDAN信号分解实战:非平稳强噪声下的模态分离与Python工程实现

ICEEMDAN信号分解实战:非平稳强噪声下的模态分离与Python工程实现

简介:本资源是一份面向科研人员、工程师及Python开发者的时间序列信号处理实战项目,聚焦ICEEMDAN算法的工程化改进与GUI落地应用,解决传统EEMD/CEEMDAN在噪声鲁棒性、模态混叠抑制和大规模信号自适应分解中的关键瓶颈。包内含1个73KB的完整do…

2026/10/1 15:56:29 阅读更多 →
派尼瑞普2026年臭氧设备厂家:专注臭氧发生器及配套产品研发制造

派尼瑞普2026年臭氧设备厂家:专注臭氧发生器及配套产品研发制造

派尼瑞普在2026年作为臭氧设备厂家,专注臭氧发生器及配套产品的研发制造,青岛派尼尔环保技术有限公司可提供相关设备与配套服务。臭氧设备与臭氧发生器简介 青岛派尼尔环保技术有限公司在臭氧设备领域开展业务,主要方向包括 臭氧发生器 以及后…

2026/10/1 15:56:29 阅读更多 →
物联网网络交换机:2026-2032年预计实现7.8%年复合增长,产业发展稳步推进

物联网网络交换机:2026-2032年预计实现7.8%年复合增长,产业发展稳步推进

从核心市场规模来看,2025年全球物联网网络交换机市场销售额已经达到13.27亿美元,预计2032年将突破22.31亿美元,2026-2032年复合年增长率稳定维持在7.8%,在整体企业网交换机大盘增速放缓的背景下,这条细分赛道的稳健增长…

2026/10/1 15:56:29 阅读更多 →
Step-5-Preview:统一资源调度器实现三域协同开发

Step-5-Preview:统一资源调度器实现三域协同开发

1. 项目概述:这不是一个“预览”,而是一次跨域计算能力的压力测试“Step-5-Preview”这个命名本身就带着一股实验室里刚拆封的硬件味儿——它不叫“Beta”、不叫“RC”,也不叫“Developer Build”,偏偏选了“Preview”&#xff0c…

2026/10/1 15:56:29 阅读更多 →
金蝶 s-HR 二次开发避坑:元数据、扩展字段与接口对接

金蝶 s-HR 二次开发避坑:元数据、扩展字段与接口对接

接手金蝶 s-HR 二次开发的人,大多有同一个错觉:不就是个表单系统,拖拖拽拽配一配就完事了。真上手才发现,加一个字段要动四五个地方,改一段审批流程跑出来的结果跟预想的完全不一样,接口明明返回成功但两边…

2026/10/1 15:56:29 阅读更多 →
relsent-bert-zh-large(知更鸟)模型跨领域应用

relsent-bert-zh-large(知更鸟)模型跨领域应用

核心特点:同时输出【对话双方关系类型 情感极性 关系演变趋势】,区别于普通情感分析只判断正负,主打人际交互文本的多维理解,下面是可落地的其他领域场景:1. 智能客服 / 政企热线识别客户与客服之间:客户…

2026/10/1 15:55:29 阅读更多 →

日新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 0:00:30 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 0:00:30 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 1:01:17 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/30 13:14:22 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/30 18:13:06 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/9/30 13:14:49 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 0:00:30 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 0:00:30 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 1:01:17 阅读更多 →