每一个超时都是一个被人遗忘的产品决策
一个没有期限的调用,是一个永远等下去的承诺。总会有人决定你的用户要等多久 —— 库的作者、内核,或者你。

每一个超时都是一个产品决策 🕰️
大多数代码库里的大多数网络调用都没有显式期限。这不意味着没有期限 —— 这意味着期限是由某个库的默认值、或者操作系统的连接超时决定的,而那些数字在被选定时,对你的用户、你的界面、以及他们在等什么一无所知。
默认值经常在一分钟或更久这个量级上。世上没有任何界面应该让人等一分钟才看到按钮有反应,所以实际上,每一个没有期限的调用都是一段没有人设计过的用户体验。
我们撞上的最清楚的一例
我们曾经有个移动端界面,登录之后就像坏了一样。什么都很慢 —— 不是网络慢,是 app 慢。原因是一个在几乎所有事情之前运行的路径上做了一次令牌刷新,没有超时。网络好的时候它是毫秒级。网络降级时,身份提供方的端点慢或不可达,它就等。一直等。用户看到的是一个干脆停住的应用。
修法不是让刷新变快,而是决定这件事值得等多久,并定义超过之后会发生什么 —— 这里是八秒上限,外加回落到缓存的凭据。正确答案一直唾手可得,只是代码从来没问过这个问题。
这就是通用形状。bug 不是"慢"。bug 是"关于慢的决策不存在"。
怎么挑这个数字
从界面往回推,而不是从依赖往前推。
用户正在看什么? 一个随输入触发的建议,预算是几百毫秒 —— 超过了就算到了也没用。一次界面切换有一两秒。一个明确的"生成"动作可以留住注意力更久,前提是界面展示进度。后台同步爱多久多久,因为没人在等。
期限过了会发生什么? 这是被跳过的那一半。一个没有兜底的超时,把"慢"转换成了"错误",而后者往往更糟。有用的问题是:没有这个依赖你能提供什么 —— 一个缓存值、一个局部视图、一次排队重试外加一句诚实的"我们会在后台完成"。
总预算加得起来吗? 如果一个请求调三个服务、各给三秒超时,你的最坏情况是九秒,再加上客户端允许的时间。预算是会累加的;各自独立写下的超时,加起来从来不是任何人会批准的那个数。
期限传播才是让它成真的那部分
单次调用的超时是不够的。你要的是一个随请求一起旅行的期限:入口处确立"这件事必须在 T 之前完成",而每一个下游调用拿到的是剩余时间,而不是一份崭新的完整额度。
没有传播,调用方放弃之后工作仍在继续。用户已经拿到报错;你的数据库还在为一个没人会读的响应执行查询。在高负载下这恰好是反的 —— 系统在最受限的时刻做着最无用的工作。
大多数现代平台都有表达这件事的一等方式:带期限的 context、取消信号、请求级的 abort。把它当成真实约束来用,而不是走个形式 —— 并且要确认取消真的到达了那个昂贵的部分,而不只是它外面那层包装。
值得采纳的规则
每一个出站调用都有显式期限,每一个期限都有定义好的兜底。
在能强制的地方强制它:一个要求必须传超时参数的共享 HTTP 客户端、一条针对裸 fetch 或 connect 调用的 lint 规则、一个评审时必问的问题。习惯比具体数字更重要,因为数字一开始一定是错的、可以调 —— 而一个没有期限的调用不是一个需要调的数字,它是一个从未被做出的决策,被悄悄委托给了写默认值的那个人。