<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nghề không nhiều chẳng sắc</title>
    <description>The latest articles on DEV Community by Nghề không nhiều chẳng sắc (@nguoidungai).</description>
    <link>https://dev.to/nguoidungai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F715555%2F139174cf-bfbd-44c8-afd4-6650f11a6b88.png</url>
      <title>DEV Community: Nghề không nhiều chẳng sắc</title>
      <link>https://dev.to/nguoidungai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nguoidungai"/>
    <language>en</language>
    <item>
      <title>Logging — Request Logging</title>
      <dc:creator>Nghề không nhiều chẳng sắc</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:40:55 +0000</pubDate>
      <link>https://dev.to/nguoidungai/logging-request-logging-2080</link>
      <guid>https://dev.to/nguoidungai/logging-request-logging-2080</guid>
      <description>&lt;h1&gt;
  
  
  Request logging: vì sao mỗi log line phải có request id, và structured log cứu debug thế nào khi có sự cố production
&lt;/h1&gt;

&lt;p&gt;Request logging không phải là in &lt;code&gt;console.log('got a request')&lt;/code&gt; cho vui. Nó là dấu vết duy nhất còn lại khi một request đã đi qua service, đi qua vài downstream, gặp lỗi, và bị đóng socket. Nếu log không có gì để nối lại các dòng thuộc &lt;em&gt;cùng một&lt;/em&gt; request — không có &lt;code&gt;requestId&lt;/code&gt;, không có &lt;code&gt;traceId&lt;/code&gt; — thì log của một service RPS trung bình biến thành một mớ text xen kẽ giữa hàng chục request đồng thời, và câu hỏi "request X đã đi tới đâu, fail ở service nào" trở thành không trả lời được. Structured log (mỗi dòng là một JSON object với field cố định) + một correlation id truyền qua toàn bộ pipeline chính là cái làm log có thể query được, thay vì grep mù. Hai framework Express và Fastify tiếp cận khác nhau: Fastify tích hợp &lt;code&gt;pino&lt;/code&gt; sẵn và tự sinh &lt;code&gt;req.id&lt;/code&gt;, Express phải tự dán qua &lt;code&gt;pino-http&lt;/code&gt; hoặc middleware tay. Nhưng cả hai đều vỡ theo cùng một kiểu khi correlation id bị đứt giữa các service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cơ chế hoạt động
&lt;/h2&gt;

&lt;p&gt;Ba miếng ghép: (1) một structured logger — thường là &lt;code&gt;pino&lt;/code&gt; trong ecosystem Node vì nó ghi NDJSON và có async destination, (2) một cơ chế sinh/nhận &lt;code&gt;requestId&lt;/code&gt;, (3) một cách propagate id đó qua async work và qua HTTP call sang service khác.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pino&lt;/code&gt; là JSON logger tối giản: mỗi lời gọi &lt;code&gt;logger.info({...}, 'message')&lt;/code&gt; xuất một dòng NDJSON ra một &lt;code&gt;WritableStream&lt;/code&gt; (mặc định là stdout). Log level là số (&lt;code&gt;trace=10, debug=20, info=30, warn=40, error=50, fatal=60&lt;/code&gt;, theo pino docs), và có child logger — &lt;code&gt;logger.child({ reqId })&lt;/code&gt; tạo một logger mới bind sẵn các field, mọi log line từ child đều có &lt;code&gt;reqId&lt;/code&gt; mà không phải truyền tay.&lt;/p&gt;

&lt;p&gt;Với &lt;strong&gt;Fastify&lt;/strong&gt;, chỉ cần &lt;code&gt;logger: true&lt;/code&gt; là có pino và request logging tự động — Fastify sinh &lt;code&gt;request.id&lt;/code&gt; (mặc định là monotonic counter, đổi được qua option &lt;code&gt;genReqId&lt;/code&gt;) và log một cặp dòng "incoming request" / "request completed" cho mỗi request. Trong handler, &lt;code&gt;request.log&lt;/code&gt; là child logger đã bind &lt;code&gt;reqId&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Fastify&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fastify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Fastify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LOG_LEVEL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;redact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;req.headers.authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;req.headers.cookie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*.password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// Ưu tiên id từ upstream nếu có, không thì sinh mới&lt;/span&gt;
  &lt;span class="na"&gt;genReqId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;traceparent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
    &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;requestIdHeader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;requestIdLogLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reqId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;looking up order&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Với &lt;strong&gt;Express&lt;/strong&gt;, không có logger tích hợp — dùng &lt;code&gt;pino-http&lt;/code&gt;, một Express-compatible middleware do cùng maintainer của pino ship. &lt;code&gt;pino-http&lt;/code&gt; gắn &lt;code&gt;req.log&lt;/code&gt; và &lt;code&gt;req.id&lt;/code&gt;, tự log khi &lt;code&gt;res.on('finish')&lt;/code&gt; với &lt;code&gt;responseTime&lt;/code&gt;, &lt;code&gt;statusCode&lt;/code&gt;, &lt;code&gt;req&lt;/code&gt;, &lt;code&gt;res&lt;/code&gt; đã được serialize gọn qua &lt;code&gt;pino.stdSerializers&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pinoHttp&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pino-http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pinoHttp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LOG_LEVEL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;genReqId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;customLogLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;redact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;req.headers.authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;req.headers.cookie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;looking up order&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Miếng thứ ba — &lt;strong&gt;propagate id qua async work và qua HTTP call downstream&lt;/strong&gt; — là chỗ hầu hết implementation vỡ. Node core có &lt;code&gt;AsyncLocalStorage&lt;/code&gt; (module &lt;code&gt;node:async_hooks&lt;/code&gt;) giữ context xuyên qua callback/promise mà không phải truyền tham số:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AsyncLocalStorage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:async_hooks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AsyncLocalStorage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onRequest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_reply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;reqId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Ở tầng HTTP client — tự động gắn header khi gọi downstream&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchDownstream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;reqId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reqId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cách "chuẩn" hơn là dùng W3C Trace Context (header &lt;code&gt;traceparent&lt;/code&gt;, format &lt;code&gt;00-&amp;lt;trace-id&amp;gt;-&amp;lt;span-id&amp;gt;-&amp;lt;flags&amp;gt;&lt;/code&gt;, theo W3C Trace Context spec). OpenTelemetry SDK cho Node tự inject &lt;code&gt;traceparent&lt;/code&gt; vào outgoing HTTP call và extract vào incoming request; log có thể &lt;code&gt;bindings&lt;/code&gt; thêm &lt;code&gt;trace_id&lt;/code&gt;/&lt;code&gt;span_id&lt;/code&gt; để mọi dòng log tự động join được với trace. Đây là mặc định nếu service đã có OpenTelemetry — không phải viết tay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vấn đề gặp trong production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 1: không có correlation id — không thể truy vết request qua service.&lt;/strong&gt; Kịch bản: user báo checkout order &lt;code&gt;#42871&lt;/code&gt; fail. Log của service &lt;code&gt;orders&lt;/code&gt; có dòng &lt;code&gt;ERROR upstream timeout&lt;/code&gt;, log của service &lt;code&gt;payments&lt;/code&gt; có dòng &lt;code&gt;ERROR db connection lost&lt;/code&gt; cách đó 300ms. Có phải hai lỗi này cùng thuộc &lt;em&gt;một&lt;/em&gt; request của user không? Không biết. Không có id chung để nối lại. Grep theo &lt;code&gt;orderId&lt;/code&gt; cũng chỉ tìm được ở service &lt;code&gt;orders&lt;/code&gt; — service &lt;code&gt;payments&lt;/code&gt; không biết &lt;code&gt;orderId&lt;/code&gt; là gì, nó chỉ nhận &lt;code&gt;paymentIntentId&lt;/code&gt;. Nếu ngay từ đầu mọi service log kèm cùng một &lt;code&gt;x-request-id&lt;/code&gt; (hoặc &lt;code&gt;traceparent&lt;/code&gt;), query log aggregator theo &lt;code&gt;reqId=abc123&lt;/code&gt; ra đúng chuỗi dòng của một request qua mọi hop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SAI: log không có id gì để nối các dòng&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/checkout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout started&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://payments/charge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* không gắn header id */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ĐÚNG: sinh/nhận id ở edge, gắn vào log, propagate xuống downstream&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/checkout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout started&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://payments/charge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure mode 2: unstructured log — không query được.&lt;/strong&gt; &lt;code&gt;console.log('user ' + userId + ' bought ' + qty)&lt;/code&gt; trông đọc được nhưng không parse ngược lại thành field. Log aggregator (Elastic, Loki, Datadog) index theo field JSON; nếu log là plain string, chỉ còn full-text search. "Đếm số request lỗi 500 theo route trong 1h qua" biến thành parse regex trên free text, và grep không phân biệt được &lt;code&gt;error&lt;/code&gt; trong error message với &lt;code&gt;error&lt;/code&gt; trong stack trace. Structured log giải quyết bằng cách mỗi log line là một JSON có schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SAI: string concat, không có level, không có id, mất field&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; bought &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// ĐÚNG: object + message, có level, có reqId (qua child logger)&lt;/span&gt;
&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;durationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;purchase completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// -&amp;gt; {"level":30,"time":...,"reqId":"...","userId":"u1","qty":2,"sku":"...","durationMs":47,"msg":"purchase completed"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure mode 3: log PII/secret ra file — leak dữ liệu.&lt;/strong&gt; Log request body/headers thô sẽ ghi cả &lt;code&gt;Authorization: Bearer ...&lt;/code&gt;, &lt;code&gt;Cookie: session=...&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;, số thẻ. Log thường được ship đi khắp nơi (Kafka, S3, log aggregator, backup); một khi PII lọt vào chain đó, khó thu hồi. &lt;code&gt;pino&lt;/code&gt; có option &lt;code&gt;redact&lt;/code&gt; (theo pino docs), nhận array path để thay bằng &lt;code&gt;[Redacted]&lt;/code&gt; trước khi ghi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SAI: log toàn bộ req sẽ chứa Authorization/Cookie header&lt;/span&gt;
&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;incoming&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// ĐÚNG: redact path nhạy cảm ở tầng logger, không dựa vào code call site&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pino&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;redact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;req.headers.authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;req.headers.cookie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*.password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*.creditCard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;censor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[Redacted]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure mode 4: sync log chặn event loop.&lt;/strong&gt; Ghi log đồng bộ (&lt;code&gt;fs.writeSync&lt;/code&gt;, hay &lt;code&gt;console.log&lt;/code&gt; khi stdout là file/pipe blocking) chặn thread cho tới khi kernel nhận xong. Ở RPS cao, mỗi request đóng góp vài log line, tổng độ trễ log ăn thẳng vào p99. &lt;code&gt;pino&lt;/code&gt; mặc định ghi async qua &lt;code&gt;sonic-boom&lt;/code&gt;, nhưng nếu dựng &lt;code&gt;pino.destination({ sync: true })&lt;/code&gt; (hoặc dùng transport sai cách), toàn bộ log biến thành blocking. Với high-throughput service, để pino ở chế độ async mặc định, hoặc dùng &lt;code&gt;pino.transport()&lt;/code&gt; chạy trên worker thread riêng.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 5: correlation id trust boundary.&lt;/strong&gt; Nhận &lt;code&gt;x-request-id&lt;/code&gt; từ client public internet mà không validate: attacker có thể inject id trùng để làm nhiễu log, hoặc chèn ký tự lạ (newline) để log injection. Rule: ở edge (API gateway hoặc service ngoài cùng), &lt;em&gt;sinh&lt;/em&gt; id mới; chỉ &lt;em&gt;trust&lt;/em&gt; header id từ upstream nội bộ đã xác thực (service mesh, LB) — hoặc validate format (UUID/ULID) trước khi dùng.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cách debug và monitor
&lt;/h2&gt;

&lt;p&gt;Triệu chứng cần đọc log tốt: user báo "request fail lúc 14:23", user chỉ có thể nhớ thời điểm và một phần URL; hoặc dashboard alert &lt;code&gt;5xx rate spike&lt;/code&gt; nhưng không biết request nào là root cause. Cách tiếp cận:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query theo &lt;code&gt;reqId&lt;/code&gt; trong log aggregator.&lt;/strong&gt; Trả &lt;code&gt;x-request-id&lt;/code&gt; ra response header và hiển thị nó ở error page cho user, để user copy-paste id vào bug report. Ở Elastic/Loki/Datadog, query &lt;code&gt;reqId:"abc-123"&lt;/code&gt; trả về toàn bộ log line của request đó — qua mọi service, đã sort theo timestamp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Join log với trace.&lt;/strong&gt; Nếu đã bật OpenTelemetry, log line có &lt;code&gt;trace_id&lt;/code&gt;/&lt;code&gt;span_id&lt;/code&gt;, ngược lại trace có &lt;code&gt;attributes.http.request.id&lt;/code&gt;. Bấm từ dòng log sang trace UI (Grafana Tempo, Jaeger, Honeycomb, Datadog APM) thấy chính xác downstream call nào chậm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metric từ log.&lt;/strong&gt; Đếm log level &lt;code&gt;error&lt;/code&gt;/&lt;code&gt;warn&lt;/code&gt; theo route và emit thành metric (Loki LogQL query theo level, hoặc convert log → metric qua promtail/vector). Alert khi error rate vượt baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log volume monitoring.&lt;/strong&gt; Log volume là chi phí thật (ingest, storage, egress). Theo dõi bytes/sec log mỗi service; một spike volume thường có nghĩa hoặc là stack trace loop (một error đang loop nhanh), hoặc là ai đó bật debug log ở production quên tắt. Đặt sample cho log &lt;code&gt;info&lt;/code&gt; khi RPS quá cao (&lt;code&gt;level: 'warn'&lt;/code&gt; ở prod, hoặc chỉ log 1/N request thành công).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health của logging pipeline.&lt;/strong&gt; Nếu log destination (Fluent Bit, Vector, Filebeat) đứng, log vẫn ghi ra stdout nhưng không được ship — service vẫn chạy nhưng "mù". Monitor pipeline lag riêng, không phải chỉ log volume ở đích.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule phòng ngừa cứng: (1) mọi service &lt;em&gt;bắt buộc&lt;/em&gt; có &lt;code&gt;requestId&lt;/code&gt;/&lt;code&gt;traceId&lt;/code&gt; trong mọi log line — enforce qua template logger, không phải dựa vào từng call site nhớ; (2) không dùng &lt;code&gt;console.log&lt;/code&gt; trong code production — lint rule cấm (&lt;code&gt;no-console&lt;/code&gt; của ESLint); (3) redact tập trung ở logger config, không phải ở call site; (4) trả &lt;code&gt;x-request-id&lt;/code&gt; ra response để user báo bug có id; (5) &lt;code&gt;LOG_LEVEL&lt;/code&gt; phải configurable qua env — thay đổi mà không phải redeploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoff
&lt;/h2&gt;

&lt;p&gt;Structured log dễ query, dễ join với trace, dễ redact — nhưng tăng volume so với plain text. Một dòng &lt;code&gt;"user 1 bought 2"&lt;/code&gt; (~18 bytes) biến thành một JSON object với &lt;code&gt;level&lt;/code&gt;, &lt;code&gt;time&lt;/code&gt;, &lt;code&gt;reqId&lt;/code&gt;, &lt;code&gt;userId&lt;/code&gt;, &lt;code&gt;qty&lt;/code&gt;, &lt;code&gt;msg&lt;/code&gt; (~100+ bytes). Ở service RPS cao, chênh lệch này nhân lên thành GB/ngày và ăn ngân sách log ingest thật (Datadog/Splunk tính tiền theo GB). Bù lại: query time giảm mạnh vì aggregator index được field, không phải full-text scan; và cost của một sự cố production dài hơn vì grep mù thường lớn hơn cost log volume nhiều lần.&lt;/p&gt;

&lt;p&gt;Correlation id truyền qua header có cost: mỗi service phải nhớ đọc + gắn lại khi gọi downstream — quên một chỗ là đứt chuỗi. Automation qua OpenTelemetry instrumentation hoặc middleware chung giải quyết, nhưng thêm dependency và một tầng abstraction. Tự làm bằng &lt;code&gt;AsyncLocalStorage&lt;/code&gt; nhẹ hơn nhưng phải wrap tất cả outgoing HTTP client.&lt;/p&gt;

&lt;p&gt;Async log giảm latency trên hot path nhưng có risk mất log cuối cùng khi process crash — buffer chưa flush. &lt;code&gt;pino&lt;/code&gt; có &lt;code&gt;pino.final()&lt;/code&gt; (theo pino docs) để flush trong &lt;code&gt;uncaughtException&lt;/code&gt;/&lt;code&gt;SIGTERM&lt;/code&gt; handler. Sync log an toàn hơn nhưng chặn event loop; chỉ dùng ở CLI/script ngắn, không dùng ở server RPS cao.&lt;/p&gt;

&lt;p&gt;Rule of thumb: mặc định structured JSON log + async destination + một correlation id (ưu tiên W3C &lt;code&gt;traceparent&lt;/code&gt; nếu có OpenTelemetry, không thì &lt;code&gt;x-request-id&lt;/code&gt;) sinh ở edge và propagate xuyên suốt; &lt;code&gt;info&lt;/code&gt; level ở prod với sample nếu cần, &lt;code&gt;debug&lt;/code&gt; chỉ bật tạm qua env; redact ở logger config; trả id ra response header. Đừng tối ưu volume trước khi có sự cố đầu tiên buộc phải grep — bài học từ postmortem là "wish we had reqId" nhiều hơn "wish we logged less".&lt;/p&gt;

&lt;h2&gt;
  
  
  Câu hỏi phỏng vấn
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Vì sao cần request id trong log, và cụ thể propagate nó qua các service như thế nào?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Vì một service RPS trung bình có nhiều request đồng thời, log line từ các request khác nhau xen kẽ nhau trong file/stream. Không có một field chung để nối các dòng thuộc &lt;em&gt;cùng một&lt;/em&gt; request, thì "request X đã đi qua đâu, fail ở step nào" không trả lời được — chỉ còn grep mù. Cụ thể: ở edge service (service ngoài cùng nhận request từ client), sinh một &lt;code&gt;requestId&lt;/code&gt; (UUID/ULID) hoặc lấy từ header &lt;code&gt;x-request-id&lt;/code&gt; nếu upstream đã có; bind vào child logger (&lt;code&gt;logger.child({ reqId })&lt;/code&gt; với pino, hoặc &lt;code&gt;request.log&lt;/code&gt; sẵn có với Fastify) để mọi log line trong scope request đều mang id. Khi gọi downstream service, gắn id vào request header (&lt;code&gt;x-request-id&lt;/code&gt;, hoặc chuẩn hơn là W3C &lt;code&gt;traceparent&lt;/code&gt;); downstream đọc header đó, dùng lại làm reqId của nó — chuỗi id không đứt qua network hop. Xuyên qua async work trong cùng process, dùng &lt;code&gt;AsyncLocalStorage&lt;/code&gt; (Node core) để giữ context, không phải truyền reqId qua từng argument. Điểm ăn điểm là gọi tên hai hậu quả production: (a) không có correlation id thì incident postmortem chỉ join được manually theo timestamp — cực kỳ chậm và dễ sai khi có concurrent request; (b) nhận id từ public internet không validate là log injection vector — id phải sinh mới ở edge, chỉ trust từ upstream nội bộ; và một hậu quả về monitoring — log volume tăng vì mỗi line có thêm field, nhưng đây là chi phí phải trả để log query được thay vì phải grep mù. Bonus: trả &lt;code&gt;x-request-id&lt;/code&gt; ra response header để user báo bug kèm id, khỏi phải hỏi "khoảng lúc mấy giờ".&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on
&lt;/h2&gt;

&lt;p&gt;Dựng một chain hai service Fastify để tái hiện scenario "log không có correlation id" rồi sửa, rồi query theo id.&lt;/p&gt;

&lt;p&gt;Chuẩn bị:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;req-logging &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;req-logging
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm pkg &lt;span class="nb"&gt;set type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;module
npm i fastify pino pino-pretty
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo &lt;code&gt;service-a.mjs&lt;/code&gt; (edge, gọi service-b):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Fastify&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fastify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Fastify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;redact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;req.headers.authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;req.headers.cookie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;genReqId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;requestIdHeader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;requestIdLogLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reqId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/checkout/:orderId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout starting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Gắn requestId vào call downstream — đây là điểm dễ quên&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3102/charge/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;charge failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;downstream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.0.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo &lt;code&gt;service-b.mjs&lt;/code&gt; (downstream, fail ngẫu nhiên):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Fastify&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fastify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Fastify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;genReqId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;requestIdHeader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;requestIdLogLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reqId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/charge/:orderId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;charge received&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// Simulate downstream latency + ~30% failure&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;db connection lost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;db_down&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;charged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.0.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chạy song song, ghi log ra file để grep sau:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node service-a.mjs 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /tmp/a.log &amp;amp;
node service-b.mjs 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /tmp/b.log &amp;amp;

&lt;span class="c"&gt;# Bắn 50 request, mỗi request là một orderId khác&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 50&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:3101/checkout/order-&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &amp;amp;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chọn một request đã fail (theo response &lt;code&gt;{"ok":false,...}&lt;/code&gt;) — ví dụ &lt;code&gt;requestId=abc-def-...&lt;/code&gt;. Query log của &lt;em&gt;cả hai&lt;/em&gt; service theo id đó:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'"reqId":"abc-def-..."'&lt;/span&gt; /tmp/a.log /tmp/b.log | jq &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;'sort_by(.time)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sẽ ra chuỗi log line từ service-a → service-b → error → response, theo đúng thứ tự thời gian, cho &lt;em&gt;đúng một request&lt;/em&gt;. Đây là điều không thể làm được nếu không truyền &lt;code&gt;x-request-id&lt;/code&gt; sang downstream.&lt;/p&gt;

&lt;p&gt;Sau đó, tái hiện failure mode 1 bằng cách xóa dòng &lt;code&gt;headers: { 'x-request-id': req.id }&lt;/code&gt; trong service-a. Chạy lại và thử query: service-b sinh id mới cho mỗi request, không match id của service-a — chain bị đứt. Grep theo &lt;code&gt;reqId&lt;/code&gt; chỉ ra dòng của service-a; grep theo &lt;code&gt;orderId&lt;/code&gt; may ra được nhưng chỉ khi cả hai service tình cờ log cùng field đó.&lt;/p&gt;

&lt;p&gt;Cuối cùng, thử log volume: bench và đo size log sinh ra:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Bench với autocannon để xem log volume và độ trễ&lt;/span&gt;
npx autocannon &lt;span class="nt"&gt;-c&lt;/span&gt; 20 &lt;span class="nt"&gt;-d&lt;/span&gt; 10 http://localhost:3101/checkout/bench
&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; /tmp/a.log /tmp/b.log   &lt;span class="c"&gt;# size log sinh ra trong 10s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bật &lt;code&gt;LOG_LEVEL=warn&lt;/code&gt; (sửa &lt;code&gt;level: process.env.LOG_LEVEL ?? 'info'&lt;/code&gt; trong config) và chạy lại — volume log giảm mạnh vì &lt;code&gt;info&lt;/code&gt; bị bỏ qua, chỉ còn &lt;code&gt;warn&lt;/code&gt;/&lt;code&gt;error&lt;/code&gt;. Đây là cần chỉnh khi service RPS cao và ngân sách log ingest bị vượt: giảm level ở hot path, giữ error log nguyên vẹn.&lt;/p&gt;

</description>
      <category>production</category>
      <category>logging</category>
      <category>requestlogging</category>
      <category>observability</category>
    </item>
    <item>
      <title>Async Error Handling — Async Route</title>
      <dc:creator>Nghề không nhiều chẳng sắc</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:40:51 +0000</pubDate>
      <link>https://dev.to/nguoidungai/async-error-handling-async-route-21b9</link>
      <guid>https://dev.to/nguoidungai/async-error-handling-async-route-21b9</guid>
      <description>&lt;h1&gt;
  
  
  Async route: vì sao Promise reject trong Express 4 không tới error middleware, và cách Fastify tự bắt lỗi
&lt;/h1&gt;

&lt;p&gt;Một &lt;code&gt;async&lt;/code&gt; route handler trông giống một handler bình thường, nhưng cơ chế truyền lỗi của nó khác hẳn. Express 4 bọc lời gọi handler bằng &lt;code&gt;try/catch&lt;/code&gt; đồng bộ; một hàm &lt;code&gt;async&lt;/code&gt; return về một Promise trước khi Promise đó settle, nên &lt;code&gt;try/catch&lt;/code&gt; đồng bộ không bắt được rejection. Kết quả: lỗi rơi ra khỏi handler dưới dạng &lt;code&gt;unhandledRejection&lt;/code&gt;, không tới error middleware, không được gửi thành &lt;code&gt;500 JSON&lt;/code&gt;, và từ Node 15 mặc định làm process crash. Fastify đi hướng khác — pipeline hook tự &lt;code&gt;await&lt;/code&gt; handler, rejection được framework bắt và chuyển tới &lt;code&gt;setErrorHandler&lt;/code&gt;. Hiểu điểm khác biệt này giải thích vì sao cùng một dòng code &lt;code&gt;await db.query(...)&lt;/code&gt; lại có hai hệ quả production hoàn toàn khác nhau giữa hai framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cơ chế hoạt động
&lt;/h2&gt;

&lt;p&gt;Trong Express 4, &lt;code&gt;Layer.handle_request&lt;/code&gt; gọi handler đồng bộ trong một khối &lt;code&gt;try/catch&lt;/code&gt;. Nếu handler ném exception đồng bộ, catch được, forward qua &lt;code&gt;next(err)&lt;/code&gt;. Nhưng handler &lt;code&gt;async&lt;/code&gt; không ném — nó &lt;em&gt;return&lt;/em&gt; về một Promise. Đến khi Promise reject, control đã ra khỏi khối &lt;code&gt;try/catch&lt;/code&gt; từ lâu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Đây là bản giản lược của cách Express 4 gọi handler:&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// với async handler, ret là Promise (đã pending)&lt;/span&gt;
  &lt;span class="c1"&gt;// handler đã "return" — try/catch không còn hiệu lực với reject xảy ra sau&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                              &lt;span class="c1"&gt;// chỉ chạy khi handler ném đồng bộ&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vì Express 4 không &lt;code&gt;await ret&lt;/code&gt; và cũng không &lt;code&gt;ret.catch(next)&lt;/code&gt;, một Promise reject không có bên nào bắt. Node emit &lt;code&gt;unhandledRejection&lt;/code&gt; ở tầng process. Cách sửa chuẩn là wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// asyncHandler: bọc handler async, forward reject vào next(err)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;asyncHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;asyncHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not_found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Express 5 (bản major stable phát hành 2024) sửa vấn đề này ở tầng framework: theo Express docs mục "Migrating to Express 5", route/middleware trả về Promise reject sẽ tự bị forward vào error chain, không cần wrapper nữa. Nhưng phần lớn service production vẫn đang ở Express 4, nên wrapper vẫn là kiến thức bắt buộc.&lt;/p&gt;

&lt;p&gt;Fastify không có vấn đề tương đương. Theo Fastify docs mục "Errors", pipeline hook &lt;code&gt;await&lt;/code&gt; từng bước; nếu handler &lt;code&gt;async&lt;/code&gt; reject (throw hay return rejected Promise đều được), Fastify bắt, dẫn qua hook &lt;code&gt;onError&lt;/code&gt; rồi gọi error handler (&lt;code&gt;setErrorHandler&lt;/code&gt; nếu có, mặc định trả JSON với statusCode):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setErrorHandler&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;request failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;internal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;httpErrors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="c1"&gt;// Fastify bắt, chuyển tới setErrorHandler&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Vấn đề gặp trong production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 1: async handler Express 4 không bọc — process crash.&lt;/strong&gt; Đây là failure mode phổ biến nhất khi migrate từ callback sang &lt;code&gt;async/await&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SAI: Express 4 không bắt Promise reject từ handler async&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// reject nếu DB down&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Khi &lt;code&gt;db.orders.findById&lt;/code&gt; reject (DB timeout, connection pool exhausted, syntax error), handler return một Promise reject. Client thấy request treo cho tới khi socket timeout. Ở tầng process, Node emit &lt;code&gt;unhandledRejection&lt;/code&gt;; từ Node 15 mặc định là &lt;code&gt;--unhandled-rejections=throw&lt;/code&gt; (theo Node docs &lt;code&gt;process&lt;/code&gt; event &lt;code&gt;unhandledRejection&lt;/code&gt;), nghĩa là process crash. Trên K8s, pod bị OOM/crash-restart; một lỗi DB làm rơi cả instance. Sửa bằng wrapper &lt;code&gt;asyncHandler&lt;/code&gt; (xem trên) hoặc dùng thư viện &lt;code&gt;express-async-errors&lt;/code&gt; (monkey-patch &lt;code&gt;Layer.handle_request&lt;/code&gt; để tự &lt;code&gt;await&lt;/code&gt;) — thêm vào &lt;code&gt;import 'express-async-errors'&lt;/code&gt; ở đầu app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 2: fire-and-forget bên trong handler.&lt;/strong&gt; Cả trong Express lẫn Fastify, nếu bên trong handler có một async call không được &lt;code&gt;await&lt;/code&gt;, framework không thấy Promise đó, không có bên nào bắt reject:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SAI: emit event log không await, reject rơi vào void&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;asyncHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;auditLog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c1"&gt;// async, không await, không .catch&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nếu &lt;code&gt;auditLog.write&lt;/code&gt; reject, không ai bắt — kể cả wrapper &lt;code&gt;asyncHandler&lt;/code&gt; cũng bó tay vì handler đã &lt;code&gt;return&lt;/code&gt; xong. Đây là lỗi bất kể framework. Sửa: &lt;code&gt;await&lt;/code&gt; nếu cần chờ, hoặc &lt;code&gt;.catch(err =&amp;gt; log.error({ err }, 'audit failed'))&lt;/code&gt; nếu cố tình fire-and-forget để không block response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 3: try/catch nuốt lỗi rồi vẫn &lt;code&gt;res.send&lt;/code&gt; bình thường.&lt;/strong&gt; Ngược với failure mode 1: bọc &lt;code&gt;try/catch&lt;/code&gt; quá rộng và không phản hồi đúng status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SAI: catch tất, nuốt lỗi, trả 200 với body rỗng&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;asyncHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt;   &lt;span class="c1"&gt;// nuốt lỗi, client thấy 200 OK payload rỗng — monitor mù&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alerts không kích hoạt (status 200), nhưng downstream nhận payload rỗng và vỡ. Quy tắc: nếu bắt exception ở tầng handler, hoặc &lt;code&gt;next(err)&lt;/code&gt; để error middleware xử lý thống nhất, hoặc &lt;code&gt;res.status(...)&lt;/code&gt; với status thật (không phải 200). Đừng ép về &lt;code&gt;200 {}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 4: middleware async không bọc — treo request không log.&lt;/strong&gt; Middleware async cũng bị đúng vấn đề như handler async trong Express 4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SAI: auth middleware async, verifyToken reject, không có bên bắt&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// reject nếu token invalid&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reject không tới error middleware, request treo, &lt;code&gt;unhandledRejection&lt;/code&gt; bắn. Sửa: bọc bằng cùng wrapper &lt;code&gt;asyncHandler&lt;/code&gt; áp dụng cho middleware, hoặc dùng &lt;code&gt;try/catch&lt;/code&gt; trong middleware và &lt;code&gt;next(err)&lt;/code&gt; thủ công.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 5: Fastify — setErrorHandler thiếu, response mặc định lộ chi tiết.&lt;/strong&gt; Fastify tự bắt lỗi async, nhưng nếu không đăng ký &lt;code&gt;setErrorHandler&lt;/code&gt; và error không phải instance của &lt;code&gt;httpErrors&lt;/code&gt;, mặc định trả &lt;code&gt;500&lt;/code&gt; với message của error. Trên môi trường prod, tránh lộ chi tiết implementation; luôn đăng ký &lt;code&gt;setErrorHandler&lt;/code&gt; để chuẩn hóa payload lỗi và log theo request id.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cách debug và monitor
&lt;/h2&gt;

&lt;p&gt;Triệu chứng: request treo tới &lt;code&gt;requestTimeout&lt;/code&gt;, không log access; process log có dòng &lt;code&gt;UnhandledPromiseRejection&lt;/code&gt;; ở K8s, pod restart bất chợt sau khi có traffic lỗi DB. Cách xử lý:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bắt &lt;code&gt;unhandledRejection&lt;/code&gt; và &lt;code&gt;uncaughtException&lt;/code&gt; ở tầng process.&lt;/strong&gt; Theo Node docs &lt;code&gt;process&lt;/code&gt;, đăng ký listener để log structured trước khi crash — nếu không, chỉ thấy stack thô trên stderr, khó phân biệt request nào gây ra. Log kèm &lt;code&gt;err.stack&lt;/code&gt;, &lt;code&gt;err.code&lt;/code&gt;, và nếu có context (&lt;code&gt;AsyncLocalStorage&lt;/code&gt; request id) thì càng tốt.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unhandledRejection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fatal&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unhandled_rejection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// để process exit tự nhiên theo --unhandled-rejections=throw của Node 15+&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uncaughtException&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fatal&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uncaught_exception&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Không "nuốt" bằng listener.&lt;/strong&gt; Đừng dùng &lt;code&gt;unhandledRejection&lt;/code&gt; handler để "cứu" process (chỉ log rồi return). Node docs nói rõ: kể từ Node 15, hành vi mặc định là crash — vì process ở trạng thái không xác định sau một lỗi không bắt. Log để observability, nhưng để process chết và để orchestrator restart.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Metric.&lt;/strong&gt; Counter &lt;code&gt;nodejs_unhandled_rejections_total&lt;/code&gt; (tên tự đặt) tăng khi listener chạy; alert khi rate khác 0. Kèm HTTP error rate theo route/status để biết endpoint nào là nguồn. Với Fastify, &lt;code&gt;setErrorHandler&lt;/code&gt; là điểm tự nhiên để tăng counter theo &lt;code&gt;err.code&lt;/code&gt;/&lt;code&gt;err.statusCode&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CLI flag để chọn hành vi.&lt;/strong&gt; &lt;code&gt;node --unhandled-rejections=strict&lt;/code&gt; ép crash ngay lần đầu (kể cả trên Node &amp;lt; 15 nếu cần backport). &lt;code&gt;warn&lt;/code&gt; cho môi trường dev nếu muốn tiếp tục chạy dù có lỗi. Ở production, để mặc định (&lt;code&gt;throw&lt;/code&gt;) để phát hiện lỗi sớm.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ESLint rule.&lt;/strong&gt; &lt;code&gt;no-floating-promises&lt;/code&gt; (từ &lt;code&gt;@typescript-eslint&lt;/code&gt;) ngăn commit code có Promise không &lt;code&gt;await&lt;/code&gt;/&lt;code&gt;.catch&lt;/code&gt; — chặn failure mode 2 và 4 ngay ở CI. Với JS thuần, &lt;code&gt;promise/no-floating-promises&lt;/code&gt; từ &lt;code&gt;eslint-plugin-promise&lt;/code&gt; làm việc tương tự.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tracing.&lt;/strong&gt; OpenTelemetry &lt;code&gt;@opentelemetry/instrumentation-express&lt;/code&gt; và &lt;code&gt;@opentelemetry/instrumentation-fastify&lt;/code&gt; (theo OpenTelemetry docs mục JavaScript instrumentation) tạo span cho từng middleware/handler. Khi async reject không được bắt, span kết thúc với status &lt;code&gt;ERROR&lt;/code&gt; kèm exception attribute — trace hiện ra chính hàm ném lỗi mà không phải grep log.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule phòng ngừa: (1) mọi route/middleware &lt;code&gt;async&lt;/code&gt; trong Express 4 phải qua &lt;code&gt;asyncHandler&lt;/code&gt; hoặc import &lt;code&gt;express-async-errors&lt;/code&gt;; (2) không fire-and-forget Promise trong handler mà không có &lt;code&gt;.catch&lt;/code&gt;; (3) luôn có listener &lt;code&gt;unhandledRejection&lt;/code&gt; để log structured; (4) bật ESLint &lt;code&gt;no-floating-promises&lt;/code&gt; ở CI; (5) trong Fastify luôn đăng ký &lt;code&gt;setErrorHandler&lt;/code&gt; để chuẩn hóa payload lỗi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoff
&lt;/h2&gt;

&lt;p&gt;Wrapper trong Express 4 rẻ về runtime (một closure và một &lt;code&gt;.catch&lt;/code&gt;), nhưng đắt về kỷ luật: chỉ cần một route mới quên bọc là lại tái hiện failure mode 1. &lt;code&gt;express-async-errors&lt;/code&gt; monkey-patch giúp DX, nhưng đổi lại là behavior không rõ ràng từ code — người đọc phải biết có &lt;code&gt;import 'express-async-errors'&lt;/code&gt; ở đâu đó. Express 5 giải quyết ở tầng framework, giá là chi phí migrate (breaking changes ở &lt;code&gt;path-to-regexp&lt;/code&gt;, &lt;code&gt;req.query&lt;/code&gt; immutable, một số middleware cũ chưa tương thích).&lt;/p&gt;

&lt;p&gt;Fastify auto-catch giảm code boilerplate và loại bỏ cả một class lỗi ở review, nhưng cần chú ý: mọi throw trong handler đều đi qua error handler — không có "throw để log rồi tiếp tục". Nếu muốn behavior khác (ví dụ retry trong cùng handler), phải &lt;code&gt;try/catch&lt;/code&gt; thủ công. Ngoài ra &lt;code&gt;setErrorHandler&lt;/code&gt; mặc định (khi không tự đăng ký) trả message error thô ra client — cần override ở production.&lt;/p&gt;

&lt;p&gt;Rule of thumb: nếu vẫn ở Express 4, bọc &lt;code&gt;asyncHandler&lt;/code&gt; cho &lt;em&gt;mọi&lt;/em&gt; async route/middleware, coi đó là boilerplate cứng như &lt;code&gt;import&lt;/code&gt;; bật ESLint &lt;code&gt;no-floating-promises&lt;/code&gt;. Nếu bắt đầu service mới, chọn Fastify hoặc Express 5 để bỏ hẳn lớp lỗi này. Không dùng listener &lt;code&gt;unhandledRejection&lt;/code&gt; để "cứu" process — chỉ log rồi để crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Câu hỏi phỏng vấn
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Vì sao lỗi ném ra từ một &lt;code&gt;async&lt;/code&gt; route handler trong Express 4 không tới error middleware, và cần làm gì để nó tới?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Express 4 bọc lời gọi handler bằng &lt;code&gt;try/catch&lt;/code&gt; đồng bộ. Một hàm &lt;code&gt;async&lt;/code&gt; không ném lỗi — nó return về một Promise. Đến khi Promise reject, control đã ra khỏi &lt;code&gt;try/catch&lt;/code&gt; từ lâu, nên &lt;code&gt;catch&lt;/code&gt; block không chạy và Express không gọi &lt;code&gt;next(err)&lt;/code&gt;. Reject rơi ra tầng process thành &lt;code&gt;unhandledRejection&lt;/code&gt;; từ Node 15 mặc định &lt;code&gt;--unhandled-rejections=throw&lt;/code&gt;, nghĩa là process crash. Cách sửa chuẩn là wrapper &lt;code&gt;asyncHandler = (fn) =&amp;gt; (req, res, next) =&amp;gt; Promise.resolve(fn(req, res, next)).catch(next)&lt;/code&gt; — biến Promise reject thành &lt;code&gt;next(err)&lt;/code&gt; để error middleware bắt. Có thể thay bằng &lt;code&gt;express-async-errors&lt;/code&gt; (monkey-patch tự &lt;code&gt;await&lt;/code&gt;) hoặc migrate lên Express 5 (native support). Điểm ăn điểm là gọi được ba thứ: cơ chế (&lt;code&gt;try/catch&lt;/code&gt; đồng bộ không bắt được Promise reject), hậu quả production (một lỗi DB làm crash pod thay vì trả 500 JSON), và cách phòng thêm ngoài wrapper — listener &lt;code&gt;unhandledRejection&lt;/code&gt; để log structured, ESLint &lt;code&gt;no-floating-promises&lt;/code&gt; để chặn ở CI, và đối chiếu với Fastify — pipeline hook tự &lt;code&gt;await&lt;/code&gt; handler nên không có class lỗi này ngay từ đầu.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on
&lt;/h2&gt;

&lt;p&gt;Dựng hai server cạnh nhau, tái hiện async reject không bọc, xem process crash, rồi sửa. Toàn bộ chạy được với Node 18+.&lt;/p&gt;

&lt;p&gt;Chuẩn bị:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;async-route &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;async-route
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm pkg &lt;span class="nb"&gt;set type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;module
npm i express fastify pino
npm i &lt;span class="nt"&gt;-D&lt;/span&gt; eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo &lt;code&gt;express-broken.mjs&lt;/code&gt; — bản Express 4 KHÔNG bọc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pino&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pino&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pino&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unhandledRejection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fatal&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unhandled_rejection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uncaughtException&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fatal&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uncaught_exception&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// giả lập DB call reject&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fakeDbFind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bad&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;db_connection_refused&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// SAI: async handler không bọc, reject không tới error middleware&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fakeDbFind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;internal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express-broken :3001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo &lt;code&gt;express-fixed.mjs&lt;/code&gt; — bản có wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pino&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pino&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pino&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;asyncHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fakeDbFind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bad&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;db_connection_refused&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;asyncHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fakeDbFind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;request_failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;internal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express-fixed :3002&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo &lt;code&gt;fastify-server.mjs&lt;/code&gt; — Fastify auto-catch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Fastify&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fastify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Fastify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fakeDbFind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bad&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;db_connection_refused&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setErrorHandler&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;request_failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;internal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fakeDbFind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// reject sẽ được Fastify bắt tự động&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3003&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chạy và quan sát:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal 1: server broken&lt;/span&gt;
node &lt;span class="nt"&gt;--unhandled-rejections&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;throw express-broken.mjs

&lt;span class="c"&gt;# Terminal 2: gọi endpoint tốt và endpoint xấu&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:3001/orders/ok    &lt;span class="c"&gt;# {"id":"ok","total":100}&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:3001/orders/bad   &lt;span class="c"&gt;# request treo, sau đó server tự crash&lt;/span&gt;

&lt;span class="c"&gt;# Quan sát Terminal 1: log fatal "unhandled_rejection" + process exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chạy &lt;code&gt;express-fixed.mjs&lt;/code&gt; và &lt;code&gt;fastify-server.mjs&lt;/code&gt; song song, lặp lại &lt;code&gt;curl .../orders/bad&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node express-fixed.mjs &amp;amp;
node fastify-server.mjs &amp;amp;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;%{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; http://localhost:3002/orders/bad
&lt;span class="c"&gt;# -&amp;gt; {"error":"internal"} 500 -- error middleware chạy, process không crash&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;%{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; http://localhost:3003/orders/bad
&lt;span class="c"&gt;# -&amp;gt; {"error":"internal"} 500 -- Fastify tự bắt qua setErrorHandler&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cuối cùng bật ESLint rule để chặn floating promise ở CI. Tạo &lt;code&gt;.eslintrc.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parser"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@typescript-eslint/parser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"@typescript-eslint"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@typescript-eslint/no-floating-promises"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thêm một hàm fire-and-forget vào một file &lt;code&gt;.ts&lt;/code&gt; (ví dụ &lt;code&gt;audit.write(...)&lt;/code&gt; không &lt;code&gt;await&lt;/code&gt;), chạy &lt;code&gt;npx eslint .&lt;/code&gt; và xác nhận rule bắt được — đây là hàng rào cuối cùng ngăn failure mode 2/4 lọt vào main branch.&lt;/p&gt;

</description>
      <category>middleware</category>
      <category>asyncerrorhandling</category>
      <category>asyncroute</category>
      <category>errorhandler</category>
    </item>
    <item>
      <title>HTTP Server — Request Lifecycle</title>
      <dc:creator>Nghề không nhiều chẳng sắc</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:40:48 +0000</pubDate>
      <link>https://dev.to/nguoidungai/http-server-request-lifecycle-3518</link>
      <guid>https://dev.to/nguoidungai/http-server-request-lifecycle-3518</guid>
      <description>&lt;h1&gt;
  
  
  Request lifecycle: một HTTP request đi qua đâu, và vì sao "quên gọi next()" làm request treo cho tới khi socket timeout
&lt;/h1&gt;

&lt;p&gt;Một request tới một Express hay Fastify server không phải là "gọi handler rồi trả về response". Nó là một chuỗi các bước theo thứ tự cố định: parse HTTP, chạy qua middleware/hook, match route, gọi handler, serialize, gửi response, đóng. Nếu bất kỳ bước nào không chuyển tiếp — Express không gọi &lt;code&gt;next()&lt;/code&gt;, Fastify hook không &lt;code&gt;reply.send()&lt;/code&gt; hay không &lt;code&gt;return&lt;/code&gt; — request treo cho tới khi client hoặc server timeout đóng socket. Đây là loại lỗi không ném exception, không xuất hiện trong error log, chỉ hiện ra qua p99 latency phình lên và số socket ở trạng thái ESTABLISHED tăng dần. Hiểu chính xác lifecycle này là điều kiện tiên quyết để debug những request "biến mất" và để đặt middleware đúng thứ tự.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cơ chế hoạt động
&lt;/h2&gt;

&lt;p&gt;Bước dưới cùng giống nhau ở cả hai framework: Node core &lt;code&gt;http.Server&lt;/code&gt; nhận TCP connection, parse HTTP request line + headers, phát event &lt;code&gt;request&lt;/code&gt; với hai object &lt;code&gt;IncomingMessage&lt;/code&gt; (req) và &lt;code&gt;ServerResponse&lt;/code&gt; (res). Điểm khác nhau là những gì framework làm giữa lúc nhận &lt;code&gt;request&lt;/code&gt; và lúc response ra khỏi socket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Express&lt;/strong&gt; dựng một chuỗi middleware qua &lt;code&gt;Router&lt;/code&gt;. Mỗi lần &lt;code&gt;app.use(fn)&lt;/code&gt; hay &lt;code&gt;app.get(path, fn)&lt;/code&gt; được gọi, Express bọc &lt;code&gt;fn&lt;/code&gt; vào một &lt;code&gt;Layer&lt;/code&gt; với path regex (thông qua &lt;code&gt;path-to-regexp&lt;/code&gt;) rồi push vào một stack. Khi request đến, &lt;code&gt;Router.handle&lt;/code&gt; duyệt stack tuần tự: với mỗi layer, nếu path match, gọi &lt;code&gt;fn(req, res, next)&lt;/code&gt;. &lt;code&gt;next()&lt;/code&gt; là closure trỏ vào layer kế tiếp; chỉ khi nó được gọi thì layer sau mới chạy. Error handler được nhận diện bằng arity — hàm 4 tham số &lt;code&gt;(err, req, res, next)&lt;/code&gt; — và chỉ được duyệt tới khi &lt;code&gt;next(err)&lt;/code&gt; được gọi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1mb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;           &lt;span class="c1"&gt;// 1. parse body&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                    &lt;span class="c1"&gt;// 2. request id&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                    &lt;span class="c1"&gt;// 3. auth&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unauth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 4. route handler&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not_found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                      &lt;span class="c1"&gt;// chuyển sang error handler&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;               &lt;span class="c1"&gt;// 5. error handler (arity 4)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;internal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fastify&lt;/strong&gt; thay chuỗi middleware phẳng bằng một pipeline hook có tên và thứ tự cố định, tài liệu ở Fastify docs mục "Lifecycle": &lt;code&gt;onRequest&lt;/code&gt; → &lt;code&gt;preParsing&lt;/code&gt; → &lt;code&gt;preValidation&lt;/code&gt; → &lt;code&gt;preHandler&lt;/code&gt; → &lt;code&gt;handler&lt;/code&gt; → &lt;code&gt;preSerialization&lt;/code&gt; → &lt;code&gt;onSend&lt;/code&gt; → response gửi đi → &lt;code&gt;onResponse&lt;/code&gt;. Nhánh lỗi rẽ qua &lt;code&gt;onError&lt;/code&gt;. Route matching dùng &lt;code&gt;find-my-way&lt;/code&gt; (radix trie) thay vì regex tuyến tính. Serialization dùng &lt;code&gt;fast-json-stringify&lt;/code&gt; compile schema thành hàm serialize chuyên biệt, thay vì &lt;code&gt;JSON.stringify&lt;/code&gt; chung:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Fastify&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fastify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Fastify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onRequest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;         &lt;span class="c1"&gt;// trước khi body được parse&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preHandler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;        &lt;span class="c1"&gt;// sau parse+validate, trước handler&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;httpErrors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unauthorized&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;httpErrors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;                                    &lt;span class="c1"&gt;// preSerialization -&amp;gt; onSend -&amp;gt; socket&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Điểm chung: cả hai chỉ tiến sang bước kế tiếp khi bước hiện tại "kết thúc". Ở Express, "kết thúc" nghĩa là &lt;code&gt;next()&lt;/code&gt; hoặc gửi response. Ở Fastify async, "kết thúc" nghĩa là promise resolve/reject. Nếu không xảy ra sự kiện đó, pipeline đứng im.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vấn đề gặp trong production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 1: quên &lt;code&gt;next()&lt;/code&gt; — request treo tới timeout.&lt;/strong&gt; Middleware chạy xong phần logic nhưng không gọi &lt;code&gt;next()&lt;/code&gt; và cũng không gửi response — chuỗi dừng lại giữa chừng:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SAI: async middleware "trông có vẻ đủ" nhưng thiếu next()&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;
  &lt;span class="c1"&gt;// quên next() — không có exception, không có log, request đứng im&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Không có lỗi nào ném. Client thấy request đang chờ. Sau vài chục giây, hoặc &lt;code&gt;keepAliveTimeout&lt;/code&gt; hoặc client-side timeout kết thúc, socket đóng ở trạng thái RST/FIN. Log của server không thấy gì vì không middleware nào tới bước gửi response. Cách sửa là luôn kết thúc middleware bằng một trong ba nhánh rõ ràng: gọi &lt;code&gt;next()&lt;/code&gt;, &lt;code&gt;next(err)&lt;/code&gt;, hoặc gửi response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ĐÚNG: bọc async middleware để lỗi và tiếp nối đều chuyển tiếp đúng&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;asyncMw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;asyncMw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Express 5 xử lý phần này gọn hơn: nếu handler là &lt;code&gt;async&lt;/code&gt;, promise reject sẽ tự bị bắt và forward vào error chain. Trên Express 4 (vẫn đang chạy ở phần lớn service), phải tự bọc như trên.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 2: sai thứ tự middleware.&lt;/strong&gt; Middleware là stack có thứ tự — thứ tự &lt;code&gt;app.use(...)&lt;/code&gt; chính là thứ tự chạy. Đặt error handler trước route sẽ khiến nó không bao giờ được gọi (vì &lt;code&gt;next(err)&lt;/code&gt; đi &lt;em&gt;xuôi&lt;/em&gt; chứ không lùi). Đặt logger sau body parser sẽ khiến logger không thấy được request nào bị reject ở tầng parse. Đặt auth sau route handler thì route đã chạy trước khi auth chặn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SAI: route đăng ký trước auth =&amp;gt; endpoint mở toang&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/admin/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listUsers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authMiddleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// ĐÚNG: middleware nào áp cho route nào phải nằm TRƯỚC route đó trong stack&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authMiddleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/admin/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listUsers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure mode 3: gửi response hai lần — &lt;code&gt;ERR_HTTP_HEADERS_SENT&lt;/code&gt;.&lt;/strong&gt; Sau khi &lt;code&gt;res.json()&lt;/code&gt; hay &lt;code&gt;res.send()&lt;/code&gt; chạy, headers đã đi ra socket. Bất kỳ lần ghi tiếp nào đều ném lỗi này. Kịch bản thường gặp: một nhánh &lt;code&gt;if&lt;/code&gt; sớm đã &lt;code&gt;res.send&lt;/code&gt;, code phía sau vẫn tiếp tục và gọi &lt;code&gt;res.send&lt;/code&gt; lần nữa vì thiếu &lt;code&gt;return&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SAI: thiếu return sau res.status(404).json(...)&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not_found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;// không return&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// chạy tiếp, ném ERR_HTTP_HEADERS_SENT + gửi payload sai&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sửa: luôn &lt;code&gt;return res.json(...)&lt;/code&gt; hoặc dùng &lt;code&gt;else&lt;/code&gt;. Trong Fastify, mô hình &lt;code&gt;return payload&lt;/code&gt; từ handler tránh được lớp lỗi này vì reply được commit bởi framework một lần duy nhất.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 4: body parser limit + slow client.&lt;/strong&gt; &lt;code&gt;express.json({ limit: '1mb' })&lt;/code&gt; mặc định ném &lt;code&gt;413 Payload Too Large&lt;/code&gt; khi vượt limit, nhưng nếu không đặt &lt;code&gt;req.setTimeout&lt;/code&gt; hoặc &lt;code&gt;server.requestTimeout&lt;/code&gt;, một client chậm có thể giữ socket mở, upload nhỏ giọt và ăn RAM. Fastify có &lt;code&gt;bodyLimit&lt;/code&gt; (mặc định ~1MB, theo Fastify docs) và tôn trọng &lt;code&gt;server.requestTimeout&lt;/code&gt; của Node core. Với upload thực tế, đặt cả &lt;code&gt;bodyLimit&lt;/code&gt; và một timeout dứt khoát ở tầng server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cách debug và monitor
&lt;/h2&gt;

&lt;p&gt;Triệu chứng "request biến mất" cần được phân biệt với "request lỗi". Log tầng access chỉ ghi khi response gửi đi; nếu request treo giữa middleware, access log không có dòng nào. Bốn cách tiếp cận:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Log từng bước lifecycle.&lt;/strong&gt; Ở Express, thêm một middleware đầu chuỗi ghi &lt;code&gt;req.id&lt;/code&gt;, timestamp bắt đầu; và một hook cuối gắn vào &lt;code&gt;res.on('finish')&lt;/code&gt; + &lt;code&gt;res.on('close')&lt;/code&gt; để phân biệt response gửi thành công (&lt;code&gt;finish&lt;/code&gt;) và socket đóng khi chưa xong (&lt;code&gt;close&lt;/code&gt; mà &lt;code&gt;res.writableEnded === false&lt;/code&gt; là request bị hủy giữa chừng). Ở Fastify, bật &lt;code&gt;logger: true&lt;/code&gt; — nó dùng &lt;code&gt;pino&lt;/code&gt; và tự log mỗi request + response với &lt;code&gt;reqId&lt;/code&gt;, &lt;code&gt;responseTime&lt;/code&gt;. Với các hook trung gian, dùng &lt;code&gt;onRequest&lt;/code&gt;/&lt;code&gt;onResponse&lt;/code&gt; để đo chênh lệch thời gian.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout dứt khoát.&lt;/strong&gt; Đặt &lt;code&gt;server.requestTimeout&lt;/code&gt; và &lt;code&gt;server.headersTimeout&lt;/code&gt; ở tầng &lt;code&gt;http.Server&lt;/code&gt; (theo Node docs &lt;code&gt;http.Server&lt;/code&gt;; &lt;code&gt;requestTimeout&lt;/code&gt; có mặc định khác 0 từ Node 18). Không dựa vào client timeout — curl mặc định không timeout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracing.&lt;/strong&gt; OpenTelemetry có &lt;code&gt;@opentelemetry/instrumentation-express&lt;/code&gt; và &lt;code&gt;@opentelemetry/instrumentation-fastify&lt;/code&gt; (theo OpenTelemetry docs mục JavaScript instrumentation). Chúng bọc từng middleware/hook thành span con, nên khi request treo, trace hiện ra chính span đang mở — biết ngay middleware nào là thủ phạm mà không phải rải &lt;code&gt;console.log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metric.&lt;/strong&gt; Histogram HTTP request duration theo route + status (&lt;code&gt;http.server.request.duration&lt;/code&gt; là tên theo OpenTelemetry semantic conventions cho HTTP). Alert khi p99 tăng đột biến hoặc khi số request không có response (không match cặp start/finish) khác 0. Kèm gauge số socket đang mở qua &lt;code&gt;server.getConnections&lt;/code&gt; — nếu con số này leo lên trong khi RPS không tăng, có request đang giữ socket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule phòng ngừa: (1) middleware phải luôn kết thúc rõ ràng — hoặc &lt;code&gt;next()&lt;/code&gt;, hoặc gửi response, hoặc &lt;code&gt;throw&lt;/code&gt;/&lt;code&gt;next(err)&lt;/code&gt;; (2) &lt;code&gt;return res.send(...)&lt;/code&gt; chứ không viết &lt;code&gt;res.send(...)&lt;/code&gt; không return; (3) mọi server production đều đặt &lt;code&gt;requestTimeout&lt;/code&gt; và &lt;code&gt;headersTimeout&lt;/code&gt; khớp với LB phía trước; (4) route đăng ký sau khi mọi middleware toàn cục đã &lt;code&gt;app.use&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoff
&lt;/h2&gt;

&lt;p&gt;Express linh hoạt vì middleware là hàm arbitrary — có thể chèn bất cứ gì vào bất cứ chỗ nào trong stack. Cái giá là không có ranh giới rõ giữa "trước parse" và "sau validate": một middleware Express về nguyên tắc chạy ở mọi thời điểm trước handler, người viết tự phải giữ thứ tự đúng. Không có schema-based serialization, nên serialize dùng &lt;code&gt;JSON.stringify&lt;/code&gt; chung — chậm hơn khi payload lớn. Router dựa trên regex, chi phí tuyến tính theo số route được đăng ký.&lt;/p&gt;

&lt;p&gt;Fastify chạy nhanh hơn ở workload JSON điển hình — theo benchmark repo &lt;code&gt;fastify/benchmarks&lt;/code&gt; do maintainer Fastify duy trì, throughput trên các route JSON đơn giản cao hơn Express đáng kể (nhiều lần trên cùng phần cứng, cùng payload). Ba nguyên nhân, đều là quyết định thiết kế: (a) &lt;code&gt;find-my-way&lt;/code&gt; radix trie router, tra route ở chi phí gần hằng số theo độ dài path chứ không theo số route; (b) &lt;code&gt;fast-json-stringify&lt;/code&gt; compile schema thành hàm serialize chuyên biệt, tránh reflection và loại field không có trong schema; (c) pipeline hook cố định giúp Fastify pre-compile chuỗi call, không phải duyệt stack động mỗi request. Đổi lại: schema-based có nghĩa payload phải có schema (hoặc chấp nhận rơi về &lt;code&gt;JSON.stringify&lt;/code&gt; chung); mô hình hook cố định khó "chèn ngang" hơn middleware phẳng của Express; ecosystem plugin nhỏ hơn.&lt;/p&gt;

&lt;p&gt;Rule of thumb: chọn Express khi ưu tiên độ quen thuộc, plugin ecosystem rộng, hoặc middleware bên thứ ba không có bản Fastify. Chọn Fastify khi payload JSON là hot path, RPS cao, hoặc muốn schema validation + typed reply có sẵn. Đừng chọn Fastify chỉ vì "nhanh hơn" trên một service RPS thấp — chênh lệch throughput ở dưới vài trăm RPS không thấy được, còn chi phí học/refactor thì thấy ngay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Câu hỏi phỏng vấn
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Một HTTP request đi qua những bước nào trong một Express app, và điều gì xảy ra nếu một middleware quên gọi &lt;code&gt;next()&lt;/code&gt;?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Node core &lt;code&gt;http.Server&lt;/code&gt; parse HTTP request rồi phát event &lt;code&gt;request&lt;/code&gt; với &lt;code&gt;(req, res)&lt;/code&gt;. Express đưa cặp này vào &lt;code&gt;Router.handle&lt;/code&gt;, duyệt tuần tự stack các &lt;code&gt;Layer&lt;/code&gt; đã đăng ký qua &lt;code&gt;app.use&lt;/code&gt;/&lt;code&gt;app.METHOD&lt;/code&gt;. Mỗi layer match path sẽ được gọi &lt;code&gt;(req, res, next)&lt;/code&gt;; chỉ khi &lt;code&gt;next()&lt;/code&gt; được gọi mới sang layer tiếp theo. &lt;code&gt;next(err)&lt;/code&gt; nhảy tới middleware error handler gần nhất (nhận diện bằng arity 4: &lt;code&gt;err, req, res, next&lt;/code&gt;). Khi tới route handler, nó gửi response qua &lt;code&gt;res.json&lt;/code&gt;/&lt;code&gt;res.send&lt;/code&gt;; response ghi xong headers rồi body, event &lt;code&gt;finish&lt;/code&gt; phát ra và socket có thể quay lại pool keep-alive. Nếu một middleware quên &lt;code&gt;next()&lt;/code&gt; và cũng không gửi response, pipeline dừng lại: không có exception, không có log access, request treo cho tới khi &lt;code&gt;server.requestTimeout&lt;/code&gt;/&lt;code&gt;headersTimeout&lt;/code&gt; hoặc client-side timeout đóng socket. Điểm ăn điểm là gọi tên hậu quả production — p99 latency phình lên nhưng error log không thấy gì, số socket ESTABLISHED tăng dần — và cách phòng: bọc async middleware để reject tự forward qua &lt;code&gt;next&lt;/code&gt;, luôn &lt;code&gt;return res.send(...)&lt;/code&gt;, đặt &lt;code&gt;requestTimeout&lt;/code&gt; ở tầng &lt;code&gt;http.Server&lt;/code&gt; khớp với LB, dùng OpenTelemetry instrumentation để trace phơi ra chính hook/middleware đang mở khi request treo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on
&lt;/h2&gt;

&lt;p&gt;Dựng hai server tối thiểu trong cùng repo và làm chúng vỡ theo đúng failure mode ở trên, rồi sửa.&lt;/p&gt;

&lt;p&gt;Chuẩn bị:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;req-lifecycle &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;req-lifecycle
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm pkg &lt;span class="nb"&gt;set type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;module
npm i express fastify pino
npm i &lt;span class="nt"&gt;-D&lt;/span&gt; autocannon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo &lt;code&gt;express-server.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1mb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="c1"&gt;// LOG mỗi bước lifecycle&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hrtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})))&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writableEnded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aborted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Middleware CỐ Ý QUÊN next() — tái hiện request treo&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/broken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_res&lt;/span&gt; &lt;span class="cm"&gt;/*, next */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;broken-mw-entered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="c1"&gt;// không next(), không res.send =&amp;gt; request treo&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express :3001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;      &lt;span class="c1"&gt;// 5s: request treo sẽ bị cắt&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headersTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo &lt;code&gt;fastify-server.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Fastify&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fastify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Fastify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bodyLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onRequest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onRequest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preHandler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preHandler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onResponse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onResponse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elapsedTime&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="c1"&gt;// Handler cố ý không return -&amp;gt; Fastify treo cho tới requestTimeout&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/broken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;  &lt;span class="c1"&gt;// pending mãi mãi&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3002&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chạy song song hai server và quan sát:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node express-server.mjs &amp;amp;
node fastify-server.mjs &amp;amp;

&lt;span class="c"&gt;# 1) request bình thường: thấy log các step in / finish&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:3001/ok
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:3002/ok

&lt;span class="c"&gt;# 2) request treo: curl đợi cho tới khi server cắt tại requestTimeout=5s&lt;/span&gt;
&lt;span class="nb"&gt;time &lt;/span&gt;curl &lt;span class="nt"&gt;-sv&lt;/span&gt; http://localhost:3001/broken
&lt;span class="nb"&gt;time &lt;/span&gt;curl &lt;span class="nt"&gt;-sv&lt;/span&gt; http://localhost:3002/broken

&lt;span class="c"&gt;# 3) bắn tải song song vào /ok trong lúc một request đang treo trên /broken —&lt;/span&gt;
&lt;span class="c"&gt;#    quan sát /ok vẫn phản hồi bình thường (event loop không bị block,&lt;/span&gt;
&lt;span class="c"&gt;#    chỉ có socket của /broken đang giữ)&lt;/span&gt;
npx autocannon &lt;span class="nt"&gt;-c&lt;/span&gt; 50 &lt;span class="nt"&gt;-d&lt;/span&gt; 10 http://localhost:3001/ok
npx autocannon &lt;span class="nt"&gt;-c&lt;/span&gt; 50 &lt;span class="nt"&gt;-d&lt;/span&gt; 10 http://localhost:3002/ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sau đó sửa &lt;code&gt;/broken&lt;/code&gt; ở Express bằng cách thêm &lt;code&gt;next()&lt;/code&gt; (hoặc gửi response), và ở Fastify bằng cách &lt;code&gt;return {...}&lt;/code&gt; từ handler. Chạy lại &lt;code&gt;time curl&lt;/code&gt; và xác nhận response trả về ngay, không còn phải chờ &lt;code&gt;requestTimeout&lt;/code&gt;. Cuối cùng bật một OpenTelemetry SDK với &lt;code&gt;@opentelemetry/instrumentation-express&lt;/code&gt; hoặc &lt;code&gt;@opentelemetry/instrumentation-fastify&lt;/code&gt; (theo hướng dẫn ở OpenTelemetry docs), reproduce lại request treo và xem trace: span của middleware/hook đang treo hiện ra ở trạng thái chưa đóng — đây là cách khoanh vùng thủ phạm trong service thật mà không phải rải &lt;code&gt;console.log&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>core</category>
      <category>httpserver</category>
      <category>requestlifecycle</category>
      <category>node</category>
    </item>
    <item>
      <title>End-to-end Design Walkthrough — Full System Design</title>
      <dc:creator>Nghề không nhiều chẳng sắc</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:40:44 +0000</pubDate>
      <link>https://dev.to/nguoidungai/end-to-end-design-walkthrough-full-system-design-26i0</link>
      <guid>https://dev.to/nguoidungai/end-to-end-design-walkthrough-full-system-design-26i0</guid>
      <description>&lt;h1&gt;
  
  
  Full system design walkthrough: vì sao quy trình 5 bước quan trọng hơn thuật ngữ, và cái bẫy nhảy thẳng vào deep-dive
&lt;/h1&gt;

&lt;p&gt;Một buổi phỏng vấn system design 45–60 phút không đánh giá xem thí sinh biết bao nhiêu tên công nghệ, mà đánh giá xem thí sinh có xử lý được một bài toán mơ hồ theo một quy trình có kỷ luật hay không. Cùng đề "thiết kế URL shortener", người thất bại thường bắt đầu bằng "dùng Cassandra vì scale tốt" ngay ở phút thứ hai — chưa hỏi scale bao nhiêu, chưa biết read/write ratio, chưa đồng ý interface. Người pass đi theo một trình tự cứng: &lt;strong&gt;requirements → estimation → high-level → deep-dive → tradeoff&lt;/strong&gt;. Cùng một đề, cùng thời lượng, nhưng người thứ hai dẫn dắt được interviewer thay vì bị dí. Bài này mô tả quy trình đó ở mức thực dụng, các thất bại thường gặp khi bỏ bước, và một hands-on end-to-end trên URL shortener để tự luyện.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cơ chế hoạt động
&lt;/h2&gt;

&lt;p&gt;Quy trình chuẩn được nhiều tài liệu tổng hợp (Alex Xu — &lt;em&gt;System Design Interview&lt;/em&gt; vol.1; Donne Martin — &lt;em&gt;system-design-primer&lt;/em&gt;; Kleppmann — &lt;em&gt;Designing Data-Intensive Applications&lt;/em&gt;) đều có 4–5 bước gần như trùng nhau. Đây là khung 5 bước dùng thực tế trong phỏng vấn:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Requirements clarification&lt;/strong&gt; (~5 phút). Chốt functional requirement (hệ thống làm gì — API nào, use case nào) và non-functional requirement (availability target, latency target, consistency yêu cầu, security, cost). Không đoán — hỏi lại interviewer. Output: 3–5 gạch đầu dòng functional + 3–5 gạch NFR + danh sách "out of scope" (analytics, billing, ...).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back-of-the-envelope estimation&lt;/strong&gt; (~5 phút). Tính DAU, QPS peak/avg, read:write ratio, storage growth theo năm, bandwidth. Không cần chính xác — cần đúng order of magnitude để quyết định "cần shard hay không", "cache có ý nghĩa không", "một node đủ hay phải scale ngang".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-level design&lt;/strong&gt; (~10–15 phút). Vẽ box diagram: client → LB → app tier → cache → primary datastore → async worker → object storage/CDN. Định nghĩa API (endpoint, request/response, status code) và data model (schema chính, index chính). Ở bước này chưa đi sâu vào từng box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep-dive&lt;/strong&gt; (~15–20 phút). Interviewer sẽ chỉ vào một box và hỏi "cái này scale thế nào?". Đây là nơi giải thích: sharding scheme (partition key nào, tại sao), replication (sync/async, quorum), cache strategy (cache-aside, write-through, TTL, invalidation), consistency model (read-your-write, eventual), bottleneck và cách xử lý.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tradeoff &amp;amp; wrap-up&lt;/strong&gt; (~5 phút). Nêu lựa chọn nào đánh đổi cái gì, cái gì có thể làm khác nếu NFR đổi (vd: nếu strong consistency là bắt buộc thì phải bỏ multi-region active-active), và các failure mode chính đã được xử lý (single AZ down, DB primary chết, cache miss storm).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Một mental checklist ngắn để không lạc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[R] Functional API:      ...
[R] NFR (SLA, RPS, p99): ...
[R] Out of scope:        ...
[E] DAU / QPS:           ...
[E] Read:Write ratio:    ...
[E] Storage / year:      ...
[H] API contract:        POST /... GET /...
[H] Data model:          table(...), index(...)
[H] Diagram:             client -&amp;gt; LB -&amp;gt; app -&amp;gt; cache -&amp;gt; DB / queue -&amp;gt; worker
[D] Partition key:       ...
[D] Cache strategy:      ...
[D] Consistency:         ...
[D] Bottleneck &amp;amp; fix:    ...
[T] What breaks:         ...
[T] What we would change:...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Điểm quan trọng: các bước này &lt;strong&gt;không phải tuyến tính cứng&lt;/strong&gt;. Deep-dive có thể phát hiện estimation sai → quay lại estimation. Nhưng bỏ hẳn một bước — điển hình là bỏ estimation và requirements — thì phần sau mất neo, mọi lựa chọn thành opinion không có căn cứ.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vấn đề gặp trong production
&lt;/h2&gt;

&lt;p&gt;Đây là "production" của bài phỏng vấn — thất bại thật sự lặp lại ở loạt vòng System Design mà nhiều tài liệu (Alex Xu, &lt;code&gt;interviewing.io&lt;/code&gt; blog, Grokking SDI) đã tổng kết:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 1: nhảy vào chi tiết không có cấu trúc.&lt;/strong&gt; Đề "design Twitter", ngay phút thứ 3 đã vẽ Kafka + Cassandra + Redis + Elasticsearch. Interviewer hỏi "read:write ratio là bao nhiêu?" → không biết. Hỏi "vì sao Cassandra chứ không Postgres?" → không trả lời được vì chưa biết access pattern. Đây là dạng thất bại phổ biến nhất: chọn công nghệ trước khi có yêu cầu.&lt;/p&gt;

&lt;p&gt;Sai:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Sẽ dùng Cassandra vì scale tốt, Kafka làm message bus, Redis cache,
 Elasticsearch cho search, Nginx load balancer..."
(đã 5 phút trôi qua, chưa có 1 dòng requirement, chưa có API, chưa có schema)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Đúng — bắt đầu ngược lại:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Trước khi chọn stack, cần rõ vài thứ:
 - System hỗ trợ post text/media hay chỉ text?
 - Timeline home feed là chronological hay ranked?
 - Read heavy hay write heavy? Order of magnitude DAU?
 - Latency target cho feed? p99 dưới 200ms?
 - Cần strong consistency cho counter không, hay eventual ok?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sau khi có câu trả lời mới sang estimation, rồi high-level, rồi mới nói tới Kafka/Cassandra — và lúc đó mỗi lựa chọn có lý do gắn với requirement cụ thể.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 2: bỏ back-of-envelope estimation.&lt;/strong&gt; Không có ước lượng thì mọi câu "cần shard không", "một DB đủ không", "cache có đáng không" đều thành đoán mò. Tệ hơn: chọn kiến trúc quá phức tạp cho tải nhỏ (over-engineer) hoặc quá đơn giản cho tải lớn (miss bottleneck). Ví dụ với URL shortener: nếu không tính ra "read gấp nhiều lần write" (thường được cite ở tỉ lệ order-of-magnitude 100:1 trong các tài liệu SDI), sẽ không thấy vì sao cache là quyết định trung tâm chứ không phải phụ. Cách sửa: sau khi có DAU và QPS, luôn viết ra một dòng "read RPS ≈ ..., write RPS ≈ ..., ratio ≈ ..." và một dòng "storage tăng ≈ ... / năm" trước khi vẽ box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 3: thiết kế thiếu trọng tâm — bỏ sót thành phần bắt buộc.&lt;/strong&gt; Vẽ xong high-level mà thiếu load balancer, thiếu cache tier, thiếu async worker cho việc nặng, thiếu CDN cho static, thiếu health check / retry / timeout — đến deep-dive bị hỏi "làm sao xử lý spike?" thì đứng hình. Rule để không sót: mỗi box phải trả lời được ba câu — "nó scale thế nào (horizontal/vertical)", "nó fail thì sao (replica, failover)", "nó bị chậm thì có gì đỡ (cache, queue, backpressure)". Nếu một box không trả lời được cả ba, đó là chỗ interviewer sẽ đâm vào.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 4: deep-dive không có ưu tiên.&lt;/strong&gt; Interviewer nói "đi sâu vào phần thấy thú vị nhất" — thí sinh chọn phần dễ (thường là tầng app stateless) thay vì phần khó (sharding datastore, hot key, cache invalidation). Cách xử lý: chủ động chỉ ra 2–3 chỗ &lt;em&gt;thật sự&lt;/em&gt; khó của bài rồi chọn một để đi sâu — điều này cho thấy đã nhận diện được risk area, khác hẳn với đi sâu chỗ an toàn cho có.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 5: quản lý thời gian sai — hết giờ ở high-level.&lt;/strong&gt; Dành 20 phút cho requirements + high-level, còn 5 phút cho deep-dive và không có wrap-up. Interviewer chấm điểm deep-dive nặng nhất nên phần yếu nhất lại là phần nặng điểm nhất. Rule cứng: đặt time-box mềm cho mỗi bước và tự nhắc mình sang bước tiếp — nếu chưa xong requirement sau 7 phút thì bỏ những thứ còn mơ hồ vào "assumption" và đi tiếp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 6: không nêu tradeoff, chỉ nêu lựa chọn.&lt;/strong&gt; Nói "dùng Postgres" không đủ; phải nói "chọn Postgres vì write pattern có transaction, chấp nhận scale ngang khó hơn Cassandra — nếu QPS write vượt X thì sẽ shard theo user_id hoặc chuyển sang một wide-column store". Mỗi lựa chọn kèm điều kiện đảo. Đây là cách interviewer phân biệt senior với người học thuộc bài.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cách debug và monitor
&lt;/h2&gt;

&lt;p&gt;"Debug" ở đây là tự kiểm tra buổi phỏng vấn khi đang chạy — không có monitor tự động cho câu trả lời miệng, nhưng có heuristic thay thế.&lt;/p&gt;

&lt;p&gt;Triệu chứng đang lạc bước:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Đã 10 phút mà chưa vẽ box đầu tiên → mắc kẹt ở requirement, cần bỏ chi tiết chưa rõ vào "assumption: ..." và đi tiếp.&lt;/li&gt;
&lt;li&gt;Interviewer hỏi cùng một câu hai lần theo cách khác → dấu hiệu câu trả lời chưa chạm; dừng lại xác nhận lại câu hỏi thay vì nói tiếp.&lt;/li&gt;
&lt;li&gt;Đã vẽ 6 box mà chưa có API/schema nào → high-level thiếu "mặt phẳng dữ liệu", quay lại định nghĩa API và data model trước khi thêm box.&lt;/li&gt;
&lt;li&gt;Không có con số nào trong phần deep-dive (không QPS, không kích thước row, không cache hit ratio giả định) → deep-dive đang là opinion; thêm một estimation cụ thể.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Checklist "monitor" trước khi kết thúc — dùng như post-flight check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Đã hỏi ít nhất 3 câu clarify requirement chưa?&lt;/li&gt;
&lt;li&gt;[ ] Có 1 dòng QPS + 1 dòng storage growth + 1 dòng ratio chưa?&lt;/li&gt;
&lt;li&gt;[ ] Có API contract (method, path, request, response) chưa?&lt;/li&gt;
&lt;li&gt;[ ] Có schema chính (bảng, primary key, index) chưa?&lt;/li&gt;
&lt;li&gt;[ ] Có LB + cache + primary store + async layer trên diagram chưa?&lt;/li&gt;
&lt;li&gt;[ ] Đã nêu partition key và tại sao chưa?&lt;/li&gt;
&lt;li&gt;[ ] Đã nêu ít nhất 2 failure mode và cách xử lý chưa?&lt;/li&gt;
&lt;li&gt;[ ] Đã nêu ít nhất 1 tradeoff kèm điều kiện đảo chưa?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule phòng ngừa: luyện quy trình bằng cách viết ra khung 5 bước ở đầu tờ giấy nháp (hoặc góc whiteboard) &lt;em&gt;trước khi&lt;/em&gt; nghe đề — buộc bản thân đi qua tuần tự, không cho phép nhảy. Sau vài lần lặp, quy trình trở thành reflex, không phải checklist phải nhớ.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoff
&lt;/h2&gt;

&lt;p&gt;Quy trình có cấu trúc bao quát được toàn bộ bề mặt bài toán, giảm rủi ro bỏ sót và giúp interviewer chấm dễ (vì họ đã có rubric theo bước) — đổi lại nó cần luyện tập nhiều buổi để không trở thành đọc kịch bản cứng nhắc. Đi quá cứng theo bước dễ mất tự nhiên: chuyển từ "estimation" sang "high-level" mà bỏ qua manh mối interviewer vừa gợi ý là mất điểm giao tiếp; ngược lại đi quá tự do thì thiếu một bước nào đó, mất điểm coverage. Time-box quá chặt thì không kịp deep-dive; quá lỏng thì hết giờ ở requirement. Chọn công nghệ sớm (trước estimation) giúp câu chuyện có "hook" nhưng dễ bị hỏi ngược "tại sao đúng cái đó?" và không trả lời được; chọn muộn (sau deep-dive) an toàn hơn nhưng câu chuyện lan man.&lt;/p&gt;

&lt;p&gt;Rule of thumb: đi đúng 5 bước theo thứ tự, nhưng time-box mềm — nếu một bước tràn quá 30% thời lượng dự kiến, chốt assumption và đi tiếp; luôn kết thúc bằng ít nhất một tradeoff kèm điều kiện đảo. Trong khoảng 10 buổi mock đầu tiên tuân thủ khung này chặt để tạo reflex; sau đó nới lỏng theo tình huống, giữ 5 mốc bắt buộc (requirement, estimation, API/schema, một deep-dive có số, một tradeoff).&lt;/p&gt;

&lt;h2&gt;
  
  
  Câu hỏi phỏng vấn
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Quy trình thiết kế hệ thống trong một buổi phỏng vấn System Design gồm những bước nào, và tại sao thứ tự đó lại quan trọng?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Năm bước theo thứ tự: &lt;strong&gt;requirements&lt;/strong&gt; (functional + non-functional + out-of-scope, hỏi lại interviewer để chốt), &lt;strong&gt;back-of-the-envelope estimation&lt;/strong&gt; (DAU, QPS đọc/ghi, storage growth, ratio đọc/ghi — đủ order of magnitude để quyết định shard/cache), &lt;strong&gt;high-level design&lt;/strong&gt; (API contract, data model chính, box diagram với LB + app + cache + DB + async worker), &lt;strong&gt;deep-dive&lt;/strong&gt; (partition key, replication, consistency model, cache strategy và invalidation, bottleneck cụ thể), và &lt;strong&gt;tradeoff + wrap-up&lt;/strong&gt; (mỗi lựa chọn kèm điều kiện đảo, failure mode chính đã cover). Thứ tự quan trọng vì mỗi bước là input cho bước sau: không có requirement thì không biết estimate cái gì; không có estimation thì mọi lựa chọn công nghệ ở high-level đều là opinion; không có high-level ổn thì deep-dive lạc chỗ; không có tradeoff thì interviewer không phân biệt được senior với người học thuộc bài. Điểm ăn điểm là gọi tên hai failure mode production của chính quy trình phỏng vấn: (1) nhảy thẳng vào chi tiết công nghệ (Kafka/Cassandra/Redis) trước khi có requirement — mọi câu "tại sao chọn cái này?" đều đứng hình; (2) thiết kế thiếu trọng tâm — bỏ sót load balancer/cache/async worker hoặc không nêu partition key ở deep-dive; cách xử lý là bám khung 5 bước với time-box mềm, luôn có ít nhất một con số ở deep-dive, và mỗi lựa chọn kèm điều kiện đảo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on
&lt;/h2&gt;

&lt;p&gt;Bài tập: thiết kế end-to-end một URL shortener theo đúng 5 bước, rồi dựng phiên bản tối thiểu bằng Docker để tự bắn tải và quan sát bottleneck xuất hiện ở đúng chỗ mà phần deep-dive đã dự đoán.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bước 1 — Requirements (viết ra trước khi code):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Functional:
  - POST /shorten { url } -&amp;gt; { short }
  - GET  /:short          -&amp;gt; 301 -&amp;gt; original URL
  - (optional) custom alias
Non-functional:
  - Read heavy (click gấp nhiều lần tạo mới) — ratio order-of-magnitude 100:1
  - p99 redirect &amp;lt; 100ms
  - Availability cao (redirect là hot path)
  - Short code không đoán được
Out of scope:
  - Analytics chi tiết, billing, user auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bước 2 — Estimation (qualitative, đủ để quyết định):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Giả sử vài trăm triệu URL mới / năm  -&amp;gt; vài GB row data / tháng (thân URL + short)
Read RPS peak lớn hơn write RPS ~100 lần  -&amp;gt; cache là quyết định trung tâm
Short code 7 ký tự base62 -&amp;gt; ~62^7 ≈ hơn 3 nghìn tỉ tổ hợp — dư
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bước 3 — High-level design + API + schema:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client -&amp;gt; nginx (LB, TLS) -&amp;gt; app (stateless) -&amp;gt; Redis (cache) -&amp;gt; Postgres (primary)
                                              -&amp;gt; Postgres write path (POST /shorten)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;short&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;long_url&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bước 4 — Deep-dive (viết ra trước khi dựng):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Partition key: short (hash-shard nếu vượt 1 node)
Cache: cache-aside, key = short, TTL vài giờ, invalidate khi update
Hot key: một short viral -&amp;gt; Redis single key nóng, cần local in-process cache 1s TTL
ID generation: base62 của counter tăng dần (Snowflake-like) hoặc random 7-char + retry on conflict
Consistency: eventual — cache stale vài giây chấp nhận được
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bước 5 — Dựng phiên bản tối thiểu để verify deep-dive đúng.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tạo thư mục lab và service Node tối giản (&lt;code&gt;server.js&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ioredis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PG_URL&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIS_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;USE_CACHE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;USE_CACHE&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shortCode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CREATE TABLE IF NOT EXISTS urls(short TEXT PRIMARY KEY,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; long_url TEXT NOT NULL,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; created_at TIMESTAMPTZ NOT NULL DEFAULT now())&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/shorten&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;short&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shortCode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INSERT INTO urls(short,long_url) VALUES($1,$2)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;short&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;short&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;short&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;long&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;USE_CACHE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;long&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;u:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;short&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT long_url FROM urls WHERE short=$1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;short&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;long&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;long_url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;USE_CACHE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;u:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;short&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;long&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EX&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;301&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;long&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"url-lab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ioredis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^5"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json server.js ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;pw&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;urls&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6379:6379"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;PG_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://postgres:pw@pg:5432/urls&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://redis:6379&lt;/span&gt;
      &lt;span class="na"&gt;USE_CACHE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pg&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dựng cụm và tạo một URL để lấy short code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; url-lab &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;url-lab
&lt;span class="c"&gt;# lưu 4 file trên vào thư mục này&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;

&lt;span class="nv"&gt;SHORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST localhost:3000/shorten &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"url":"https://example.com/very/long/path"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/.*"short":"\([^"]*\)".*/\1/'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"short=&lt;/span&gt;&lt;span class="nv"&gt;$SHORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
curl &lt;span class="nt"&gt;-sI&lt;/span&gt; &lt;span class="s2"&gt;"localhost:3000/&lt;/span&gt;&lt;span class="nv"&gt;$SHORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bắn tải vào redirect (read hot path), CHƯA bật cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS: brew install wrk ; Debian/Ubuntu: sudo apt-get install wrk&lt;/span&gt;
wrk &lt;span class="nt"&gt;-t4&lt;/span&gt; &lt;span class="nt"&gt;-c100&lt;/span&gt; &lt;span class="nt"&gt;-d20s&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:3000/&lt;/span&gt;&lt;span class="nv"&gt;$SHORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# ghi lại Requests/sec và Latency p99&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bật cache và đo lại:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# sửa USE_CACHE trong docker-compose.yml thành "1"&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; app
wrk &lt;span class="nt"&gt;-t4&lt;/span&gt; &lt;span class="nt"&gt;-c100&lt;/span&gt; &lt;span class="nt"&gt;-d20s&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:3000/&lt;/span&gt;&lt;span class="nv"&gt;$SHORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# kỳ vọng: RPS tăng nhiều lần, p99 giảm rõ; Postgres CPU giảm sâu&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify Postgres đã hết là bottleneck khi có cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;pg psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-d&lt;/span&gt; urls &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT query, calls, mean_exec_time FROM pg_stat_statements ORDER BY calls DESC LIMIT 5;"&lt;/span&gt;
&lt;span class="c"&gt;# nếu chưa có extension:&lt;/span&gt;
&lt;span class="c"&gt;#   docker compose exec pg psql -U postgres -c "CREATE EXTENSION pg_stat_statements"&lt;/span&gt;
&lt;span class="c"&gt;# và thêm shared_preload_libraries='pg_stat_statements' vào postgresql.conf rồi restart&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tái hiện hot-key: chọn 1 short code duy nhất và bắn &lt;code&gt;wrk&lt;/code&gt; với nhiều kết nối vào chính key đó — Redis single-key sẽ trở thành bottleneck (một shard, một key). Thêm một tầng in-process LRU cache 1s TTL trong &lt;code&gt;server.js&lt;/code&gt; và đo lại — RPS lại nhảy vì phần lớn request không còn ra tới Redis. Đây chính là hot-key mitigation mà phần deep-dive đã dự đoán.&lt;/p&gt;

&lt;p&gt;Cuối cùng, viết một trang one-pager tóm gọn: 5 bước, mỗi bước 3–5 dòng, khớp với những gì lab vừa verify. Đây là artifact mang vào phỏng vấn thật — chỉ khác đề, khung không đổi.&lt;/p&gt;

</description>
      <category>patterns</category>
      <category>endtoenddesignwalkthrough</category>
      <category>fullsystemdesign</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Idempotency — Safe Retry</title>
      <dc:creator>Nghề không nhiều chẳng sắc</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:40:41 +0000</pubDate>
      <link>https://dev.to/nguoidungai/idempotency-safe-retry-24o1</link>
      <guid>https://dev.to/nguoidungai/idempotency-safe-retry-24o1</guid>
      <description>&lt;h1&gt;
  
  
  Safe retry: idempotency key để retry một request không biến thành hai lần charge
&lt;/h1&gt;

&lt;p&gt;Trong hệ phân tán, retry là mặc định — client, gateway, load balancer, queue consumer đều retry khi timeout hoặc lỗi tạm thời. Vấn đề là nhiều thao tác quan trọng không &lt;strong&gt;idempotent&lt;/strong&gt; tự nhiên: một request &lt;code&gt;POST /charges&lt;/code&gt; gửi hai lần thì trừ tiền khách hai lần, một message &lt;code&gt;OrderCreated&lt;/code&gt; xử lý hai lần thì ship hai đơn. Idempotency key là cơ chế để server nhận diện "cùng một intent" giữa các lần retry và chỉ thực hiện side effect &lt;em&gt;một lần&lt;/em&gt;, trong khi vẫn trả về response giống hệt cho mọi lần gọi lặp — về mặt hiệu ứng thấy được từ bên ngoài, đây là cái người ta hay gọi là "exactly-once effect" (dù ở tầng transport vẫn là at-least-once).&lt;/p&gt;

&lt;h2&gt;
  
  
  Cơ chế hoạt động
&lt;/h2&gt;

&lt;p&gt;Client sinh một identifier duy nhất cho mỗi thao tác (thường là UUIDv4) và đính kèm request — quy ước phổ biến là HTTP header &lt;code&gt;Idempotency-Key&lt;/code&gt; (Stripe API dùng đúng tên này, và IETF có draft &lt;code&gt;draft-ietf-httpapi-idempotency-key-header&lt;/code&gt; chuẩn hoá cùng tên header). Server dùng key làm identity của thao tác trong một cửa sổ TTL:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Nhận request với key &lt;code&gt;K&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tra &lt;code&gt;K&lt;/code&gt; trong idempotency store. Nếu tồn tại và request cũ ở trạng thái &lt;em&gt;terminal&lt;/em&gt; (đã có response), trả lại response đã lưu — không chạy lại business logic.&lt;/li&gt;
&lt;li&gt;Nếu tồn tại nhưng đang &lt;em&gt;in-flight&lt;/em&gt;, trả &lt;code&gt;409 Conflict&lt;/code&gt; (hoặc chờ, tuỳ contract).&lt;/li&gt;
&lt;li&gt;Nếu chưa tồn tại, &lt;code&gt;INSERT&lt;/code&gt; bản ghi với unique constraint trên key, chạy business logic, persist response, commit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Điểm cốt lõi là bước insert + bước business logic + bước lưu response phải nằm trong &lt;strong&gt;cùng một transaction boundary&lt;/strong&gt; — hoặc chí ít, phải có cơ chế đảm bảo không có window mà một retry khác nhìn thấy "chưa có key" trong lúc lần đầu vẫn đang chạy dở.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;idempotency_keys&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;key&lt;/span&gt;           &lt;span class="nb"&gt;TEXT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;       &lt;span class="nb"&gt;BIGINT&lt;/span&gt;      &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;request_hash&lt;/span&gt;  &lt;span class="nb"&gt;TEXT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- fingerprint payload&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;        &lt;span class="nb"&gt;TEXT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- in_flight | succeeded | failed&lt;/span&gt;
  &lt;span class="n"&gt;response_code&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;response_body&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;    &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;locked_until&lt;/span&gt;  &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;idempotency_keys&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;-- để TTL sweeper dùng&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Express handler cho POST /charges&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/charges&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;missing Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bodyHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;canonicalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Cố gắng chiếm key. Unique constraint ép serialize retry cùng key.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`INSERT INTO idempotency_keys (user_id, key, request_hash, status, locked_until)
       VALUES ($1, $2, $3, 'in_flight', now() + interval '30 seconds')
       ON CONFLICT (user_id, key) DO NOTHING
       RETURNING key`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bodyHash&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// 2. Key đã tồn tại — đọc bản cũ.&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`SELECT status, request_hash, response_code, response_body
         FROM idempotency_keys WHERE user_id=$1 AND key=$2 FOR UPDATE`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request_hash&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;bodyHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idempotency key reused with different payload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in_flight&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;request in progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response_code&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response_body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Lần đầu — chạy business logic trong cùng transaction.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;charge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createCharge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// dùng chung client&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`UPDATE idempotency_keys
       SET status='succeeded', response_code=$3, response_body=$4
       WHERE user_id=$1 AND key=$2`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Với message queue (Kafka, SQS, RabbitMQ), pattern giống hệt: dedup theo một business key (order id, payment intent id) trong một dedup store — Kafka producer còn có &lt;code&gt;enable.idempotence=true&lt;/code&gt; để bản thân producer không ghi trùng vào broker, nhưng đó là idempotency ở tầng transport, không thay được dedup ở tầng consumer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vấn đề gặp trong production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 1: race condition với hai retry song song cùng key.&lt;/strong&gt; Không phải retry nào cũng tuần tự — khi client timeout ở lần 1 và bắn lại lần 2, request 1 có thể vẫn đang chạy dở trên server khác. Nếu handler chỉ làm "&lt;code&gt;SELECT&lt;/code&gt; xem key có chưa, nếu chưa thì &lt;code&gt;INSERT&lt;/code&gt; và chạy", hai request đều thấy chưa có key và cùng chạy — double charge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SAI: check-then-act không atomic&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM idempotency_keys WHERE key=$1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;charge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;  &lt;span class="c1"&gt;// hai request cùng vào đây&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO idempotency_keys ...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix là để database tự serialize bằng unique constraint (&lt;code&gt;INSERT ... ON CONFLICT DO NOTHING&lt;/code&gt; như snippet ở trên) hoặc một distributed lock (Redis &lt;code&gt;SET NX PX&lt;/code&gt;, DynamoDB conditional write). Cái &lt;em&gt;duy nhất&lt;/em&gt; an toàn là một atomic op quyết định "ai là người thắng slot key này".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 2: cache response của lỗi non-terminal.&lt;/strong&gt; Nếu server lưu response cho cả các lỗi tạm thời (&lt;code&gt;503&lt;/code&gt;, timeout upstream), lần retry sau sẽ nhận lại lỗi thay vì được chạy lại thật. Ngược lại, nếu quên lưu response khi request đã hoàn tất, retry sẽ chạy business logic lại. Nguyên tắc: chỉ persist response khi thao tác đạt trạng thái &lt;em&gt;terminal&lt;/em&gt; (đã charge xong hoặc đã reject bằng business rule cứng như "thẻ hết hạn"). Với lỗi hạ tầng, xoá bản ghi key (hoặc để status &lt;code&gt;failed_retryable&lt;/code&gt;) và cho retry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 3: key scoping quá rộng.&lt;/strong&gt; Đưa &lt;code&gt;Idempotency-Key&lt;/code&gt; làm primary key toàn hệ thống nghĩa là một user có thể (vô tình hoặc cố ý) đâm vào key của user khác. Ngoài lỗ hổng bảo mật, còn có bug thật: một client library sinh key kém entropy (dùng timestamp giây) sẽ va chạm. Luôn scope theo &lt;code&gt;(tenant/user_id, key)&lt;/code&gt; như trong schema trên.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 4: server sinh key thay vì client.&lt;/strong&gt; Nếu server tự sinh key khi thấy request thiếu header, mỗi lần retry sẽ có key mới và mất luôn ý nghĩa dedup. Key &lt;em&gt;phải&lt;/em&gt; do client sinh trước khi bắn request, và giữ nguyên qua mọi lần retry của cùng intent — thường là sinh ngay khi user bấm nút "Pay", lưu trong memory/localStorage, retry cùng key cho tới khi có response terminal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 5: side effect ngoài transaction.&lt;/strong&gt; Business logic có thể chạm hệ ngoài không rollback được (gửi email, gọi payment gateway ngoài, publish event Kafka). Nếu commit idempotency row thành công nhưng crash trước khi gửi email, retry sẽ thấy key &lt;code&gt;succeeded&lt;/code&gt; và bỏ qua email — dropped. Nếu gửi email xong rồi crash trước khi commit row, retry sẽ gửi email lần hai. Cách xử lý chuẩn là &lt;strong&gt;transactional outbox&lt;/strong&gt;: ghi event vào bảng outbox trong cùng transaction với idempotency row, một relay riêng đọc outbox và publish với retry idempotent ở phía downstream. Với payment gateway ngoài, forward luôn &lt;code&gt;Idempotency-Key&lt;/code&gt; xuống (Stripe, Adyen, Braintree đều accept) để chuỗi idempotent xuyên tầng.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 6: TTL cắt trước khi client hết retry.&lt;/strong&gt; Idempotency store phải có TTL (không giữ mãi), nhưng nếu TTL ngắn hơn window client retry (mobile client có thể retry sau vài giờ khi có mạng lại), retry rơi vào "key không còn" và chạy lại. Stripe docs nêu retention khoảng một ngày cho key — chọn TTL đủ dài so với retry policy dài nhất của client hợp lệ, không đặt bằng vài phút.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cách debug và monitor
&lt;/h2&gt;

&lt;p&gt;Triệu chứng ngoài production của một hệ idempotency hỏng thường là &lt;strong&gt;duplicate charge / duplicate order&lt;/strong&gt; report từ support, không phải error log — vì mỗi request lẻ đều &lt;code&gt;200 OK&lt;/code&gt;. Vì vậy phải chủ động đo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate rate metric&lt;/strong&gt;: tỉ lệ request mà idempotency lookup match key có sẵn / tổng số request có key. Rate quá thấp (gần 0) nghĩa là client không retry, hoặc client sinh key mới mỗi lần — dedup không có tác dụng. Rate quá cao đột biến là dấu hiệu retry storm (thường do client timeout config quá ngắn so với p99 server).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-flight collision rate&lt;/strong&gt;: số lần trả &lt;code&gt;409&lt;/code&gt; do key đang chạy. Đột biến ở đây thường trùng với latency spike phía dependency (payment gateway chậm khiến client retry trong lúc server chưa xong).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload mismatch rate&lt;/strong&gt;: số lần retry với cùng key nhưng &lt;code&gt;request_hash&lt;/code&gt; khác. Thường lộ bug client — user sửa amount rồi retry với key cũ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response header đánh dấu replay&lt;/strong&gt;: nhiều API (Stripe là ví dụ) trả một header đánh dấu request được replay từ store thay vì chạy mới. Log header này ở client side để phân biệt "server thực sự chạy" và "server chỉ trả lại cache".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Về công cụ: với Postgres store, &lt;code&gt;pg_stat_user_tables&lt;/code&gt; xem hot key trên bảng &lt;code&gt;idempotency_keys&lt;/code&gt;, &lt;code&gt;pg_locks&lt;/code&gt; khi nghi có contention; với DynamoDB store (AWS Lambda Powertools idempotency module dùng đúng pattern này), CloudWatch metric &lt;code&gt;ConditionalCheckFailedRequests&lt;/code&gt; bắn khi hai retry đâm vào cùng key — chính là dấu hiệu dedup đang hoạt động, không phải lỗi.&lt;/p&gt;

&lt;p&gt;Rule phòng ngừa cứng: (1) mọi endpoint có side effect ngoài (charge, ship, send) đều bắt buộc &lt;code&gt;Idempotency-Key&lt;/code&gt;, reject &lt;code&gt;400&lt;/code&gt; nếu thiếu; (2) key phải scope theo tenant; (3) TTL của store lớn hơn retry window dài nhất của client cộng biên an toàn; (4) mọi tích hợp với gateway ngoài forward key xuống downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoff
&lt;/h2&gt;

&lt;p&gt;Idempotency an toàn cho retry nhưng phải trả bằng &lt;strong&gt;state&lt;/strong&gt;: một bảng/DynamoDB store lớn dần theo QPS nhân với TTL, một extra write mỗi request, và độ phức tạp trong transaction boundary. Với endpoint không có side effect (GET, read query), thêm idempotency chỉ tốn tài nguyên vô ích — HTTP đã định nghĩa GET là idempotent theo spec, retry an toàn sẵn.&lt;/p&gt;

&lt;p&gt;Có hai điểm đánh đổi tinh tế hơn. Thứ nhất, &lt;strong&gt;strict dedup và conflict tolerance&lt;/strong&gt;: nếu chọn trả &lt;code&gt;409&lt;/code&gt; khi request đang in-flight, client phải biết cách backoff và thử lại — nhiều client library không xử lý &lt;code&gt;409&lt;/code&gt; khéo, sinh ra retry loop. Chọn "block chờ" thay vì &lt;code&gt;409&lt;/code&gt; thì đơn giản cho client nhưng chiếm connection của server lâu hơn. Thứ hai, &lt;strong&gt;key granularity&lt;/strong&gt;: key hẹp (mỗi request unique) an toàn nhưng dedup không dùng lại được; key rộng (theo business id như &lt;code&gt;order_id&lt;/code&gt;) tận dụng dedup tốt hơn nhưng phải chắc business id không được tái sử dụng.&lt;/p&gt;

&lt;p&gt;Rule of thumb: bất cứ endpoint nào tạo/charge/send/publish với side effect thấy được từ ngoài, mặc định coi là &lt;em&gt;phải&lt;/em&gt; idempotent, và bắt buộc key do client sinh, scope theo tenant, TTL lớn hơn retry window; endpoint read-only thì để nguyên vì HTTP method đã đảm bảo semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Câu hỏi phỏng vấn
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Thiết kế một payment endpoint idempotent để client retry không tạo charge trùng. Nêu cả cơ chế và các cách nó vẫn có thể vỡ.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Client sinh UUIDv4 cho mỗi intent "bấm nút Pay" và gửi qua header &lt;code&gt;Idempotency-Key&lt;/code&gt;, giữ nguyên key qua mọi lần retry cho tới khi có response terminal. Server có một bảng &lt;code&gt;idempotency_keys&lt;/code&gt; với primary key &lt;code&gt;(user_id, key)&lt;/code&gt;, unique constraint để hai retry song song không cùng insert được. Handler &lt;code&gt;INSERT ... ON CONFLICT DO NOTHING&lt;/code&gt; trong một transaction, nếu insert được thì chạy business logic (tạo charge, gọi payment gateway — forward luôn key xuống gateway để dedup xuyên tầng) và update row với response terminal trong cùng transaction. Nếu conflict, đọc row cũ, so &lt;code&gt;request_hash&lt;/code&gt; để chống reuse key với payload khác, rồi trả lại response cũ nếu đã terminal hoặc &lt;code&gt;409&lt;/code&gt; nếu đang in-flight. TTL của bảng phải lớn hơn retry window dài nhất của client hợp lệ. Điểm ăn điểm là gọi tên các failure mode: race condition khi check-then-act không atomic (phải dựa unique constraint hoặc distributed lock, không phải SELECT rồi INSERT); side effect ngoài transaction (email, publish event) phải đi qua transactional outbox chứ không gọi trực tiếp trong handler; key phải do client sinh chứ không phải server (không thì retry sinh key mới, mất dedup); và phải monitor duplicate rate cùng conditional-check-failed để biết dedup có thực sự hoạt động, vì hệ hỏng thường không log error mà chỉ lộ ra ở duplicate charge report từ support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on
&lt;/h2&gt;

&lt;p&gt;Dựng lại toàn bộ chain và ép nó vỡ, rồi sửa. Chạy trên máy local với Docker.&lt;/p&gt;

&lt;p&gt;Bước 1 — Postgres và schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; idem-pg &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pw &lt;span class="nt"&gt;-p&lt;/span&gt; 5432:5432 postgres:16
&lt;span class="nb"&gt;sleep &lt;/span&gt;3
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; idem-pg psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="sh"&gt;'
CREATE TABLE idempotency_keys (
  key TEXT NOT NULL,
  user_id BIGINT NOT NULL,
  request_hash TEXT NOT NULL,
  status TEXT NOT NULL,
  response_code INT,
  response_body JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (user_id, key)
);
CREATE TABLE charges (
  id BIGSERIAL PRIMARY KEY,
  user_id BIGINT NOT NULL,
  amount_cents INT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
&lt;/span&gt;&lt;span class="no"&gt;SQL
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bước 2 — server Node dùng đúng handler ở phần "Cơ chế hoạt động" phía trên (copy khối &lt;code&gt;app.post('/charges', ...)&lt;/code&gt;, bọc quanh bằng &lt;code&gt;express()&lt;/code&gt; + &lt;code&gt;pg.Pool&lt;/code&gt; nối tới &lt;code&gt;localhost:5432&lt;/code&gt;, thêm &lt;code&gt;sha256&lt;/code&gt; bằng &lt;code&gt;crypto.createHash('sha256')&lt;/code&gt;), rồi &lt;code&gt;node server.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Bước 3 — Test A, cùng key gửi 5 lần chỉ tạo 1 charge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;uuidgen&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in &lt;/span&gt;1 2 3 4 5&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST localhost:3000/charges &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"amount_cents": 1000}'&lt;/span&gt;
  &lt;span class="nb"&gt;echo
&lt;/span&gt;&lt;span class="k"&gt;done
&lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; idem-pg psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT count(*) FROM charges;"&lt;/span&gt;
&lt;span class="c"&gt;# expected: count = 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bước 4 — Test B, race condition song song cùng key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;uuidgen&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in &lt;/span&gt;1 2 3 4 5 6 7 8&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST localhost:3000/charges &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"amount_cents": 2000}'&lt;/span&gt; &amp;amp;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;wait
&lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; idem-pg psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT count(*) FROM charges WHERE amount_cents=2000;"&lt;/span&gt;
&lt;span class="c"&gt;# expected: 1 charge; 7 request kia trả 409 in-flight hoặc 200 replay&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bước 5 — Test C, payload mismatch reuse key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;uuidgen&lt;span class="si"&gt;)&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST localhost:3000/charges &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"amount_cents": 500}'&lt;/span&gt;
&lt;span class="nb"&gt;echo
&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST localhost:3000/charges &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"amount_cents": 999999}'&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;span class="c"&gt;# expected lần 2: 422 payload mismatch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bước 6 — ép nó vỡ để so sánh. Trong &lt;code&gt;server.js&lt;/code&gt;, thay khối &lt;code&gt;INSERT ... ON CONFLICT DO NOTHING&lt;/code&gt; bằng "&lt;code&gt;SELECT&lt;/code&gt; rồi nếu không có thì &lt;code&gt;INSERT INTO charges&lt;/code&gt;" (check-then-act không atomic). &lt;code&gt;TRUNCATE charges, idempotency_keys&lt;/code&gt; rồi chạy lại Test B — count sẽ lên 8: mất dedup, double (thực ra octuple) charge. Đây là failure mode 1.&lt;/p&gt;

&lt;p&gt;Bước 7 — thêm một counter Prometheus cho nhánh &lt;code&gt;ins.rowCount === 0&lt;/code&gt; (duplicate hit) và một cho nhánh 422 (payload mismatch). Chạy Test A + Test B + Test C xen kẽ, scrape &lt;code&gt;/metrics&lt;/code&gt;, xác nhận duplicate rate và payload mismatch rate khớp với hành vi test. Cuối cùng, thêm một background job &lt;code&gt;DELETE FROM idempotency_keys WHERE created_at &amp;lt; now() - interval '10 seconds'&lt;/code&gt; chạy mỗi 5 giây; gửi request với key &lt;code&gt;K&lt;/code&gt;, đợi 20 giây rồi retry cùng key &lt;code&gt;K&lt;/code&gt; — kết quả sẽ tạo charge thứ hai, tái hiện failure mode 6 "TTL cắt trước retry window".&lt;/p&gt;

</description>
      <category>consistency</category>
      <category>idempotency</category>
      <category>saferetry</category>
      <category>queue</category>
    </item>
    <item>
      <title>Rate Limiting — Throttling</title>
      <dc:creator>Nghề không nhiều chẳng sắc</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:40:37 +0000</pubDate>
      <link>https://dev.to/nguoidungai/rate-limiting-throttling-3pl1</link>
      <guid>https://dev.to/nguoidungai/rate-limiting-throttling-3pl1</guid>
      <description>&lt;h1&gt;
  
  
  Throttling: vì sao in-memory rate limit "biến mất" sau khi scale ngang, và chọn token bucket hay sliding window
&lt;/h1&gt;

&lt;p&gt;Throttling là cơ chế giới hạn số request một client (user, IP, API key, tenant) được xử lý trong một khoảng thời gian, để chống abuse, bảo vệ downstream, và phân bổ công bằng dung lượng service. Định nghĩa nghe đơn giản, nhưng lý do dev gặp nó trong việc thật lại rất cụ thể: sau khi scale service từ 1 pod lên 8 pod, cùng cấu hình "100 req/min mỗi user" đột nhiên trở thành 800 req/min thực tế — vì mỗi pod đếm riêng trong RAM, và load balancer rải request đều tám hướng. Rate limit vẫn "chạy", log không có lỗi, nhưng downstream vẫn bị flood. Đó là failure mode dẫn tới việc phải chuyển counter sang store phân tán, và kèm theo là câu hỏi chọn algorithm nào — token bucket, sliding window, hay leaky bucket — mỗi cái đánh đổi khác nhau.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cơ chế hoạt động
&lt;/h2&gt;

&lt;p&gt;Bốn thuật toán phổ biến, khác nhau ở cách đếm và cách xử lý burst.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixed window counter.&lt;/strong&gt; Chia thời gian thành khung cố định (mỗi phút bắt đầu tại giây 0). Mỗi request &lt;code&gt;INCR&lt;/code&gt; một key &lt;code&gt;rl:{user}:{minute}&lt;/code&gt;, nếu counter vượt limit thì reject. Đơn giản nhất, một &lt;code&gt;INCR&lt;/code&gt; + &lt;code&gt;EXPIRE&lt;/code&gt; trên Redis là xong. Nhược điểm cứng: tại biên khung có thể chịu &lt;strong&gt;gấp đôi limit&lt;/strong&gt; trong một cửa sổ trượt — user gửi 100 req vào giây 59 của phút 12:00, rồi 100 req vào giây 01 của phút 12:01, tức 200 req trong 2 giây thật, trong khi limit là 100/phút.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sliding window log.&lt;/strong&gt; Lưu timestamp của từng request trong sorted set, mỗi request &lt;code&gt;ZADD&lt;/code&gt; + &lt;code&gt;ZREMRANGEBYSCORE&lt;/code&gt; xoá các entry cũ hơn &lt;code&gt;now - window&lt;/code&gt;, rồi &lt;code&gt;ZCARD&lt;/code&gt; để đếm. Chính xác tuyệt đối nhưng tốn bộ nhớ tuyến tính theo số request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sliding window counter.&lt;/strong&gt; Cách Cloudflare mô tả trên engineering blog: giữ counter của khung hiện tại và khung trước, ước lượng lượng request trong cửa sổ trượt bằng nội suy có trọng số theo phần trăm khung trước còn nằm trong window. Chỉ tốn hai counter, sai số rất nhỏ so với log thuần, và không có failure mode biên như fixed window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token bucket.&lt;/strong&gt; Bucket có capacity &lt;code&gt;B&lt;/code&gt; token, refill với tốc độ &lt;code&gt;r&lt;/code&gt; token/s. Mỗi request tiêu một token; hết token thì reject. Cho phép &lt;strong&gt;burst&lt;/strong&gt; tới &lt;code&gt;B&lt;/code&gt; khi bucket đầy, nhưng steady-state trung bình vẫn bằng &lt;code&gt;r&lt;/code&gt;. Đây là thuật toán Stripe mô tả trong bài "Scaling your API with rate limiters" và cũng là gốc của GCRA (Generic Cell Rate Algorithm) mà &lt;code&gt;redis-cell&lt;/code&gt; module implement dưới lệnh &lt;code&gt;CL.THROTTLE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Bản token bucket phân tán trên Redis phải atomic — không dùng &lt;code&gt;GET&lt;/code&gt; rồi &lt;code&gt;SET&lt;/code&gt; (race condition kinh điển). Dùng Lua script để read-modify-write trong một round trip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- token_bucket.lua — KEYS[1]=bucket key, ARGV: capacity, refill_rate, now_ms, cost&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;-- tokens/sec&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;-- ms&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'HMGET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'tokens'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ts'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;

&lt;span class="c1"&gt;-- refill dựa trên khoảng thời gian trôi qua&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;math.max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;math.min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;
  &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'HMSET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'tokens'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- TTL đủ dài để refill từ empty về full, +biên&lt;/span&gt;
&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PEXPIRE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;math.ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client gọi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node + ioredis; script được EVALSHA sau khi SCRIPT LOAD một lần&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evalsha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;SHA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`rl:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;// cap=100, 10 tok/s&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Retry-After&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rate_limited&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hai điểm mấu chốt: (1) toàn bộ logic chạy trong Redis dưới dạng script atomic — không có window giữa GET và SET cho hai request cùng chạy race; (2) &lt;code&gt;PEXPIRE&lt;/code&gt; đảm bảo key idle được thu hồi, nếu không mỗi user id sinh một key vĩnh viễn và Redis phình memory. Với sliding window counter thì cùng nguyên tắc: một Lua script đọc counter hai khung, tính weighted sum, &lt;code&gt;INCR&lt;/code&gt; khung hiện tại nếu allow.&lt;/p&gt;

&lt;p&gt;Trong HTTP, response reject trả &lt;code&gt;429 Too Many Requests&lt;/code&gt; (RFC 6585) kèm &lt;code&gt;Retry-After&lt;/code&gt; (giây hoặc HTTP date) và thường thêm &lt;code&gt;X-RateLimit-Limit&lt;/code&gt;, &lt;code&gt;X-RateLimit-Remaining&lt;/code&gt;, &lt;code&gt;X-RateLimit-Reset&lt;/code&gt; — pattern GitHub API dùng, nhiều SDK tự parse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vấn đề gặp trong production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 1: rate limit in-memory không chia sẻ khi scale ngang.&lt;/strong&gt; Đây là failure mode kinh điển và cũng là lý do đề bài luôn quay lại "distributed store". App dùng &lt;code&gt;express-rate-limit&lt;/code&gt; mặc định (in-memory Map) hoặc một object counter tự viết:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sai — mỗi pod đếm riêng&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;counters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// userId -&amp;gt; { count, resetAt }&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;resetAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resetAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resetAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chạy 1 pod thì đúng. Scale lên N pod sau load balancer round-robin: total budget cho một user trở thành &lt;code&gt;N × 100&lt;/code&gt;/phút, và không có cách nào phía app phát hiện vì mỗi pod thấy counter riêng vẫn dưới ngưỡng. Downstream (DB, third-party API) nhận flood, mọi user khác chậm theo. Bản đúng là đưa counter ra store dùng chung — Redis là mặc định, hoặc dùng một sidecar/service ratelimit chuyên biệt như Envoy &lt;code&gt;ratelimit&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// đúng — Redis-backed token bucket atomic&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evalsha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SHA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`rl:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Retry-After&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure mode 2: race condition khi implement token bucket bằng GET + SET.&lt;/strong&gt; Một variation tinh vi: dev biết cần Redis, nhưng viết logic ở app layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sai — TOCTOU giữa GET và SET&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`rl:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`rl:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hai request song song đều đọc &lt;code&gt;tokens=1&lt;/code&gt;, cả hai đều pass check, cả hai đều set về &lt;code&gt;0&lt;/code&gt; — một token bị tiêu hai lần. Với QPS cao, sai lệch tích lại đáng kể. Sửa bằng &lt;code&gt;INCR&lt;/code&gt;/&lt;code&gt;DECR&lt;/code&gt; (atomic single-op) cho fixed window, hoặc Lua script cho logic phức tạp hơn như token bucket/sliding window. &lt;code&gt;WATCH&lt;/code&gt;/&lt;code&gt;MULTI&lt;/code&gt; cũng làm được nhưng cost cao hơn Lua vì có retry loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 3: fixed window boundary burst.&lt;/strong&gt; Cấu hình "100 req/phút", user gửi 100 req vào 12:00:59.9 và 100 req nữa vào 12:01:00.1 — cùng cửa sổ trượt 2 giây nhận 200 req. Nếu downstream sensitive với burst (payment provider, DB có connection pool nhỏ), số spike thật cao hơn expected 2x. Chuyển sang sliding window counter hoặc token bucket triệt tiêu vấn đề này. Nếu vẫn muốn giữ fixed window vì đơn giản, thêm một token bucket capacity nhỏ chạy song song để chặn burst (kiểu "sustained rate + burst limit" — Stripe dùng nhiều tầng như thế).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 4: Redis là single point of failure cho rate limit.&lt;/strong&gt; Khi Redis down, mọi call &lt;code&gt;EVALSHA&lt;/code&gt; fail. Hai lựa chọn phải quyết trước:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fail-open&lt;/strong&gt;: nếu Redis lỗi, cho qua. An toàn cho availability nhưng bỏ trống chống abuse trong khoảng lỗi — attacker biết pattern có thể ép Redis chậm để né limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail-closed&lt;/strong&gt;: nếu Redis lỗi, reject. An toàn chống abuse nhưng biến sự cố Redis thành sự cố toàn service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Production thường fail-open có timeout ngắn (vài chục ms) kèm alert, cộng thêm một layer L7 (Nginx &lt;code&gt;limit_req&lt;/code&gt;, Envoy local rate limit) chạy per-instance với ngưỡng rộng như safety net:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;race&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evalsha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SHA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;rj&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rl_timeout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ratelimit.fail_open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// alert khi metric này &amp;gt; 0&lt;/span&gt;
  &lt;span class="c1"&gt;// cho qua, fallback layer ngoài sẽ chặn abuse cực đoan&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure mode 5: hot key trên Redis do một tenant lớn.&lt;/strong&gt; Rate limit key cho một API key traffic cao (crawler, mobile client phổ biến) trở thành hot key — mọi request đều &lt;code&gt;EVALSHA&lt;/code&gt; vào cùng một slot Redis, một node cluster gánh nặng bất đối xứng. Sharding rate limit theo user cứu được phần lớn user, nhưng không giải quyết tenant khổng lồ. Cách xử lý: shard key thêm theo bucket con (&lt;code&gt;rl:{user}:{bucket=hash(req_id)%K}&lt;/code&gt;) rồi cộng dồn, hoặc dùng thuật toán probabilistic (approximated counting) để tolerate sai số nhỏ đổi lấy phân tải. Với API gateway thật (Envoy &lt;code&gt;ratelimit&lt;/code&gt;, Kong), cấu hình local rate limit trước global rate limit để cắt phần lớn traffic ngay tại pod, chỉ traffic còn lại mới hỏi service tập trung.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 6: không phân biệt được ai đang bị limit.&lt;/strong&gt; Rate limit theo IP đơn thuần đụng NAT (một văn phòng hàng trăm user chung một IP public bị limit như một user), hoặc đụng CDN/proxy (mọi request đến từ IP CDN). Cấu hình &lt;code&gt;limit_req_zone&lt;/code&gt; của Nginx theo &lt;code&gt;$binary_remote_addr&lt;/code&gt; mà không set &lt;code&gt;real_ip_header&lt;/code&gt; đúng sẽ throttle theo IP CDN. Với API cần authenticated, key limit theo &lt;code&gt;user_id&lt;/code&gt; hoặc &lt;code&gt;api_key&lt;/code&gt;; với public endpoint, kết hợp IP + fingerprint (User-Agent + Accept-Language + đặc trưng TLS) để tránh cả false positive lẫn false negative. Trust source của IP thật (header &lt;code&gt;X-Forwarded-For&lt;/code&gt; phải được validate từ proxy tin cậy, không phải từ client tuỳ ý).&lt;/p&gt;

&lt;h2&gt;
  
  
  Cách debug và monitor
&lt;/h2&gt;

&lt;p&gt;Triệu chứng chính khi rate limit sai: (a) downstream vẫn thấy tải cao hơn expected dù rate limit "đang chạy" — dấu hiệu multi-instance drift hoặc race condition; (b) reject rate tăng đột biến cho một nhóm user hợp pháp — dấu hiệu key sai (limit theo IP mà user sau NAT chung), hoặc ngưỡng quá thấp so với UX thật; (c) reject rate = 0 kéo dài trong khi log downstream có timeout — rate limit không hiệu lực (fail-open đang che, hoặc key không match).&lt;/p&gt;

&lt;p&gt;Metric bắt buộc:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reject rate&lt;/strong&gt; (số 429 / tổng request) tổng, và slice theo tenant/route. Đây là chỉ số hàng đầu — reject rate 0% nghĩa là limit chưa bao giờ kích, ngưỡng có thể quá cao (hoặc code không gọi tới limiter); reject rate &amp;gt; vài phần trăm liên tục nghĩa là ngưỡng quá thấp hoặc có abuse thật.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top rejected keys&lt;/strong&gt; — user_id/api_key/IP bị reject nhiều nhất. Phân biệt "một attacker bị chặn" (đúng) vs "top là user hợp pháp" (ngưỡng sai).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis latency của rate limit ops&lt;/strong&gt; — &lt;code&gt;redis-cli --latency&lt;/code&gt; hoặc metric client (ioredis/lettuce exporter). Latency tăng là dấu hiệu hot key hoặc script chậm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail-open counter&lt;/strong&gt; — mọi lần Redis timeout mà rate limit cho qua đều tăng counter này; alert khi &amp;gt; 0 kéo dài.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;429 response headers accuracy&lt;/strong&gt; — kiểm tra &lt;code&gt;X-RateLimit-Remaining&lt;/code&gt; giảm đúng nhịp bằng cách bắn traffic có kiểm soát và so; nếu client thấy &lt;code&gt;Remaining&lt;/code&gt; không giảm nhưng vẫn bị 429, header sinh sai.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Công cụ chẩn đoán: &lt;code&gt;redis-cli MONITOR&lt;/code&gt; để xem lệnh thật đang chạy (không dùng lâu vì tốn CPU Redis); &lt;code&gt;redis-cli --hotkeys&lt;/code&gt; (Redis phải bật &lt;code&gt;maxmemory-policy allkeys-lfu&lt;/code&gt;) để tìm hot key; &lt;code&gt;SLOWLOG GET&lt;/code&gt; để xem script Lua chậm; &lt;code&gt;CLIENT LIST&lt;/code&gt; xem connection distribution. Với Envoy &lt;code&gt;ratelimit&lt;/code&gt;, &lt;code&gt;ratelimit_service.total_hits&lt;/code&gt; và &lt;code&gt;ratelimit_service.over_limit&lt;/code&gt; là hai metric chính. Với Nginx &lt;code&gt;limit_req&lt;/code&gt;, log format ghi &lt;code&gt;$limit_req_status&lt;/code&gt; cho biết một request được PASSED, DELAYED, hay REJECTED.&lt;/p&gt;

&lt;p&gt;Rule phòng ngừa: (1) không bao giờ dùng counter in-memory cho rate limit ở service scale ngang — mặc định là distributed store; (2) mọi read-modify-write phải atomic (Lua script hoặc single-op như &lt;code&gt;INCR&lt;/code&gt;); (3) chọn key limit theo entity thực sự cần chống abuse (user_id / api_key), không mặc định IP; (4) chạy hai tầng — local burst limit tại proxy + global sustained limit qua Redis — để cắt phần lớn traffic trước khi tới store trung tâm; (5) load test rate limit path chính với QPS thật để verify không sai số dưới tải.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoff
&lt;/h2&gt;

&lt;p&gt;Rate limit chống abuse, bảo vệ downstream, phân bổ công bằng — nhưng mọi lựa chọn implementation đều đánh đổi.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixed window&lt;/strong&gt;: rẻ nhất (một &lt;code&gt;INCR&lt;/code&gt;), memory ít nhất (một key per user per window), nhưng chịu boundary burst 2x. Phù hợp cho limit "thô" chống abuse thô bạo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sliding window log&lt;/strong&gt;: chính xác nhất, không có burst effect, nhưng memory tuyến tính theo số request — không dùng được khi limit lớn và traffic cao.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sliding window counter&lt;/strong&gt;: gần chính xác như log, memory bằng fixed window (hai counter), sai số vài phần trăm. Sweet spot cho phần lớn use case API rate limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token bucket / GCRA&lt;/strong&gt;: cho phép burst có kiểm soát (&lt;code&gt;capacity&lt;/code&gt;), steady-state chính xác, tự nhiên khớp với "user thi thoảng bắn liên tiếp là hợp lệ". Tốn hơn fixed window (Lua script) nhưng đáng.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local vs distributed store&lt;/strong&gt;: local (in-memory hoặc Nginx &lt;code&gt;limit_req_zone&lt;/code&gt; per-worker) nhanh nhất, không network hop, nhưng sai số N-instance drift. Distributed (Redis-backed) chính xác toàn cục, nhưng thêm latency mỗi request (thường vài ms) và Redis thành dependency phải HA + monitor. Hybrid — local burst cap + global sustained — là kiến trúc Stripe và Envoy khuyến nghị.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fail-open vs fail-closed&lt;/strong&gt; khi store lỗi: fail-open ưu tiên availability, hy sinh chống abuse; fail-closed ngược lại. Production thường fail-open với timeout ngắn + safety net local + alert.&lt;/p&gt;

&lt;p&gt;Rule of thumb: mặc định là &lt;strong&gt;sliding window counter hoặc token bucket, backed bằng Redis + Lua script atomic, key theo entity thật (user_id/api_key), fail-open với timeout ~50ms và alert, kèm một local burst cap tại proxy&lt;/strong&gt;. Chỉ chọn fixed window khi biết chắc chấp nhận được biên burst, và chỉ giữ in-memory khi service dứt khoát single-instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Câu hỏi phỏng vấn
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Token bucket và sliding window khác nhau thế nào, và khi scale ngang thì rate limit thay đổi ra sao?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Token bucket có một bucket capacity &lt;code&gt;B&lt;/code&gt; refill với rate &lt;code&gt;r&lt;/code&gt; token/s: mỗi request tiêu một token, hết token thì reject. Nó cho phép &lt;strong&gt;burst&lt;/strong&gt; tới &lt;code&gt;B&lt;/code&gt; khi bucket đầy (user vừa mới im lặng lâu có thể bắn nhanh một loạt), còn steady-state trung bình vẫn bằng &lt;code&gt;r&lt;/code&gt;. Sliding window (counter) đếm số request trong một cửa sổ trượt liên tục — không có khái niệm burst credit, hai request cách nhau đúng bằng &lt;code&gt;1/limit&lt;/code&gt; là ngưỡng. Token bucket phù hợp khi user thật hay có bursty traffic hợp lệ (mobile app đồng bộ khi mở, batch script); sliding window phù hợp khi cần rate đều tay để bảo vệ downstream nhạy cảm với spike. Fixed window là cách rẻ nhất nhưng chịu boundary burst 2x, sliding window counter (như Cloudflare mô tả) khắc phục bằng nội suy hai khung.&lt;/p&gt;

&lt;p&gt;Khi scale ngang từ 1 lên N pod, rate limit in-memory trở thành &lt;code&gt;N × limit&lt;/code&gt; thực tế vì mỗi pod đếm riêng — đây là failure mode cần xử lý bằng distributed store. Cách chuẩn là đưa counter vào Redis với &lt;strong&gt;atomic op&lt;/strong&gt; (INCR cho fixed window, hoặc Lua script cho token bucket/sliding window) để tránh race giữa GET và SET. Điểm ăn điểm là gọi được thêm: (1) Redis biến rate limit thành dependency, cần fail-open + timeout ngắn + alert để không biến sự cố Redis thành sự cố cả service; (2) hot key khi một tenant lớn dồn về một Redis slot — giải bằng shard con hoặc hai tầng (local burst limit + global limit); (3) chọn entity limit đúng (user_id/api_key, không phải IP thuần vì NAT/CDN); (4) monitor bằng reject rate + top rejected keys + Redis latency + fail-open counter, không chỉ "có 429 hay không".&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on
&lt;/h2&gt;

&lt;p&gt;Dựng Redis + hai instance Node app, đo hiệu ứng multi-instance drift với in-memory limiter, rồi chuyển sang distributed token bucket qua Lua script và xác nhận rate thực sự đúng ngưỡng.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; rl-redis &lt;span class="nt"&gt;-p&lt;/span&gt; 6379:6379 redis:7-alpine
&lt;span class="nb"&gt;mkdir &lt;/span&gt;throttle-lab &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;throttle-lab
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm i express ioredis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo &lt;code&gt;app-inmem.js&lt;/code&gt; (bản sai — in-memory Map):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app-inmem.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;counters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;WINDOW_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;  &lt;span class="c1"&gt;// 10 req / 10s / user&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resetAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;resetAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;WINDOW_MS&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
  &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;LIMIT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rate_limited&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo &lt;code&gt;token_bucket.lua&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'HMGET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'t'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ts'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;math.min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;math.max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'HMSET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'t'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PEXPIRE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;math.ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cap&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;tostring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo &lt;code&gt;app-redis.js&lt;/code&gt; (bản đúng — distributed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app-redis.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ioredis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6379&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SCRIPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./token_bucket.lua&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;SHA&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CAP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;// capacity 10, refill 1 token/s&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;race&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evalsha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SHA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`rl:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CAP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;rj&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rl_timeout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Retry-After&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rate_limited&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fail_open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;degraded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="p"&gt;;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;SHA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LOAD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SCRIPT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listening&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;})()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chạy hai instance bản in-memory và bắn traffic rải đều hai port để mô phỏng load balancer round-robin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3001 node app-inmem.js &amp;amp;
&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3002 node app-inmem.js &amp;amp;
&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="c"&gt;# 40 req cùng uid=alice, LIMIT là 10/10s&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 40&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;i &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;3001 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;3002&lt;span class="si"&gt;)&lt;/span&gt;
  curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code} "&lt;/span&gt; &lt;span class="s2"&gt;"http://127.0.0.1:&lt;/span&gt;&lt;span class="nv"&gt;$port&lt;/span&gt;&lt;span class="s2"&gt;/api?uid=alice"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;span class="c"&gt;# Kỳ vọng: mỗi pod cho qua ~10 → tổng ~20 pass thay vì đúng 10&lt;/span&gt;
&lt;span class="c"&gt;# → chính là multi-instance drift, budget thực = N × config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kill hai process và chạy bản Redis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; %1 %2 2&amp;gt;/dev/null
&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3001 node app-redis.js &amp;amp;
&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3002 node app-redis.js &amp;amp;
&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 40&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;i &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;3001 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;3002&lt;span class="si"&gt;)&lt;/span&gt;
  curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code} "&lt;/span&gt; &lt;span class="s2"&gt;"http://127.0.0.1:&lt;/span&gt;&lt;span class="nv"&gt;$port&lt;/span&gt;&lt;span class="s2"&gt;/api?uid=alice"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;span class="c"&gt;# Kỳ vọng: đúng ~10 pass 200 (capacity bucket), phần còn lại 429&lt;/span&gt;
&lt;span class="c"&gt;# bất kể request rơi vào pod nào&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quan sát Redis state trong lúc chạy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; rl-redis redis-cli
&lt;span class="c"&gt;# &amp;gt; HGETALL rl:alice&lt;/span&gt;
&lt;span class="c"&gt;# &amp;gt; MONITOR      # xem EVALSHA thực tế của cả hai pod bắn vào cùng key&lt;/span&gt;
&lt;span class="c"&gt;# &amp;gt; SLOWLOG GET 10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test hot key bằng &lt;code&gt;redis-benchmark&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; rl-redis redis-benchmark &lt;span class="nt"&gt;-n&lt;/span&gt; 100000 &lt;span class="nt"&gt;-c&lt;/span&gt; 50 &lt;span class="nt"&gt;-t&lt;/span&gt; evalsha &lt;span class="nt"&gt;-r&lt;/span&gt; 1
&lt;span class="c"&gt;# so với: docker exec -it rl-redis redis-benchmark -n 100000 -c 50 -t evalsha -r 1000&lt;/span&gt;
&lt;span class="c"&gt;# key range 1 vs 1000 cho khác biệt latency rõ — đó là hot-key effect&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test fail-open: dừng Redis khi app đang chạy và xác nhận request vẫn được trả (kèm &lt;code&gt;degraded: true&lt;/code&gt;), rồi start lại Redis và verify limit lại kích hoạt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stop rl-redis
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"http://127.0.0.1:3001/api?uid=alice"&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;degraded
&lt;span class="c"&gt;# expected: {"ok":true,"port":"3001","degraded":true}&lt;/span&gt;
docker start rl-redis
&lt;span class="nb"&gt;sleep &lt;/span&gt;2
&lt;span class="c"&gt;# gọi liên tiếp 20 lần, phần lớn phải 429 vì bucket đã cạn&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 20&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code} "&lt;/span&gt; &lt;span class="s2"&gt;"http://127.0.0.1:3001/api?uid=alice"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cuối cùng, so sánh boundary burst giữa fixed window (thay Lua bằng &lt;code&gt;INCR&lt;/code&gt; + &lt;code&gt;EXPIRE 60&lt;/code&gt;) và token bucket bằng cách bắn 100 request vào giây thứ 59 rồi 100 request nữa vào giây thứ 61 — fixed window cho qua tất cả 200 trong 2 giây thật, token bucket cắt đúng theo capacity + refill. Đó là bằng chứng nhìn thấy được cho tradeoff algorithm ở phần Tradeoff phía trên.&lt;/p&gt;

</description>
      <category>ratelimit</category>
      <category>ratelimiting</category>
      <category>throttling</category>
      <category>caching</category>
    </item>
    <item>
      <title>Message Queue — Async Processing</title>
      <dc:creator>Nghề không nhiều chẳng sắc</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:40:33 +0000</pubDate>
      <link>https://dev.to/nguoidungai/message-queue-async-processing-4g8h</link>
      <guid>https://dev.to/nguoidungai/message-queue-async-processing-4g8h</guid>
      <description>&lt;h1&gt;
  
  
  Async processing qua message queue: vì sao đẩy việc nặng ra khỏi request path, và cái giá phải trả bằng eventual consistency
&lt;/h1&gt;

&lt;p&gt;Async processing là mô hình tách một request thành hai giai đoạn: request handler nhận việc, xác nhận với client, rồi giao phần xử lý thật cho một worker chạy ngoài request path — thường qua một message queue (RabbitMQ, AWS SQS, Kafka, Redis Streams, hoặc queue trên nền Redis như BullMQ/Sidekiq). Lý do dev gặp nó trong việc thật rất cụ thể: một endpoint gọi payment provider mất 3s, gửi email confirm mất 1s, resize ảnh mất 5s — nếu làm tuần tự trong request, p99 latency của endpoint là tổng các con số đó, và một downstream chậm hoặc chết đủ để làm timeout hết thread pool của app server. Đẩy vào queue thì request trả về trong vài chục ms; nhưng đổi lại, cái "xong" mà client thấy không còn nghĩa là việc đã thực sự hoàn thành.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cơ chế hoạt động
&lt;/h2&gt;

&lt;p&gt;Ba thành phần: &lt;strong&gt;producer&lt;/strong&gt; (thường là API server) đóng gói việc thành message rồi publish vào broker; &lt;strong&gt;broker&lt;/strong&gt; (RabbitMQ/SQS/Kafka…) giữ message trong queue có persistence tuỳ cấu hình; &lt;strong&gt;consumer/worker&lt;/strong&gt; poll hoặc được push message, xử lý, rồi ack để broker biết xoá. Nếu worker chết trước khi ack, broker redeliver — đây là gốc của semantic &lt;strong&gt;at-least-once&lt;/strong&gt;: mỗi message được giao &lt;em&gt;ít nhất&lt;/em&gt; một lần, có thể nhiều lần. Exactly-once trong hệ phân tán chỉ đạt được ở lớp application bằng cách consumer viết idempotent, không phải bằng cấu hình broker.&lt;/p&gt;

&lt;p&gt;Ví dụ với RabbitMQ + Node (&lt;code&gt;amqplib&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// producer — trong HTTP handler&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createConfirmChannel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image.resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;durable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;s3Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendToQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image.resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;persistent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// ghi xuống disk, sống sót broker restart&lt;/span&gt;
    &lt;span class="na"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// để consumer dedupe&lt;/span&gt;
    &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="c1"&gt;// đợi broker confirm đã nhận (publisher confirm)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForConfirms&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="c1"&gt;// 202 Accepted — chưa xong, đã nhận&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// consumer — process riêng&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createChannel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prefetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="c1"&gt;// chỉ giữ 8 message unacked mỗi consumer&lt;/span&gt;
&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image.resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;resizeAndUpload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// việc thật&lt;/span&gt;
    &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// requeue = false → gửi sang dead-letter exchange đã config&lt;/span&gt;
    &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ba chi tiết dễ bỏ sót nhưng quyết định correctness trong production: &lt;code&gt;persistent: true&lt;/code&gt; + queue &lt;code&gt;durable: true&lt;/code&gt; để message sống sót broker restart; &lt;code&gt;publisher confirm&lt;/code&gt; để producer biết broker đã nhận (không có nó thì &lt;code&gt;sendToQueue&lt;/code&gt; là fire-and-forget); &lt;code&gt;prefetch&lt;/code&gt; để giới hạn số message một consumer giữ, nếu không một consumer chậm sẽ ôm cả queue.&lt;/p&gt;

&lt;p&gt;Ngoài "job queue" cổ điển (một producer → một queue → worker pool cạnh tranh), còn hai mô hình hay dùng: &lt;strong&gt;pub/sub fanout&lt;/strong&gt; (một message được nhân bản tới nhiều queue, mỗi consumer group xử lý độc lập — RabbitMQ fanout exchange, Kafka topic với nhiều consumer group) để cùng một event trigger nhiều side-effect độc lập, và &lt;strong&gt;log-based queue&lt;/strong&gt; (Kafka, Redis Streams) nơi message không bị xoá sau khi ack mà tồn theo offset, cho phép replay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vấn đề gặp trong production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 1: xử lý đồng bộ trong request path.&lt;/strong&gt; Endpoint đăng ký tài khoản gọi tuần tự: ghi DB, gọi Stripe tạo customer, gửi welcome email qua SendGrid, đẩy analytics event. Tổng latency là tổng của bốn call, và bất kỳ call nào chậm/timeout đều làm cả request fail dù DB đã ghi xong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sai — mọi side-effect chặn response&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/signup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;    &lt;span class="c1"&gt;// 500ms–timeout&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sendgrid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;welcomeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                 &lt;span class="c1"&gt;// vài trăm ms&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="c1"&gt;// vài chục ms&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// p99 = tổng, và fail-any-fail-all&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hậu quả: khi SendGrid chậm, endpoint timeout dù user đã được tạo — client retry, sinh duplicate user; app server thread pool bị các request treo giữ, các endpoint khác cũng chậm theo. Bản đúng là cắt bốn công việc thành bốn message, request chỉ ghi DB + enqueue rồi trả về:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/signup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// enqueue trong cùng transaction (outbox pattern) — xem failure mode 4&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;billing.create_customer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email.send_welcome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;analytics.signup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// trả về ngay, worker lo phần còn lại&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure mode 2: queue tăng không kiểm soát khi producer nhanh hơn consumer.&lt;/strong&gt; Đây là failure mode kinh điển của async processing. Traffic đột biến, producer publish nhiều msg/s, worker pool xử lý chậm hơn — depth tăng tuyến tính. Vì worker vẫn "chạy bình thường" và không có exception, nhiều team không phát hiện cho tới khi user báo "email confirm tới sau vài tiếng" hoặc broker chạm giới hạn memory. RabbitMQ trong tình huống này kích &lt;strong&gt;flow control&lt;/strong&gt;: publisher bị chặn để bảo vệ broker; nếu queue có &lt;code&gt;max-length&lt;/code&gt; thì message cũ hoặc mới bị drop tuỳ policy. Cách xử lý: alert trên queue depth và trên &lt;strong&gt;oldest message age&lt;/strong&gt; (không chỉ depth — depth ổn định vẫn có thể che một backlog cũ nếu message mới liên tục vượt lên); autoscale worker theo depth; và dùng &lt;strong&gt;backpressure&lt;/strong&gt; ở producer (nếu depth vượt ngưỡng, degrade sang synchronous hoặc từ chối request non-critical).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 3: poison message làm kẹt queue.&lt;/strong&gt; Một message có payload sai schema, worker throw, không ack, broker redeliver, worker throw tiếp — vòng lặp vô hạn. Nếu consumer requeue vào đầu queue, cùng message chặn mọi message sau nó:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sai — requeue vô điều kiện&lt;/span&gt;
&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// requeue = true → poison loop&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bản đúng: đếm redelivery, sau N lần đẩy sang &lt;strong&gt;dead-letter queue&lt;/strong&gt; (DLQ) để xử lý thủ công, không chặn hàng chính. RabbitMQ config &lt;code&gt;x-dead-letter-exchange&lt;/code&gt; trên queue; SQS có &lt;code&gt;RedrivePolicy&lt;/code&gt; với &lt;code&gt;maxReceiveCount&lt;/code&gt;; Kafka không có DLQ built-in nhưng convention là producer publish message hỏng sang topic &lt;code&gt;*.dlq&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 4: mất message vì dual-write giữa DB và queue.&lt;/strong&gt; Handler ghi DB rồi publish message; giữa hai bước, process crash — DB có record, queue không có event. Hoặc ngược lại: publish trước, ghi DB fail — event tồn tại cho một record không có thật. Đây là bài toán dual-write không thể giải bằng try/catch. Giải pháp production-grade là &lt;strong&gt;transactional outbox&lt;/strong&gt;: trong cùng DB transaction với business write, insert một row vào bảng &lt;code&gt;outbox&lt;/code&gt;; một relay process (hoặc CDC như Debezium đọc WAL) đọc bảng đó và publish sang queue, đánh dấu row đã xử lý. Nếu crash, relay tiếp tục từ row chưa publish — không mất, có thể duplicate (idempotent consumer xử lý).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 5: consumer không idempotent.&lt;/strong&gt; Vì delivery là at-least-once, một message có thể được xử lý hai lần (worker crash sau khi làm việc nhưng trước khi ack, network partition khiến ack không tới broker, redeliver sau visibility timeout của SQS…). Nếu handler tính phí, mỗi lần duplicate là một lần trừ tiền:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sai — charge mỗi lần message tới&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;// duplicate = double charge&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// đúng — dedupe theo idempotency key ổn định&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// Stripe dedupe phía server&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Với side-effect nội bộ (không có provider dedupe), dùng bảng &lt;code&gt;processed_jobs(job_id primary key)&lt;/code&gt; insert-if-not-exists trong cùng transaction với business write; nếu insert vi phạm primary key thì bỏ qua message. Message id (RabbitMQ &lt;code&gt;messageId&lt;/code&gt;, SQS &lt;code&gt;MessageId&lt;/code&gt;, Kafka &lt;code&gt;key+offset&lt;/code&gt;) làm idempotency key ổn định.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 6: visibility timeout / ack timeout ngắn hơn thời gian xử lý.&lt;/strong&gt; SQS có visibility timeout mặc định thấp hơn nhiều so với job nặng; nếu job resize ảnh mất lâu hơn ngưỡng, SQS coi worker đã chết và redeliver — hai worker cùng xử lý cùng message. Tương tự RabbitMQ có consumer timeout mặc định (broker huỷ channel nếu delivery không ack trong ngưỡng). Cách xử lý: đặt visibility timeout dài hơn p99 xử lý cộng biên; với job dài không lường trước, extend visibility định kỳ trong khi vẫn chạy (SQS &lt;code&gt;ChangeMessageVisibility&lt;/code&gt;, Kafka &lt;code&gt;heartbeat&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Cách debug và monitor
&lt;/h2&gt;

&lt;p&gt;Triệu chứng backlog: &lt;strong&gt;queue depth&lt;/strong&gt; tăng đơn điệu theo thời gian; &lt;strong&gt;oldest message age&lt;/strong&gt; vượt SLO của loại việc đó (email confirm tới sau vài phút là bất thường); consumer log không có error nhưng throughput giữ nguyên trong khi depth tăng — nghĩa là producer &amp;gt; consumer. Triệu chứng poison: một message id lặp lại nhiều lần trong log kèm stack trace giống hệt; DLQ depth tăng dần trong khi main queue chạy bình thường. Triệu chứng duplicate: business metric (số email gửi, số charge) cao hơn số job enqueue.&lt;/p&gt;

&lt;p&gt;Metric bắt buộc theo dõi:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Queue depth / backlog&lt;/strong&gt; — số message ready. RabbitMQ: &lt;code&gt;rabbitmqctl list_queues name messages messages_ready messages_unacknowledged&lt;/code&gt; hoặc endpoint &lt;code&gt;/api/queues&lt;/code&gt; của management plugin; scrape bằng &lt;code&gt;rabbitmq_prometheus&lt;/code&gt; plugin. SQS: CloudWatch &lt;code&gt;ApproximateNumberOfMessagesVisible&lt;/code&gt;. Kafka: consumer lag = high watermark − committed offset, xem qua &lt;code&gt;kafka-consumer-groups.sh --describe&lt;/code&gt; hoặc Burrow/kafka-lag-exporter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oldest message age&lt;/strong&gt; — SQS &lt;code&gt;ApproximateAgeOfOldestMessage&lt;/code&gt;, RabbitMQ với head message timestamp. Depth có thể ổn định nhưng age tăng nếu có message bị bỏ lại phía sau (nhất là khi ordering + partition).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumer throughput&lt;/strong&gt; (msg/s ack) và &lt;strong&gt;processing latency&lt;/strong&gt; (thời gian từ receive tới ack) — phân biệt "consumer chậm" vs "message ít".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DLQ depth&lt;/strong&gt; — mọi tăng đều phải alert; DLQ ổn ở 0 hoặc rất thấp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redelivery count&lt;/strong&gt; — RabbitMQ header &lt;code&gt;x-death&lt;/code&gt;, SQS &lt;code&gt;ApproximateReceiveCount&lt;/code&gt;. Message có count cao là ứng viên poison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publisher confirm / publish failure rate&lt;/strong&gt; — nếu producer không nhận confirm, message có thể chưa vào broker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tool chẩn đoán: &lt;code&gt;rabbitmqctl list_queues&lt;/code&gt; để chụp nhanh, &lt;code&gt;rabbitmq_prometheus&lt;/code&gt; + Grafana cho dashboard, AWS Console/CloudWatch cho SQS, &lt;code&gt;kafka-consumer-groups.sh --describe --group &amp;lt;g&amp;gt;&lt;/code&gt; cho lag Kafka. Khi debug một job cụ thể, log message id ở cả producer và consumer để trace end-to-end; nếu có tracing (OpenTelemetry), inject trace context vào message header và tiếp tục span ở consumer.&lt;/p&gt;

&lt;p&gt;Rule phòng ngừa: (1) mọi queue có DLQ + alert trên DLQ depth &amp;gt; 0; (2) mọi consumer idempotent, không có ngoại lệ; (3) mọi producer dùng publisher confirm hoặc outbox — không fire-and-forget cho việc quan trọng; (4) alert trên oldest message age, không chỉ depth; (5) load test để biết throughput baseline của worker, autoscale theo depth với ceiling để không đè chết downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoff
&lt;/h2&gt;

&lt;p&gt;Queue &lt;strong&gt;decouple&lt;/strong&gt; producer khỏi consumer — producer không cần biết consumer còn sống, không bị block khi consumer chậm, có thể scale hai bên độc lập. Queue &lt;strong&gt;smooth&lt;/strong&gt; tải — spike traffic được absorb vào depth, worker rút với tốc độ ổn định thay vì để downstream sập. Queue &lt;strong&gt;cho phép retry&lt;/strong&gt; ở tầng infra — worker crash, broker redeliver, không phải viết retry logic trong app.&lt;/p&gt;

&lt;p&gt;Cái giá: (1) &lt;strong&gt;eventual consistency&lt;/strong&gt; — client nhận 202 nghĩa là "đã nhận", không phải "đã xong"; UI phải phản ánh đúng trạng thái (polling job status, hoặc optimistic + reconcile), business rule không được giả định side-effect xong ngay; (2) &lt;strong&gt;thêm hạ tầng&lt;/strong&gt; — broker phải HA, phải monitor, phải backup, phải upgrade; một component thêm là một nguồn incident thêm; (3) &lt;strong&gt;at-least-once đẩy gánh idempotency về consumer&lt;/strong&gt; — mọi handler side-effect phải có dedupe key; (4) &lt;strong&gt;debug phức tạp hơn&lt;/strong&gt; — flow không còn là stack trace một request, phải trace qua queue, log message id, có khi phải replay từ DLQ; (5) &lt;strong&gt;ordering không miễn phí&lt;/strong&gt; — muốn giữ thứ tự phải hy sinh parallelism (single consumer per queue, hoặc partition theo key).&lt;/p&gt;

&lt;p&gt;Rule of thumb: đẩy vào queue mọi thứ (a) không cần đồng bộ với response cho user, (b) có side-effect ra hệ khác (email, payment, third-party API), (c) tốn nhiều hơn khoảng một trăm ms hoặc có tail latency dài; giữ đồng bộ mọi thứ user cần thấy kết quả ngay (đọc dữ liệu vừa write, validate form). Nếu chọn async, mặc định at-least-once + idempotent consumer + DLQ + outbox — thiếu bất kỳ mảnh nào trong bốn mảnh này là để lại failure mode chờ nổ.&lt;/p&gt;

&lt;h2&gt;
  
  
  Câu hỏi phỏng vấn
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Khi nào nên dùng message queue thay vì xử lý đồng bộ trong request, và những failure mode nào phải chuẩn bị trước khi đưa lên production?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Dùng queue khi công việc (a) không cần trả kết quả ngay cho client — email, notification, analytics, index update; (b) gọi third-party có tail latency dài hoặc SLA không đảm bảo — payment provider, mail service; (c) là fan-out một event tới nhiều consumer độc lập; (d) cần smooth spike traffic để bảo vệ downstream. Giữ đồng bộ nếu client cần thấy kết quả tức thì hoặc business rule cần strong consistency (đọc lại chính write vừa làm). Cơ chế cốt lõi: producer publish → broker persist → worker consume + ack; nếu worker chết trước ack, broker redeliver — nên delivery là at-least-once, không phải exactly-once. Failure mode phải chuẩn bị: (1) queue backlog khi producer nhanh hơn consumer — cần alert trên depth và oldest message age, autoscale worker, backpressure ở producer; (2) poison message — mọi queue phải có DLQ với &lt;code&gt;maxReceiveCount&lt;/code&gt;, không requeue vô điều kiện; (3) dual-write giữa DB và queue — dùng transactional outbox (insert vào bảng outbox trong cùng DB transaction, relay đọc và publish) để không mất event khi crash giữa hai bước; (4) duplicate delivery — consumer phải idempotent, dùng message id làm dedupe key hoặc idempotency key với provider (ví dụ Stripe idempotency key); (5) visibility timeout ngắn hơn thời gian xử lý — extend visibility định kỳ hoặc đặt timeout lớn hơn p99. Điểm ăn điểm là gọi đúng tên at-least-once + outbox + DLQ + idempotent consumer như một bộ tứ không thể thiếu, và nêu concrete metric (queue depth, oldest message age, DLQ depth, consumer lag) chứ không nói chung "monitor queue".&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on
&lt;/h2&gt;

&lt;p&gt;Dựng một RabbitMQ local, tái hiện backlog khi consumer chậm, thêm DLQ cho poison message, và xác nhận idempotent consumer chịu được duplicate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; rmq &lt;span class="nt"&gt;-p&lt;/span&gt; 5672:5672 &lt;span class="nt"&gt;-p&lt;/span&gt; 15672:15672 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;RABBITMQ_DEFAULT_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;RABBITMQ_DEFAULT_PASS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev &lt;span class="se"&gt;\&lt;/span&gt;
  rabbitmq:3-management
&lt;span class="c"&gt;# UI: http://localhost:15672  (dev/dev)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;queue-lab &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;queue-lab
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm i amqplib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo &lt;code&gt;setup.js&lt;/code&gt; để declare queue chính + DLQ:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;amqp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;amqplib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;amqp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;amqp://dev:dev@localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createChannel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertExchange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jobs.dlx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;direct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;durable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jobs.dlq&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;durable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bindQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jobs.dlq&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jobs.dlx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image.resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image.resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;durable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-dead-letter-exchange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jobs.dlx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-dead-letter-routing-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image.resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;queues ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo &lt;code&gt;producer.js&lt;/code&gt; bắn 5000 job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;amqp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;amqplib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;amqp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;amqp://dev:dev@localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createConfirmChannel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;poison&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;   &lt;span class="c1"&gt;// 1% poison để test DLQ&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;poison&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendToQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image.resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;persistent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForConfirms&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;published 5000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo &lt;code&gt;consumer.js&lt;/code&gt; xử lý chậm + idempotent + DLQ khi payload hỏng:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;amqp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;amqplib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// production: bảng DB có primary key&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;work&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;poison&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;   &lt;span class="c1"&gt;// idempotent&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;poison&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bad payload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nx"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;amqp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;amqp://dev:dev@localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createChannel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prefetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image.resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;work&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// requeue=false → route qua x-dead-letter-exchange sang DLQ&lt;/span&gt;
      &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;consuming...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chạy và quan sát:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node setup.js
node consumer.js &amp;amp;
node producer.js

&lt;span class="c"&gt;# xem depth tăng vọt rồi rút chậm&lt;/span&gt;
watch &lt;span class="nt"&gt;-n&lt;/span&gt; 1 &lt;span class="s1"&gt;'docker exec rmq rabbitmqctl list_queues name messages messages_ready messages_unacknowledged'&lt;/span&gt;

&lt;span class="c"&gt;# xem DLQ nhận ~50 poison message (1% của 5000)&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;rmq rabbitmqctl list_queues name messages | &lt;span class="nb"&gt;grep &lt;/span&gt;dlq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kỳ vọng: main queue depth nhảy lên gần 5000 rồi rút dần với tốc độ tương ứng với prefetch × số consumer. DLQ tăng dần lên khoảng 50 khi các poison message rớt qua. Chạy &lt;code&gt;node consumer.js &amp;amp;&lt;/code&gt; thêm vài lần để có 4 consumer song song, depth rút nhanh gấp bốn. Chạy lại &lt;code&gt;node producer.js&lt;/code&gt; khi consumer đang bận: xác nhận request producer không bị chặn — đó chính là decoupling nhìn thấy được. Cuối cùng, replay DLQ bằng cách viết một script nhỏ đọc &lt;code&gt;jobs.dlq&lt;/code&gt;, sửa payload, publish lại sang &lt;code&gt;image.resize&lt;/code&gt; sau khi đã fix bug producer — đây là workflow xử lý DLQ trong production thật.&lt;/p&gt;

</description>
      <category>queue</category>
      <category>messagequeue</category>
      <category>asyncprocessing</category>
      <category>messaging</category>
    </item>
    <item>
      <title>Cache Invalidation — Stale Data</title>
      <dc:creator>Nghề không nhiều chẳng sắc</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:40:30 +0000</pubDate>
      <link>https://dev.to/nguoidungai/cache-invalidation-stale-data-5gb1</link>
      <guid>https://dev.to/nguoidungai/cache-invalidation-stale-data-5gb1</guid>
      <description>&lt;h1&gt;
  
  
  Stale data và cache stampede: vì sao TTL một mình không đủ và vì sao origin sập khi key hết hạn
&lt;/h1&gt;

&lt;p&gt;Stale data là dữ liệu trong cache đã lỗi thời so với source of truth. Nó xuất hiện vì cache và origin là hai bản sao, và bất kỳ cơ chế đồng bộ nào — TTL, event-based invalidation, versioning — đều có cửa sổ giữa lúc origin đổi và lúc cache biết chuyện. Cái giá phải trả trong production không chỉ là "user nhìn thấy giá cũ vài giây". Khi một key hot vừa hết hạn, hàng nghìn request cùng miss, cùng đâm xuống DB để tính lại — đó là cache stampede (dogpile, thundering herd), và nó đủ sức đưa origin xuống trong vài chục giây.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cơ chế hoạt động
&lt;/h2&gt;

&lt;p&gt;Có bốn cơ chế invalidation dùng thật:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TTL (time-to-live).&lt;/strong&gt; Mỗi entry gắn một hạn dùng. Hết hạn coi như miss, đọc lại từ origin. Đơn giản, không cần coordination giữa writer và cache. Nhược điểm: staleness bounded bởi TTL, và tất cả replica của cùng một key hết hạn cùng lúc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event-based invalidation.&lt;/strong&gt; Khi origin thay đổi, phát một event (thường qua pub/sub, CDC như Debezium, hoặc gọi trực tiếp DEL) để cache xoá hoặc cập nhật entry. Fresh gần như realtime, nhưng đòi hỏi coupling giữa write path và cache — writer phải biết mọi key phái sinh từ dữ liệu vừa đổi.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Versioning (cache key có version).&lt;/strong&gt; Key gắn version của dữ liệu, ví dụ &lt;code&gt;user:123:v42&lt;/code&gt;. Đổi dữ liệu thì tăng version, key cũ tự nhiên bị bỏ qua, không cần xoá gì. Kỹ thuật này còn được gọi là generational caching; Rails cache dùng cách tương tự với &lt;code&gt;cache_key_with_version&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-flight (request coalescing).&lt;/strong&gt; Không phải invalidation, mà là cách xử lý miss: khi N request cùng miss cùng một key, chỉ &lt;em&gt;một&lt;/em&gt; trong số đó được phép gọi origin, các request còn lại đợi kết quả của nó. Go có &lt;code&gt;golang.org/x/sync/singleflight&lt;/code&gt; implement sẵn pattern này; Facebook memcache dùng "leases" (paper của Nishtala et al., NSDI 2013) cho cùng ý tưởng ở scale phân tán.&lt;/p&gt;

&lt;p&gt;Kết hợp điển hình: TTL để bounded staleness, single-flight để chặn stampede khi key hết hạn, event-based invalidation để cắt TTL sớm khi có write. Ví dụ single-flight với Redis SETNX làm distributed lock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;GetOrLoad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="c"&gt;// cache hit&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;lockKey&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"lock:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;
    &lt;span class="c"&gt;// SET NX PX = chỉ set nếu chưa có, kèm TTL để tránh deadlock nếu holder chết&lt;/span&gt;
    &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetNX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lockKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// request khác đang load; đợi ngắn rồi thử lại cache&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lockKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Vấn đề gặp trong production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 1: cache stampede khi key hot hết hạn.&lt;/strong&gt; Một product page top-traffic có TTL 60s. Đúng giây thứ 60, mọi replica cache đồng loạt miss; nếu QPS là 10k, thì 10k request cùng gọi cùng một query nặng xuống DB trong cùng một tick. DB connection pool cạn, latency của cả service tăng vọt, health check timeout, autoscaler kích hoạt nhưng không kịp. Code sai điển hình:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;GetProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// N request cùng vào đây khi key vừa hết hạn&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// origin ăn N query cùng lúc&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sửa bằng single-flight (in-process với &lt;code&gt;singleflight.Group&lt;/code&gt; nếu chỉ một node; distributed lock qua Redis SETNX nếu nhiều node). Ngoài single-flight, kỹ thuật XFetch (Vattani et al., "Optimal Probabilistic Cache Stampede Prevention", 2015) refresh entry &lt;em&gt;sớm hơn&lt;/em&gt; TTL với xác suất tăng dần khi tới gần hạn — trải đều tải refresh thay vì dồn vào một tick.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 2: stale data vô hạn khi event-based invalidation rớt event.&lt;/strong&gt; Writer phát event &lt;code&gt;DEL cache:user:123&lt;/code&gt; qua Kafka, cache consumer xoá key. Nếu consumer bị lag, chết, hoặc event bị drop (Kafka retention hết trước khi consumer kịp đọc), key stale mãi mãi vì không có TTL backstop. Rule: event-based invalidation &lt;strong&gt;luôn&lt;/strong&gt; đi kèm TTL — event là fast-path, TTL là safety net. Cấu hình TTL vừa đủ dài để chấp nhận cost, đủ ngắn để bound staleness khi event rớt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 3: thundering herd sau khi flush cache toàn cục.&lt;/strong&gt; Deploy mới, ai đó chạy &lt;code&gt;FLUSHDB&lt;/code&gt; để bust cache. Mọi key biến mất cùng lúc, mọi request tiếp theo miss, origin nhận toàn bộ traffic trực tiếp. Không phải stampede trên một key, mà stampede trên cả cache. Cách xử lý: không flush toàn cục ở giờ cao điểm; nếu cần bust, dùng versioning (đổi prefix key) để cache cũ hết dần theo TTL thay vì biến mất tức thì; hoặc warm cache trước khi cắt traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 4: write-through mà không invalidate downstream cache.&lt;/strong&gt; App có L1 (in-process LRU) và L2 (Redis). Write đi qua và update Redis, nhưng L1 trên từng node vẫn giữ giá trị cũ tới khi TTL local hết. User hit node A thấy dữ liệu mới, hit node B thấy dữ liệu cũ — read-your-write bị phá. Sửa bằng cách hoặc bỏ L1 cho những entity cần consistency, hoặc phát invalidation message qua pub/sub tới tất cả node để mọi L1 evict đồng thời.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 5: negative caching quên set TTL ngắn.&lt;/strong&gt; Cache cả kết quả "không tồn tại" để chống cache penetration, nhưng dùng cùng TTL với hit thật. Khi entity thực sự được tạo sau đó, cache vẫn trả &lt;code&gt;not found&lt;/code&gt; cho tới khi TTL hết. TTL của negative entry phải ngắn hơn nhiều so với positive entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cách debug và monitor
&lt;/h2&gt;

&lt;p&gt;Triệu chứng cache stampede: origin QPS/latency đột ngột spike theo bậc thang mỗi khi TTL của key hot hết hạn (đồ thị hình răng cưa); cache hit ratio đột nhiên tụt trong vài giây; DB connection pool queue depth tăng, một số request timeout. Triệu chứng stale data: user report thấy dữ liệu cũ sau khi write; log write path thành công nhưng read path vẫn trả giá trị trước đó; phân tán không đều giữa các node (L1 chưa evict).&lt;/p&gt;

&lt;p&gt;Metric cần có:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache hit ratio&lt;/strong&gt; — hit / (hit + miss). Thấp bất thường hoặc dip theo pattern là dấu hiệu invalidation quá aggressive hoặc TTL quá ngắn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin request rate&lt;/strong&gt; cho các key đi qua cache — spike khi TTL hết là dấu hiệu stampede.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staleness distribution&lt;/strong&gt; — nếu event-based, đo độ trễ từ lúc origin commit tới lúc cache reflect (cần timestamp trong payload).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock contention&lt;/strong&gt; cho single-flight — số request phải đợi lock, thời gian đợi trung bình.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis &lt;code&gt;INFO stats&lt;/code&gt;&lt;/strong&gt; cho &lt;code&gt;keyspace_hits&lt;/code&gt;, &lt;code&gt;keyspace_misses&lt;/code&gt;, &lt;code&gt;evicted_keys&lt;/code&gt;; &lt;code&gt;redis-cli MONITOR&lt;/code&gt; (rất nặng, chỉ dùng ngắn) để xem pattern thực tế.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trong Prometheus/Grafana, alert khi origin request rate cho một key spike gấp nhiều lần baseline trong vài giây. Rule phòng ngừa: mỗi entry phải có TTL (không entry sống mãi), mọi query nặng đi qua cache phải có single-flight, và mọi event-based invalidation phải có TTL backstop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoff
&lt;/h2&gt;

&lt;p&gt;TTL ngắn giảm staleness nhưng tăng origin load (miss nhiều hơn) và tăng nguy cơ stampede. TTL dài giảm origin load nhưng user thấy dữ liệu cũ lâu hơn. Event-based invalidation cho freshness gần realtime nhưng đòi hỏi coupling và infrastructure (message bus, CDC) — mỗi kênh event là một điểm có thể lỗi. Versioning tránh phải xoá gì cả và cho immutable cache entry (dễ CDN), nhưng tốn thêm indirection để nhìn ra version hiện hành, và cache cũ chiếm bộ nhớ tới khi TTL evict.&lt;/p&gt;

&lt;p&gt;Single-flight loại bỏ stampede gần như hoàn toàn với cost một round-trip lock; nhược là nếu holder của lock chết mà không release, các request đợi bị block tới khi lock TTL hết — nên lock TTL phải ngắn và có fallback (đợi timeout rồi tự gọi origin). XFetch tránh cần lock, phân tán refresh theo xác suất, nhưng một số request phải chịu latency của refresh sớm dù cache còn hạn.&lt;/p&gt;

&lt;p&gt;Rule of thumb: mọi entry cache dùng chung có TTL (không có ngoại lệ); mọi query có thể sinh stampede (query nặng, traffic cao) bọc single-flight; dùng event-based invalidation cho freshness nhưng luôn giữ TTL làm safety net; versioning cho immutable snapshot hoặc khi có CDN.&lt;/p&gt;

&lt;h2&gt;
  
  
  Câu hỏi phỏng vấn
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Cache stampede là gì, xảy ra khi nào, và phòng thế nào trong một hệ có nhiều node cache?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cache stampede (dogpile / thundering herd) xảy ra khi một key hot vừa expire hoặc bị invalidate; N request đang inflight cùng miss, cùng gọi origin tính lại cùng một giá trị. Origin (thường là DB hoặc service downstream) bất ngờ nhận N lần công việc trùng lặp, connection pool cạn, latency tăng, và có thể sập kéo theo cả service phụ thuộc. Nguyên nhân gốc là cache miss không có coordination giữa các request. Phòng bằng single-flight: chỉ một request được phép gọi origin cho một key tại một thời điểm, các request khác đợi kết quả — in-process dùng &lt;code&gt;singleflight.Group&lt;/code&gt; (Go) hoặc tương đương, distributed dùng Redis &lt;code&gt;SET NX EX&lt;/code&gt; làm lock với TTL ngắn để tránh deadlock nếu holder chết. Bổ sung XFetch (probabilistic early refresh) để refresh key sớm hơn TTL thật, trải đều tải refresh thay vì dồn vào một tick. Điểm ăn điểm là gọi tên hậu quả production cụ thể (DB connection pool cạn, cascade failure, autoscaler không kịp), phân biệt in-process vs distributed single-flight, và nêu TTL backstop cho event-based invalidation để tránh stale vô hạn khi event rớt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on
&lt;/h2&gt;

&lt;p&gt;Dựng một Redis local và một origin giả chậm, tái hiện stampede, đo, sửa bằng single-flight, đo lại.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; redis-stampede &lt;span class="nt"&gt;-p&lt;/span&gt; 6379:6379 redis:7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo file &lt;code&gt;stampede.go&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"sync"&lt;/span&gt;
    &lt;span class="s"&gt;"sync/atomic"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/redis/go-redis/v9"&lt;/span&gt;
    &lt;span class="s"&gt;"golang.org/x/sync/singleflight"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;rdb&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Addr&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"localhost:6379"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;originCalls&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;sf&lt;/span&gt;          &lt;span class="n"&gt;singleflight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Group&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;slowOrigin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddInt64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;originCalls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// giả query DB nặng&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;GetNaive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;slowOrigin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;GetSingleFlight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// double-check sau khi giành lock: có thể request khác đã set xong&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;slowOrigin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StoreInt64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;originCalls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"hot"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"hot"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s: 1000 requests, %d origin calls, took %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadInt64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;originCalls&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"naive"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GetNaive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"single-flight"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GetSingleFlight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chạy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go mod init stampede-lab
go get github.com/redis/go-redis/v9 golang.org/x/sync/singleflight
go run stampede.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output kỳ vọng: bản naive báo cỡ vài trăm origin call cho 1000 request (mỗi request miss trước khi request đầu kịp set) — đúng là stampede. Bản single-flight báo đúng 1 origin call cho 1000 request cùng key. Chạy lại nhiều lần với &lt;code&gt;sleep 2&lt;/code&gt; giữa các lần để ép key expire và xem stampede tái xuất trong bản naive mỗi vòng TTL, trong khi bản single-flight vẫn giữ 1 origin call/vòng.&lt;/p&gt;

&lt;p&gt;Bước tiếp: mở rộng bằng Redis &lt;code&gt;SET NX EX&lt;/code&gt; để single-flight xuyên node (spawn hai process Go song song, xác nhận chỉ một trong hai gọi &lt;code&gt;slowOrigin&lt;/code&gt; khi key expire). Thêm Prometheus counter cho &lt;code&gt;origin_calls_total&lt;/code&gt; và &lt;code&gt;cache_hits_total&lt;/code&gt;, scrape bằng &lt;code&gt;prometheus&lt;/code&gt; local, vẽ chart hình răng cưa của bản naive rồi flat của bản fix — đó là artefact đưa vào postmortem hay design doc thật.&lt;/p&gt;

</description>
      <category>caching</category>
      <category>cacheinvalidation</category>
      <category>staledata</category>
    </item>
    <item>
      <title>CAP Theorem — Consistency vs Availability</title>
      <dc:creator>Nghề không nhiều chẳng sắc</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:40:27 +0000</pubDate>
      <link>https://dev.to/nguoidungai/cap-theorem-consistency-vs-availability-41ij</link>
      <guid>https://dev.to/nguoidungai/cap-theorem-consistency-vs-availability-41ij</guid>
      <description>&lt;h1&gt;
  
  
  CAP: khi network partition xảy ra, chỉ được chọn C hoặc A — không có "cả ba"
&lt;/h1&gt;

&lt;p&gt;CAP theorem là kết quả của Gilbert và Lynch (formal proof năm 2002 cho conjecture Brewer đưa ra ở PODC keynote 2000): một hệ phân tán có shared state không thể đồng thời cung cấp cả linearizable &lt;strong&gt;Consistency&lt;/strong&gt;, &lt;strong&gt;Availability&lt;/strong&gt; (mọi request tới non-failing node đều trả lời không lỗi), và &lt;strong&gt;Partition tolerance&lt;/strong&gt; khi có network partition. Trong thực tế, partition là thứ sẽ xảy ra — TCP retransmit, GC pause dài, switch chết, cross-region link flap — nên P là ràng buộc bắt buộc, không phải lựa chọn. Câu hỏi thật là: khi partition xảy ra, hệ thống hy sinh C hay A? Chọn sai gây ra hai loại incident khác nhau: chọn AP mà dữ liệu cần linearizable dẫn tới double-charge, oversell inventory, split-brain; chọn CP mà dữ liệu chỉ cần eventually consistent dẫn tới downtime không cần thiết, user không đọc được profile của chính mình.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cơ chế hoạt động
&lt;/h2&gt;

&lt;p&gt;Định nghĩa formal theo Gilbert và Lynch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt; ở đây là &lt;strong&gt;linearizability&lt;/strong&gt;: mọi read sau một write hoàn tất phải thấy giá trị mới (hoặc mới hơn); tồn tại một total order các operation phù hợp với real-time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability&lt;/strong&gt;: mọi request tới một non-failing node phải nhận response (không timeout, không error).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partition tolerance&lt;/strong&gt;: hệ thống tiếp tục hoạt động dù network drop tuỳ ý message giữa các node.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Proof intuition: giả sử có 2 node N1, N2 giữ cùng key &lt;code&gt;x=0&lt;/code&gt;. Client ghi &lt;code&gt;x=1&lt;/code&gt; vào N1. Link N1 và N2 đứt. Một client khác đọc &lt;code&gt;x&lt;/code&gt; từ N2. Nếu N2 trả về &lt;code&gt;0&lt;/code&gt; thì không linearizable (mất C). Nếu N2 chờ đến khi thấy được N1 thì mất A. Nếu N2 từ chối phục vụ thì cũng mất A. Không có cách thứ ba.&lt;/p&gt;

&lt;p&gt;Trong hệ CP, mỗi write phải qua quorum (Raft, Paxos, ZAB); khi node bị isolate khỏi quorum, nó từ chối phục vụ để giữ linearizability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// etcd/Raft-style: khi mất quorum, leader step down và write fail&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"order/42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"paid"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// err là ErrLeaderChanged hoặc context.DeadlineExceeded khi ở minority side&lt;/span&gt;
    &lt;span class="c"&gt;// client thấy unavailable — đúng contract CP&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cluster lost quorum: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trong hệ AP, node minority vẫn nhận write, gắn version vector / vector clock, đồng bộ sau khi partition heal — có thể conflict và cần resolution (last-write-wins, CRDT, application merge):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Cassandra: tunable consistency, ONE cho phép write vào node đơn lẻ
&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO cart (user_id, item, qty) VALUES (%s, %s, %s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;consistency_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ConsistencyLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ONE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# AP-style, vẫn ghi khi partition
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# read sau đó với ONE có thể trả về giá trị cũ ở replica khác
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Brewer viết lại năm 2012 (bài "CAP Twelve Years Later" trên IEEE Computer) rằng framing "chọn 2 trong 3" gây hiểu lầm: partition hiếm, và ngay cả một service cũng có thể là CP cho một số operation (thanh toán) và AP cho operation khác (đếm view). CAP là quyết định per-operation, không phải per-system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vấn đề gặp trong production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 1: giả định là CA (có cả ba) trong một hệ phân tán.&lt;/strong&gt; Đây là cái bẫy phổ biến nhất. Team dùng Postgres primary cộng read replica, viết code như thể replica luôn có dữ liệu mới nhất:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SAI — assume read-after-write consistency across replicas
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_order_and_show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO orders ... RETURNING id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
    &lt;span class="c1"&gt;# ngay lập tức đọc từ replica để hiển thị
&lt;/span&gt;    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;replica&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM orders WHERE id = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;  &lt;span class="c1"&gt;# None khi replication lag lớn hơn round-trip
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Khi replication lag tăng (batch load, network hiccup), &lt;code&gt;order&lt;/code&gt; trả về &lt;code&gt;None&lt;/code&gt;. Bug này không hiện trong staging vì lag ở đó thường rất nhỏ. Cách sửa: read-your-writes bằng cách đọc lại từ primary sau khi write, hoặc dùng session token / GTID để wait cho replica bắt kịp, hoặc chấp nhận eventual consistency và render optimistic từ input.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ĐÚNG — đọc lại từ primary trong cùng transaction
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_order_and_show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO orders ... RETURNING id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM orders WHERE id = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure mode 2: chọn AP cho use case cần linearizable.&lt;/strong&gt; Kinh điển: dùng DynamoDB eventually-consistent read hoặc Cassandra &lt;code&gt;ONE&lt;/code&gt; cho counter tồn kho, kết quả oversell. Hai request cùng thấy &lt;code&gt;stock=1&lt;/code&gt;, cả hai decrement, cuối cùng &lt;code&gt;stock=-1&lt;/code&gt;. Postmortem loại này lặp đi lặp lại trong các báo cáo Jepsen của Kyle Kingsbury: Riak, Cassandra, MongoDB (khi chưa dùng majority write concern và linearizable read concern) đều bị bắt lỗi consistency claim.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SAI — Cassandra ONE cho inventory decrement, không linearizable&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UPDATE inventory SET stock = stock - 1 WHERE sku = ?"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ĐÚNG — dùng lightweight transaction (Paxos) khi cần compare-and-set&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"UPDATE inventory SET stock = ? WHERE sku = ? IF stock = ?"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;newStock&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentStock&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// trả về applied=false nếu ai đó đã ghi trước — retry&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure mode 3: chọn CP nhưng không lường được blast radius khi mất quorum.&lt;/strong&gt; GitHub công bố postmortem về một sự cố cross-DC partition ngắn (dưới một phút) trong tháng 10 năm 2018 khiến MySQL Orchestrator promote replica ở DC khác; khi link phục hồi, có writes ở hai phía — team chọn ưu tiên consistency, dừng writes để reconcile thủ công, chấp nhận degradation kéo dài nhiều giờ. Đó là quyết định CP đúng nhưng cost cao. Nhiều team gặp incident tương tự với Consul, etcd, Zookeeper cluster khi cross-AZ latency spike làm minority partition từ chối phục vụ; nếu health check của app không phân biệt "cluster down" với "cluster unavailable to me", cả service tier bị đánh dấu unhealthy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 4: split-brain do quorum sai.&lt;/strong&gt; Cluster 2 node với "majority = 1" là công thức split-brain. Khi partition xảy ra, cả hai bên đều nghĩ mình là majority. Đây là lý do Raft/ZAB yêu cầu odd number of voting member và các hệ như etcd, Consul cảnh báo rõ ràng khi số voter chẵn. Với 3 node chịu mất được 1; với 5 node chịu mất được 2 — đó là tradeoff giữa fault tolerance và write latency (quorum càng lớn càng chậm).&lt;/p&gt;

&lt;h2&gt;
  
  
  Cách debug và monitor
&lt;/h2&gt;

&lt;p&gt;Triệu chứng partition thường bị chẩn đoán nhầm thành "database chậm". Cần phân biệt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cluster health metric&lt;/strong&gt;: &lt;code&gt;etcd_server_has_leader&lt;/code&gt;, &lt;code&gt;etcd_server_leader_changes_seen_total&lt;/code&gt;, &lt;code&gt;consul.raft.leader.lastContact&lt;/code&gt;, MongoDB &lt;code&gt;rs.status()&lt;/code&gt; — thay đổi state đột ngột là dấu hiệu leader election, thường do partition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replication lag&lt;/strong&gt;: Postgres &lt;code&gt;pg_stat_replication.replay_lag&lt;/code&gt;, MySQL &lt;code&gt;SHOW SLAVE STATUS&lt;/code&gt; với &lt;code&gt;Seconds_Behind_Master&lt;/code&gt;, Kafka consumer lag. Lag tăng đột ngột trên tất cả replica cùng lúc là network issue, không phải replica chậm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quorum loss log&lt;/strong&gt;: etcd log &lt;code&gt;lost quorum&lt;/code&gt;, Zookeeper chuyển &lt;code&gt;LEADING&lt;/code&gt; sang &lt;code&gt;LOOKING&lt;/code&gt;, Consul &lt;code&gt;raft: heartbeat timeout&lt;/code&gt; — những dòng này phải được alert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client-side symptom&lt;/strong&gt;: tỷ lệ &lt;code&gt;context.DeadlineExceeded&lt;/code&gt; hoặc &lt;code&gt;NoHostAvailable&lt;/code&gt; tăng đột biến trên một subset client (một AZ), trong khi các AZ khác ổn — partial partition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency test&lt;/strong&gt;: chạy Jepsen (framework của Kyle Kingsbury) hoặc &lt;code&gt;elle&lt;/code&gt; để verify hệ mình dùng có giữ contract quảng cáo không. Nhiều DB đã bị Jepsen bắt lỗi consistency claim, kết quả được document công khai trên &lt;code&gt;jepsen.io&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule phòng ngừa:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Không deploy quorum-based cluster với số voter chẵn.&lt;/li&gt;
&lt;li&gt;Đặt cluster member ở ít nhất 3 failure domain (AZ) khác nhau; tránh 2-DC setup nếu cần CP.&lt;/li&gt;
&lt;li&gt;Health check của app phải phân biệt "tôi không tới được DB" với "DB down globally"; không cascade fail cả tier khi chỉ minority-side unhealthy.&lt;/li&gt;
&lt;li&gt;Test partition trong staging bằng &lt;code&gt;tc qdisc&lt;/code&gt; (drop packet), &lt;code&gt;iptables -A INPUT -s &amp;lt;peer&amp;gt; -j DROP&lt;/code&gt;, hoặc chaos tool (Toxiproxy, Chaos Mesh).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tradeoff
&lt;/h2&gt;

&lt;p&gt;CP đổi availability lấy correctness: khi partition, minority side từ chối phục vụ, client thấy error, nhưng không bao giờ có hai giá trị mâu thuẫn cùng tồn tại. Phù hợp cho money, inventory, unique constraint (username, order ID), config store, service discovery — nơi trả về stale hoặc conflict tệ hơn trả về error. Cost: write latency cao hơn (phải wait quorum ack), và availability giảm đúng bằng xác suất mất quorum.&lt;/p&gt;

&lt;p&gt;AP đổi consistency lấy uptime: mọi replica vẫn nhận request khi partition, đồng bộ sau. Phù hợp cho shopping cart (merge được), social feed, view counter, log ingestion, DNS-style lookup — nơi stale trong vài giây chấp nhận được và downtime tệ hơn. Cost: application phải handle conflict resolution (CRDT, LWW, user chọn merge), read có thể thấy giá trị cũ, monotonic guarantee yếu.&lt;/p&gt;

&lt;p&gt;PACELC (Daniel Abadi, 2010) mở rộng CAP: &lt;strong&gt;P&lt;/strong&gt;artition thì chọn &lt;strong&gt;A&lt;/strong&gt; hay &lt;strong&gt;C&lt;/strong&gt;; &lt;strong&gt;E&lt;/strong&gt;lse thì chọn &lt;strong&gt;L&lt;/strong&gt;atency hay &lt;strong&gt;C&lt;/strong&gt;onsistency. Kể cả khi không có partition, việc chờ replica ack (strong consistency) vẫn thêm latency; nhiều hệ (DynamoDB, Cassandra) chọn EL — hy sinh consistency lấy latency thấp ngay cả trong điều kiện bình thường.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb&lt;/strong&gt;: quyết định per-operation, không per-system. Money / inventory / unique ID chọn CP path (Raft-backed store, LWT, transactional DB với &lt;code&gt;SERIALIZABLE&lt;/code&gt;). Feed / cart / counter / cache chọn AP path (eventual, CRDT hoặc idempotent merge). Không viết code như thể hệ CA tồn tại — nó không tồn tại trong hệ phân tán thật.&lt;/p&gt;

&lt;h2&gt;
  
  
  Câu hỏi phỏng vấn
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;CAP theorem nói gì, và tại sao "chọn 2 trong 3" là cách phát biểu sai? Cho một use case cụ thể để minh hoạ khi nào chọn CP, khi nào chọn AP.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;CAP (Gilbert và Lynch, 2002, formal hoá conjecture của Brewer 2000) chứng minh rằng trong hệ phân tán có shared state, khi network partition xảy ra không thể đồng thời giữ linearizable consistency và availability (mọi request tới non-failing node đều nhận response). "Chọn 2 trong 3" gây hiểu lầm vì Partition tolerance không phải lựa chọn — network sẽ partition (TCP timeout, GC pause, switch chết); nếu hệ thống không tolerate partition, nó không phải hệ phân tán. Câu hỏi thật là khi partition xảy ra, hy sinh C hay A. Ví dụ: hệ thanh toán và inventory phải CP — dùng Postgres transactional hoặc Raft-backed store (etcd, Spanner-style); khi minority side mất quorum, nó từ chối write, client thấy error, nhưng không có double-charge hay oversell. Ngược lại, shopping cart hoặc social feed nên AP — Cassandra hoặc DynamoDB eventually consistent, merge conflict bằng CRDT hoặc LWW; user vẫn add item được khi partition, đồng bộ sau. Điểm ăn điểm là: nêu Brewer 2012 clarification rằng CAP là quyết định per-operation không per-system; đề cập PACELC — kể cả khi không partition vẫn có tradeoff latency-vs-consistency; kể failure mode thật — giả định CA gây bug read-after-write khi có replication lag, chọn AP cho inventory gây oversell (Jepsen documented cho nhiều DB), chọn CP không lường quorum loss gây incident kiểu GitHub tháng 10 năm 2018 (partition ngắn nhưng downtime dài để reconcile).&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on
&lt;/h2&gt;

&lt;p&gt;Dựng một cluster etcd 3 node local qua Docker để tận mắt quan sát hành vi CP khi partition. Tạo file &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;etcd1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;quay.io/coreos/etcd:v3.5.9&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;etcd --name etcd1&lt;/span&gt;
      &lt;span class="s"&gt;--initial-advertise-peer-urls http://etcd1:2380&lt;/span&gt;
      &lt;span class="s"&gt;--listen-peer-urls http://0.0.0.0:2380&lt;/span&gt;
      &lt;span class="s"&gt;--advertise-client-urls http://etcd1:2379&lt;/span&gt;
      &lt;span class="s"&gt;--listen-client-urls http://0.0.0.0:2379&lt;/span&gt;
      &lt;span class="s"&gt;--initial-cluster etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380&lt;/span&gt;
      &lt;span class="s"&gt;--initial-cluster-state new&lt;/span&gt;
      &lt;span class="s"&gt;--initial-cluster-token cap-lab&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2379:2379"&lt;/span&gt;
  &lt;span class="na"&gt;etcd2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;quay.io/coreos/etcd:v3.5.9&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;etcd --name etcd2&lt;/span&gt;
      &lt;span class="s"&gt;--initial-advertise-peer-urls http://etcd2:2380&lt;/span&gt;
      &lt;span class="s"&gt;--listen-peer-urls http://0.0.0.0:2380&lt;/span&gt;
      &lt;span class="s"&gt;--advertise-client-urls http://etcd2:2379&lt;/span&gt;
      &lt;span class="s"&gt;--listen-client-urls http://0.0.0.0:2379&lt;/span&gt;
      &lt;span class="s"&gt;--initial-cluster etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380&lt;/span&gt;
      &lt;span class="s"&gt;--initial-cluster-state new&lt;/span&gt;
      &lt;span class="s"&gt;--initial-cluster-token cap-lab&lt;/span&gt;
  &lt;span class="na"&gt;etcd3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;quay.io/coreos/etcd:v3.5.9&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;etcd --name etcd3&lt;/span&gt;
      &lt;span class="s"&gt;--initial-advertise-peer-urls http://etcd3:2380&lt;/span&gt;
      &lt;span class="s"&gt;--listen-peer-urls http://0.0.0.0:2380&lt;/span&gt;
      &lt;span class="s"&gt;--advertise-client-urls http://etcd3:2379&lt;/span&gt;
      &lt;span class="s"&gt;--listen-client-urls http://0.0.0.0:2379&lt;/span&gt;
      &lt;span class="s"&gt;--initial-cluster etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380&lt;/span&gt;
      &lt;span class="s"&gt;--initial-cluster-state new&lt;/span&gt;
      &lt;span class="s"&gt;--initial-cluster-token cap-lab&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chạy và verify leader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;etcd1 etcdctl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--endpoints&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://etcd1:2379,http://etcd2:2379,http://etcd3:2379 &lt;span class="se"&gt;\&lt;/span&gt;
  endpoint status &lt;span class="nt"&gt;--write-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;table
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;etcd1 etcdctl put /order/42 paid
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;etcd1 etcdctl get /order/42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo partition bằng cách cắt etcd1 khỏi hai node còn lại (etcd1 rơi vào minority):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network disconnect cap-lab_default cap-lab-etcd1-1
&lt;span class="c"&gt;# hoặc dùng iptables trong container để drop packet tới etcd2, etcd3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thử write vào minority side — quan sát nó fail sau election timeout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;etcd1 etcdctl &lt;span class="nt"&gt;--command-timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5s put /order/43 attempted
&lt;span class="c"&gt;# expected: context deadline exceeded — CP behavior đúng&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thử write vào majority side (etcd2 hoặc etcd3) — vẫn chạy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;etcd2 etcdctl put /order/43 paid
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;etcd2 etcdctl get /order/43
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Heal partition và verify etcd1 catch up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network connect cap-lab_default cap-lab-etcd1-1
&lt;span class="nb"&gt;sleep &lt;/span&gt;5
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;etcd1 etcdctl get /order/43   &lt;span class="c"&gt;# thấy giá trị từ majority&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sau đó lặp lại thí nghiệm với một AP store (Cassandra 3-node) để so sánh. Dùng &lt;code&gt;docker compose&lt;/code&gt; với image &lt;code&gt;cassandra:4.1&lt;/code&gt;, tạo keyspace với &lt;code&gt;replication_factor = 3&lt;/code&gt;, ghi bằng &lt;code&gt;CONSISTENCY ONE&lt;/code&gt; khi partition, quan sát mỗi node có thể trả về giá trị khác nhau, rồi resolve sau khi heal. So sánh hai hành vi trên cùng một scenario: etcd từ chối write ở minority (CP), Cassandra chấp nhận write ở mọi node (AP) — chính là tradeoff C-vs-A trong thực tế.&lt;/p&gt;

</description>
      <category>cap</category>
      <category>captheorem</category>
      <category>consistencyvsavailability</category>
      <category>pacelc</category>
    </item>
    <item>
      <title>Redundancy &amp; Replication — Data Redundancy</title>
      <dc:creator>Nghề không nhiều chẳng sắc</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:40:23 +0000</pubDate>
      <link>https://dev.to/nguoidungai/redundancy-replication-data-redundancy-2m3j</link>
      <guid>https://dev.to/nguoidungai/redundancy-replication-data-redundancy-2m3j</guid>
      <description>&lt;h1&gt;
  
  
  Data redundancy: một node lỗi là mất dữ liệu, và replica async không cứu được nếu không ai theo dõi lag
&lt;/h1&gt;

&lt;p&gt;Data redundancy là việc giữ nhiều bản copy của cùng một dataset trên các node khác nhau để khi một node chết, dataset không đi theo nó. Cơ chế thực thi phổ biến nhất là replication: một node là source-of-truth (primary/leader), các node khác nhận và apply lại các thay đổi. Nghe đơn giản, nhưng phần lớn incident mất dữ liệu ở database không đến từ "chưa có replica" mà từ "có replica nhưng không sync kịp lúc primary chết", hoặc "replica đã broken từ lâu mà không ai biết vì không có alert trên replication lag". Postmortem của GitLab.com tháng 1/2017 là ví dụ điển hình: một engineer xoá nhầm data directory trên primary, mọi replica đã ngừng streaming từ trước, và các backup channel khác đều hỏng — dữ liệu vài giờ cuối mất hẳn dù về danh nghĩa hệ thống "có replica".&lt;/p&gt;

&lt;h2&gt;
  
  
  Cơ chế hoạt động
&lt;/h2&gt;

&lt;p&gt;Ở database quan hệ (Postgres, MySQL), replication chạy trên write-ahead log (WAL / binlog): mọi thay đổi trước khi vào table file đều được append vào một file log tuần tự; replica kết nối tới primary, stream các WAL record đó, và apply lại y hệt vào bản data local. Vì WAL là physical/logical record của mọi thay đổi đã commit, replica hội tụ về cùng state với primary — miễn là nó theo kịp.&lt;/p&gt;

&lt;p&gt;Điểm quyết định là thời điểm primary trả "commit ok" về client:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Async replication&lt;/strong&gt; — primary flush WAL local rồi trả ngay. WAL được đẩy sang replica ở background. Nhanh, nhưng nếu primary chết trong khoảng "đã commit local, chưa gửi sang replica", các transaction đó không tồn tại trên replica.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync replication&lt;/strong&gt; — primary phải chờ ít nhất một replica xác nhận đã nhận (hoặc đã flush, hoặc đã apply) WAL rồi mới trả về client. Không mất transaction đã ack, đổi lại latency mỗi commit tăng theo RTT tới replica chậm nhất, và nếu replica sync down thì primary có thể block write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quorum-based&lt;/strong&gt; — thay vì phân biệt primary/replica cứng, N replica tham gia; một write chỉ coi là committed khi được W trong N replica ack, một read cần R replica trả để lấy giá trị mới. Với &lt;code&gt;W + R &amp;gt; N&lt;/code&gt;, mọi read đảm bảo thấy write đã committed. Dynamo/Cassandra dùng tunable quorum ở tầng client; etcd/Consul/ZooKeeper dùng Raft/Paxos ép quorum majority ở tầng consensus; Kafka dùng ISR (in-sync replicas) + &lt;code&gt;acks=all&lt;/code&gt; + &lt;code&gt;min.insync.replicas&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Postgres cho phép chọn strictness qua &lt;code&gt;synchronous_commit&lt;/code&gt; và &lt;code&gt;synchronous_standby_names&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# postgresql.conf trên primary
&lt;/span&gt;&lt;span class="n"&gt;wal_level&lt;/span&gt; = &lt;span class="n"&gt;replica&lt;/span&gt;
&lt;span class="n"&gt;max_wal_senders&lt;/span&gt; = &lt;span class="m"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;synchronous_standby_names&lt;/span&gt; = &lt;span class="s1"&gt;'ANY 1 (standby1, standby2)'&lt;/span&gt;  &lt;span class="c"&gt;# cần 1 trong 2 ack
&lt;/span&gt;&lt;span class="n"&gt;synchronous_commit&lt;/span&gt; = &lt;span class="n"&gt;on&lt;/span&gt;   &lt;span class="c"&gt;# 'on' = chờ remote flush WAL; 'remote_apply' = chờ apply xong
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Với config trên, một &lt;code&gt;COMMIT&lt;/code&gt; chỉ trả về client khi ít nhất một trong hai standby đã fsync WAL. Nếu đổi thành &lt;code&gt;synchronous_commit = off&lt;/code&gt;, primary trả ngay cả trước khi fsync local — hiệu năng cao nhất, rủi ro mất vài trăm ms transaction gần nhất khi primary crash. MySQL có mô hình tương tự: async là mặc định, semi-sync qua plugin &lt;code&gt;rpl_semi_sync_source&lt;/code&gt; chờ replica ack đã ghi relay log (chưa apply), Group Replication dùng consensus theo Paxos-variant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vấn đề gặp trong production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 1: async replication + failover = mất transaction đã ack.&lt;/strong&gt; Primary chạy async, replica trễ vài giây. Primary crash, orchestrator promote replica; các transaction commit trong khoảng "đã ack cho client, chưa stream sang replica" biến mất khỏi hệ thống mới. Client tưởng đơn hàng đã ghi (đã nhận 200 OK), sau failover query lại thì không thấy. Đây là RPO ≠ 0 — bug nghiệp vụ, không phải bug code. Kịch bản GitHub tháng 10/2018 (postmortem chính thức trên blog GitHub Engineering) đúng dạng này ở quy mô cross-region: một partition mạng ngắn kích hoạt failover ở region khác, và việc reconcile hai bản sau đó mất hàng giờ vì trạng thái đã diverge.&lt;/p&gt;

&lt;p&gt;Fix ở tầng config, không phải code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# Sai: async, chấp nhận mất write khi primary chết
&lt;/span&gt;&lt;span class="n"&gt;synchronous_commit&lt;/span&gt; = &lt;span class="n"&gt;local&lt;/span&gt;
&lt;span class="n"&gt;synchronous_standby_names&lt;/span&gt; = &lt;span class="s1"&gt;''&lt;/span&gt;

&lt;span class="c"&gt;# Đúng cho data không được phép mất (đơn hàng, thanh toán):
&lt;/span&gt;&lt;span class="n"&gt;synchronous_commit&lt;/span&gt; = &lt;span class="n"&gt;on&lt;/span&gt;
&lt;span class="n"&gt;synchronous_standby_names&lt;/span&gt; = &lt;span class="s1"&gt;'ANY 1 (standby1, standby2)'&lt;/span&gt;  &lt;span class="c"&gt;# ít nhất 1 remote flush
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Với &lt;code&gt;ANY 1 (…)&lt;/code&gt;, chỉ cần một trong nhiều standby còn sống là commit vẫn tiến; primary không bị block chỉ vì một standby chậm. Đừng dùng &lt;code&gt;FIRST 1 (standby1)&lt;/code&gt; với chỉ một replica — replica đó down là toàn bộ write block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 2: replica đã ngừng streaming mà không ai biết.&lt;/strong&gt; Đây là lỗi từ postmortem GitLab.com: replication đã broken (do WAL retention hết, replica bị bỏ xa), nhưng vì không có alert trên &lt;code&gt;replication lag &amp;gt; threshold&lt;/code&gt; nên team tưởng replica đang healthy. Khi cần fallback thì replica hoặc quá cũ, hoặc đã tự stop hẳn. Nguyên nhân kỹ thuật thường gặp: primary purge WAL segment cũ trước khi replica kịp fetch (Postgres không có replication slot, hoặc slot bị drop; MySQL binlog &lt;code&gt;expire_logs_days&lt;/code&gt; quá ngắn). Fix: bật replication slot (&lt;code&gt;primary_slot_name&lt;/code&gt; trên standby + &lt;code&gt;pg_create_physical_replication_slot&lt;/code&gt; trên primary) để primary giữ WAL đến khi replica đọc xong, KÈM alert khi slot lag vượt ngưỡng — vì slot cũng có tác dụng phụ: replica chết → WAL tích dồn → đầy disk primary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 3: semi-sync degrade thành async âm thầm.&lt;/strong&gt; MySQL semi-sync có timeout (&lt;code&gt;rpl_semi_sync_source_timeout&lt;/code&gt;): nếu không replica nào ack trong khoảng đó, primary fallback sang async để không block write. Đây là hành vi thiết kế — nhưng nếu network flap kéo dài, cluster chạy async suốt mà dashboard vẫn báo "semi-sync enabled". Client tưởng đang được bảo vệ. Postgres tương đương: &lt;code&gt;synchronous_commit = on&lt;/code&gt; với &lt;code&gt;synchronous_standby_names&lt;/code&gt; — nếu tất cả standby down, primary block write (an toàn hơn nhưng gây downtime); một số team hạ thành &lt;code&gt;local&lt;/code&gt; để tránh block, và quay lại failure mode 1. Không có lựa chọn miễn phí; phải hiểu config đang trade cái gì.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 4: quorum sai — &lt;code&gt;acks=1&lt;/code&gt; hoặc &lt;code&gt;min.insync.replicas=1&lt;/code&gt;.&lt;/strong&gt; Kafka topic replication factor 3 nhưng producer &lt;code&gt;acks=1&lt;/code&gt; chỉ cần leader ack; leader crash trước khi replicate sang follower là mất message đã ack. Fix chuẩn: &lt;code&gt;acks=all&lt;/code&gt; + &lt;code&gt;min.insync.replicas=2&lt;/code&gt; (với RF=3) — producer nhận NotEnoughReplicas exception khi số ISR tụt xuống dưới 2, và có thể chọn fail-fast thay vì ghi vào cluster đã giảm redundancy. Đây là mặc định khuyến nghị trong Kafka documentation cho durability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 5: split-brain khi failover không dùng fencing.&lt;/strong&gt; Primary bị coi là chết vì network partition, orchestrator promote standby thành primary mới, client mới ghi vào đó. Nhưng primary cũ chưa thực sự chết — chỉ mất kết nối tới orchestrator. Client cũ vẫn ghi vào primary cũ. Hai bản data diverge, không merge tự động được. Cách phòng: STONITH ("shoot the other node in the head") hoặc fencing bằng cách cắt VIP/DNS/network trước khi promote; hoặc dùng consensus-based system (etcd/Consul làm lease store) để chỉ một primary có lease tại một thời điểm. Async replication làm tình huống này tệ hơn hẳn sync, vì tại thời điểm split hai bản đã có khác biệt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cách debug và monitor
&lt;/h2&gt;

&lt;p&gt;Triệu chứng replication lag: sau khi ghi vào primary, read từ replica trong vài giây tiếp theo không thấy record (read-your-writes fail); sau failover, một số record "biến mất"; trên dashboard, khoảng cách LSN giữa primary và replica tăng dần và không giảm.&lt;/p&gt;

&lt;p&gt;Metric bắt buộc (Postgres, đọc từ &lt;code&gt;pg_stat_replication&lt;/code&gt; trên primary):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;write_lag&lt;/code&gt; — khoảng thời gian từ khi primary flush WAL local đến khi standby ack đã nhận. Đo lag mạng + xử lý của walreceiver.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flush_lag&lt;/code&gt; — thêm thời gian standby fsync WAL xuống disk local. Đây là mốc &lt;code&gt;synchronous_commit = on&lt;/code&gt; chờ.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;replay_lag&lt;/code&gt; — thêm thời gian standby thực sự apply xong WAL vào data file. Đây là mốc quyết định "read từ replica có thấy write mới không".&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)&lt;/code&gt; — bytes chưa replay, đơn vị byte thay vì thời gian. Đọc cùng với thời gian để phát hiện replica đã stall (lsn diff tăng mà replay không đuổi kịp).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pg_replication_slots.active&lt;/code&gt; — slot có đang được stream không. &lt;code&gt;false&lt;/code&gt; kéo dài + &lt;code&gt;restart_lsn&lt;/code&gt; giữ nguyên = replica đã disconnect, WAL đang tích trên primary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MySQL tương đương: &lt;code&gt;SHOW REPLICA STATUS&lt;/code&gt; (hoặc &lt;code&gt;SHOW SLAVE STATUS&lt;/code&gt; bản cũ) cho &lt;code&gt;Seconds_Behind_Source&lt;/code&gt;, &lt;code&gt;Replica_IO_Running&lt;/code&gt;, &lt;code&gt;Replica_SQL_Running&lt;/code&gt;, và &lt;code&gt;Retrieved_Gtid_Set&lt;/code&gt; vs &lt;code&gt;Executed_Gtid_Set&lt;/code&gt;. Chú ý &lt;code&gt;Seconds_Behind_Source&lt;/code&gt; chỉ là ước lượng: nếu IO thread đứt, giá trị vẫn có thể là 0 vì SQL thread đang apply relay log đã có — check cả &lt;code&gt;Replica_IO_Running=Yes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Kafka: &lt;code&gt;kafka-consumer-groups.sh --describe&lt;/code&gt; cho LAG per partition, JMX metric &lt;code&gt;UnderReplicatedPartitions&lt;/code&gt; cho topic có ISR &amp;lt; RF, &lt;code&gt;kafka-topics.sh --describe --under-min-isr-partitions&lt;/code&gt; để list topic đang không đủ quorum.&lt;/p&gt;

&lt;p&gt;Alert cần có: (1) lag thời gian vượt ngưỡng SLA (vd vài giây với read-heavy service, hàng chục giây với reporting replica); (2) lag byte tăng đơn điệu không giảm trong X phút = replica đang thua tốc độ ghi; (3) replication slot &lt;code&gt;active=false&lt;/code&gt; quá vài phút; (4) &lt;code&gt;UnderReplicatedPartitions &amp;gt; 0&lt;/code&gt; với Kafka; (5) &lt;code&gt;min.insync.replicas&lt;/code&gt; không đạt.&lt;/p&gt;

&lt;p&gt;Rule phòng: không bao giờ dùng replica để chống mất dữ liệu mà không có alert lag và không có backup riêng — replica là HA, không phải backup. Replica sao chép cả lệnh &lt;code&gt;DROP TABLE&lt;/code&gt; nhanh chóng.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoff
&lt;/h2&gt;

&lt;p&gt;Sync replication cho RPO = 0 (không mất transaction đã ack) đổi lại: mỗi write phải chờ RTT tới replica sync gần nhất, throughput commit giảm, và nếu tất cả standby sync đều down thì primary block write — HA của availability giảm để đổi lấy HA của data. Async cho latency thấp và write không phụ thuộc standby, đổi lại RPO &amp;gt; 0 và replica có thể trễ hàng chục giây dưới tải lớn, gây stale read và mất write khi failover. Quorum majority (Raft/Paxos, hoặc &lt;code&gt;W+R&amp;gt;N&lt;/code&gt;) cho balance: chấp nhận latency của N/2+1 replica gần nhất chứ không phải toàn bộ, và tự động chịu được N/2 node chết mà không cần failover thủ công — đổi lại complexity vận hành cao hơn và write latency đều bị ràng buộc bởi replica chậm thứ N/2+1. Càng nhiều replica càng an toàn dữ liệu và scale read tốt hơn, nhưng chi phí storage + network + độ khó vận hành tăng tuyến tính.&lt;/p&gt;

&lt;p&gt;Rule of thumb: data không được phép mất (payment, order, user identity) dùng sync (hoặc quorum majority) với ít nhất 2 replica remote-flush, alert khi lag &amp;gt; vài giây; data chấp nhận RPO vài giây (analytics, session, cache) dùng async với 1–2 replica, alert khi lag &amp;gt; 30 giây; luôn có backup off-cluster ngoài replica — replica sao chép cả lỗi người dùng.&lt;/p&gt;

&lt;h2&gt;
  
  
  Câu hỏi phỏng vấn
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Sync và async replication khác gì nhau, và khi nào chọn cái nào?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Async: primary commit local xong trả về client ngay, WAL/binlog stream sang replica ở background. Nhanh, không phụ thuộc replica còn sống, throughput cao, nhưng RPO &amp;gt; 0 — nếu primary chết trong khoảng chưa kịp stream, các transaction đã ack cho client biến mất khỏi hệ thống sau failover. Sync: primary chờ ít nhất một replica ack đã flush (hoặc apply) WAL rồi mới trả về client. RPO = 0 cho transaction đã ack, nhưng latency mỗi commit tăng theo RTT tới replica sync chậm nhất, và nếu tất cả replica sync down thì primary block write (Postgres) hoặc âm thầm degrade về async (MySQL semi-sync với timeout). Chọn theo cost of losing data: payment/order dùng sync với &lt;code&gt;synchronous_standby_names = 'ANY 1 (…)'&lt;/code&gt; và tối thiểu 2 standby (để một cái down không block write); analytics/session/cache dùng async vì mất vài giây record không sập nghiệp vụ. Điểm ăn điểm là nêu hai failure mode production: (1) async + failover = mất write đã ack (kịch bản dạng GitHub 2018 khi network partition kích hoạt promote replica đang trễ); (2) replica ngừng streaming âm thầm (GitLab.com 2017) vì không có alert trên replication lag và slot inactive, dẫn tới "có replica trên giấy nhưng không có replica lúc cần" — nên monitor &lt;code&gt;pg_stat_replication&lt;/code&gt; (write/flush/replay lag), Kafka &lt;code&gt;UnderReplicatedPartitions&lt;/code&gt;, và luôn có backup off-cluster riêng vì replica sao chép cả &lt;code&gt;DROP TABLE&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on
&lt;/h2&gt;

&lt;p&gt;Dựng một cluster Postgres primary + streaming standby trên Docker, tạo transaction, đo lag, kill primary và promote standby, so sánh khi chạy async vs sync.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; pg-repl-lab &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;pg-repl-lab
docker network create pgnet

&lt;span class="c"&gt;# --- primary ---&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; pg-primary &lt;span class="nt"&gt;--network&lt;/span&gt; pgnet &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 5432:5432 &lt;span class="se"&gt;\&lt;/span&gt;
  postgres:16 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nv"&gt;wal_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;replica &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nv"&gt;max_wal_senders&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nv"&gt;hot_standby&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;on &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nv"&gt;synchronous_commit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;on

&lt;span class="nb"&gt;sleep &lt;/span&gt;5
&lt;span class="c"&gt;# Tạo replication user + slot trên primary&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres pg-primary psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"CREATE ROLE repl WITH REPLICATION LOGIN PASSWORD 'rp';"&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres pg-primary psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT pg_create_physical_replication_slot('standby1');"&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres pg-primary bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"echo 'host replication repl 0.0.0.0/0 md5' &amp;gt;&amp;gt; /var/lib/postgresql/data/pg_hba.conf"&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres pg-primary psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT pg_reload_conf();"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bootstrap standby bằng &lt;code&gt;pg_basebackup&lt;/code&gt; rồi start ở hot-standby mode (dùng volume riêng cho từng node ở lab thật):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume create pgstandby
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--network&lt;/span&gt; pgnet &lt;span class="nt"&gt;-v&lt;/span&gt; pgstandby:/var/lib/postgresql/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rp postgres:16 &lt;span class="se"&gt;\&lt;/span&gt;
  pg_basebackup &lt;span class="nt"&gt;-h&lt;/span&gt; pg-primary &lt;span class="nt"&gt;-U&lt;/span&gt; repl &lt;span class="nt"&gt;-D&lt;/span&gt; /var/lib/postgresql/data &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-Fp&lt;/span&gt; &lt;span class="nt"&gt;-Xs&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nt"&gt;-S&lt;/span&gt; standby1 &lt;span class="nt"&gt;-c&lt;/span&gt; fast

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; pg-standby &lt;span class="nt"&gt;--network&lt;/span&gt; pgnet &lt;span class="nt"&gt;-p&lt;/span&gt; 5433:5432 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; pgstandby:/var/lib/postgresql/data &lt;span class="se"&gt;\&lt;/span&gt;
  postgres:16 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nv"&gt;hot_standby&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;on
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kiểm tra replication đang stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres pg-primary psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT application_name, state, write_lag, flush_lag, replay_lag,
          pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes
   FROM pg_stat_replication;"&lt;/span&gt;
&lt;span class="c"&gt;# kỳ vọng: state='streaming', các cột lag rất nhỏ hoặc NULL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bắn tải write để tạo lag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres pg-primary psql &lt;span class="nt"&gt;-d&lt;/span&gt; app &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"CREATE TABLE t(id serial primary key, v text, ts timestamptz default now());"&lt;/span&gt;

docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres pg-primary psql &lt;span class="nt"&gt;-d&lt;/span&gt; app &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"INSERT INTO t(v) SELECT md5(g::text) FROM generate_series(1,100000) g;"&lt;/span&gt;

&lt;span class="c"&gt;# đọc từ standby&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres pg-standby psql &lt;span class="nt"&gt;-d&lt;/span&gt; app &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT count(*) FROM t;"&lt;/span&gt;
&lt;span class="c"&gt;# so sánh với primary — khi lag còn tồn tại, hai giá trị khác nhau trong khoảnh khắc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bật sync mode và quan sát khác biệt: sửa &lt;code&gt;postgresql.conf&lt;/code&gt; trên primary thêm &lt;code&gt;synchronous_standby_names = 'ANY 1 (walreceiver)'&lt;/code&gt;, reload, rồi stop standby (&lt;code&gt;docker stop pg-standby&lt;/code&gt;) và thử insert. Với &lt;code&gt;synchronous_commit=on&lt;/code&gt; và không còn standby ack, &lt;code&gt;INSERT&lt;/code&gt; sẽ hang cho tới khi standby quay lại — đúng behavior "bảo vệ dữ liệu, hy sinh availability write".&lt;/p&gt;

&lt;p&gt;Test failover và đo RPO thực tế:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# quay lại async để dễ tái hiện data loss&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres pg-primary psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"ALTER SYSTEM SET synchronous_commit = 'local'; SELECT pg_reload_conf();"&lt;/span&gt;

&lt;span class="c"&gt;# ép lag lớn: pause standby, insert nhiều trên primary&lt;/span&gt;
docker pause pg-standby
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres pg-primary psql &lt;span class="nt"&gt;-d&lt;/span&gt; app &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"INSERT INTO t(v) SELECT 'lost-' || g FROM generate_series(1,5000) g;"&lt;/span&gt;

&lt;span class="c"&gt;# kill primary khi standby vẫn đang pause&lt;/span&gt;
docker stop pg-primary
docker unpause pg-standby

&lt;span class="c"&gt;# promote standby thành primary mới&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres pg-standby psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT pg_promote();"&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres pg-standby psql &lt;span class="nt"&gt;-d&lt;/span&gt; app &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT count(*) FROM t WHERE v LIKE 'lost-%';"&lt;/span&gt;
&lt;span class="c"&gt;# kỳ vọng: &amp;lt; 5000 — số row thiếu chính là RPO cụ thể tại thời điểm crash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lặp lại với &lt;code&gt;synchronous_commit = on&lt;/code&gt; + &lt;code&gt;synchronous_standby_names&lt;/code&gt; set — thao tác &lt;code&gt;INSERT&lt;/code&gt; khi standby paused sẽ block thay vì thành công, tức là không có write nào bị mất vì không có write nào được ack. Đây là bằng chứng trực tiếp cho tradeoff sync vs async.&lt;/p&gt;

&lt;p&gt;Cuối cùng, mô phỏng "replica chết âm thầm giữ WAL": pause standby dài, chạy loop insert liên tục vào primary, và quan sát &lt;code&gt;pg_wal_lsn_diff&lt;/code&gt; cùng dung lượng directory &lt;code&gt;pg_wal/&lt;/code&gt; trên primary tăng đều. Đây là failure mode "slot giữ WAL cho replica chết" — nếu để lâu primary sẽ hết disk và dừng nhận write. Alert cần bắt tình huống này ở giai đoạn &lt;code&gt;lag_bytes&lt;/code&gt; vượt ngưỡng, không đợi tới lúc disk full.&lt;/p&gt;

</description>
      <category>availability</category>
      <category>redundancyreplication</category>
      <category>dataredundancy</category>
      <category>consistency</category>
    </item>
    <item>
      <title>Load Balancing — Traffic Distribution</title>
      <dc:creator>Nghề không nhiều chẳng sắc</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:40:20 +0000</pubDate>
      <link>https://dev.to/nguoidungai/load-balancing-traffic-distribution-41f4</link>
      <guid>https://dev.to/nguoidungai/load-balancing-traffic-distribution-41f4</guid>
      <description>&lt;h1&gt;
  
  
  Traffic distribution: vì sao thuật toán chỉ là phần dễ, health check mới quyết định service có sập hay không
&lt;/h1&gt;

&lt;p&gt;Load balancer (LB) đứng trước một pool backend và quyết định request kế tiếp sẽ đi tới instance nào. Về lý thuyết nó chỉ là một router có thuật toán chọn; trong thực tế nó là điểm sống chết của service, vì lỗi phổ biến nhất không phải "thuật toán phân tải chưa tối ưu" mà là LB tiếp tục route traffic vào một backend đã chết — process crash, port còn mở, hoặc app trả 500 mà LB không biết. Kết quả là một phần request (1/N nếu round-robin) đều 5xx trong khi dashboard vẫn xanh vì "backend còn listen". Bài này nói về cơ chế phân tải và cái quan trọng hơn: health check phải sâu tới đâu, và LB tự nó redundant thế nào.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cơ chế hoạt động
&lt;/h2&gt;

&lt;p&gt;Có hai tầng LB phổ biến. L4 (TCP/UDP) chỉ nhìn 5-tuple (src IP, src port, dst IP, dst port, protocol) và forward gói — nhanh, protocol-agnostic, nhưng không biết đây là HTTP GET hay POST. Ví dụ: AWS NLB, HAProxy ở &lt;code&gt;mode tcp&lt;/code&gt;, IPVS, LVS. L7 (HTTP/gRPC) parse request, biết path/header/method, có thể route theo &lt;code&gt;Host&lt;/code&gt;, rewrite path, retry idempotent request, terminate TLS — chi phí là thêm latency và CPU per request. Ví dụ: nginx, HAProxy ở &lt;code&gt;mode http&lt;/code&gt;, Envoy, AWS ALB.&lt;/p&gt;

&lt;p&gt;Thuật toán chọn backend, chọn theo workload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;round-robin&lt;/code&gt; — luân phiên. Đơn giản, tốt khi request đồng đều về chi phí.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;weighted round-robin&lt;/code&gt; — mỗi backend một weight; instance to hơn nhận nhiều hơn.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;least-connections&lt;/code&gt; — chọn backend đang có ít active connection nhất. Tốt khi request có thời gian xử lý khác nhau nhiều (long-polling, streaming, upload).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IP hash&lt;/code&gt; / &lt;code&gt;consistent hash&lt;/code&gt; — hash một key (client IP, session ID, cache key) rồi map sang backend. Bắt buộc khi cần session affinity hoặc cache locality. Consistent hashing (Ketama, Maglev) hạn chế reshuffle khi thêm/bớt node — chỉ khoảng &lt;code&gt;1/N&lt;/code&gt; key phải remap thay vì gần hết.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;power-of-two-choices&lt;/code&gt; / &lt;code&gt;least-request&lt;/code&gt; — chọn ngẫu nhiên 2 backend, lấy cái ít load hơn. Envoy dùng mặc định vì tránh được "herding" của least-connections mà không cần global state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Một config nginx L7 điển hình, có health check và timeout — không phải toy example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;api_backend&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;least_conn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;10.0.1.11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt; &lt;span class="s"&gt;max_fails=3&lt;/span&gt; &lt;span class="s"&gt;fail_timeout=10s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;10.0.1.12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt; &lt;span class="s"&gt;max_fails=3&lt;/span&gt; &lt;span class="s"&gt;fail_timeout=10s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;10.0.1.13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt; &lt;span class="s"&gt;max_fails=3&lt;/span&gt; &lt;span class="s"&gt;fail_timeout=10s&lt;/span&gt; &lt;span class="s"&gt;backup&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;keepalive&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://api_backend&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_next_upstream&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt; &lt;span class="s"&gt;timeout&lt;/span&gt; &lt;span class="s"&gt;http_502&lt;/span&gt; &lt;span class="s"&gt;http_503&lt;/span&gt; &lt;span class="s"&gt;http_504&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_next_upstream_tries&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_connect_timeout&lt;/span&gt; &lt;span class="s"&gt;2s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt;    &lt;span class="s"&gt;10s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt;    &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;      &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Đây là passive health check (nginx OSS): sau &lt;code&gt;max_fails&lt;/code&gt; lỗi trong &lt;code&gt;fail_timeout&lt;/code&gt;, backend bị đánh dấu down và bỏ khỏi pool trong &lt;code&gt;fail_timeout&lt;/code&gt; giây. HAProxy hoặc nginx Plus có active health check — LB tự chủ động gõ một endpoint (&lt;code&gt;option httpchk GET /healthz&lt;/code&gt; trong HAProxy) theo interval, không cần chờ traffic thật để phát hiện chết.&lt;/p&gt;

&lt;p&gt;Health check phải phân biệt hai mức: liveness (process còn chạy không) và readiness (process có phục vụ được không — DB reachable, cache warm, migration xong). Route traffic dựa trên readiness. Kubernetes phân tách rõ hai loại; nếu LB chỉ ping TCP port thì đó là liveness giả — port mở không có nghĩa là app hoạt động.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vấn đề gặp trong production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 1: không có health check, hoặc chỉ check TCP.&lt;/strong&gt; LB thấy port 8080 accept SYN nên coi backend healthy, trong khi app đã deadlock hoặc mất kết nối DB. Cứ 1/N request rơi vào node đó là 500. Client retry thì retry qua cùng LB, có thể lại rơi vào cùng backend nếu sticky session. Đây là kịch bản "route traffic tới backend chết" — LB phân tải thành công 100%, nhưng vào cả xác.&lt;/p&gt;

&lt;p&gt;Sai:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;10.0.1.11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# không max_fails, không active check&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;10.0.1.12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# backend crash -&amp;gt; nginx vẫn gửi&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Đúng: bật passive check tối thiểu (như block ở trên), và ở tầng app expose &lt;code&gt;/healthz&lt;/code&gt; kiểm tra dependency thật:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Express readiness endpoint — trả 200 CHỈ khi thực sự phục vụ được&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/healthz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="c1"&gt;// DB pool alive?&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                         &lt;span class="c1"&gt;// cache reachable?&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;migrationsDone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warming up&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// LB sẽ pull khỏi pool&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rồi cấu hình LB gõ endpoint này (HAProxy &lt;code&gt;option httpchk GET /healthz&lt;/code&gt; với &lt;code&gt;http-check expect status 200&lt;/code&gt;, hoặc ALB target group health check path &lt;code&gt;/healthz&lt;/code&gt;). Kết quả: node mất DB tự động drain khỏi pool trong vài giây thay vì cả đêm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 2: health check quá nông hoặc quá sâu.&lt;/strong&gt; Nông (chỉ TCP, hoặc &lt;code&gt;/healthz&lt;/code&gt; trả 200 bất kể) route vào backend chết như trên. Sâu quá — &lt;code&gt;/healthz&lt;/code&gt; check cả chain DB + Redis + downstream service — thì một service downstream flap sẽ làm toàn bộ backend đồng loạt bị mark down, LB không còn node nào healthy, cluster tự sập dù backend vẫn chạy. Đây là "correlated failure": health check gộp dependency chung. Rule thực tế: readiness chỉ check dependency critical trực tiếp, không transitive. Nếu Redis chỉ dùng cache-aside có thể fallback, đừng đưa vào readiness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 3: LB không biết backend restart chậm (slow start).&lt;/strong&gt; Container mới join pool nhận full traffic ngay khi health check pass, nhưng JVM chưa warm, JIT chưa optimize, cache chưa fill — node mới trả latency cao, error nhiều, có thể oscillate down/up. HAProxy có &lt;code&gt;slowstart&lt;/code&gt; để ramp weight từ 0 lên full trong khoảng thời gian cấu hình; nginx Plus và Envoy có tương đương. Không dùng slow-start — mỗi deploy có một đợt lag spike.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 4: sticky session + auto-scaling.&lt;/strong&gt; Bật &lt;code&gt;ip_hash&lt;/code&gt; hoặc cookie-based affinity để giữ session ở một backend, rồi scale-out. Node mới join nhưng không nhận traffic mới (hash key đã map hết vào node cũ), trong khi node cũ oversubscribed. Khi scale-in, một loạt session mất. Fix: đưa session state ra ngoài (Redis, DB) để LB tự do rebalance; nếu bắt buộc affinity vì cache locality, dùng consistent hashing thay vì modulo, và chấp nhận phải rehash một phần khi topology đổi.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 5: LB là SPOF.&lt;/strong&gt; Một instance HAProxy/nginx bare-metal đứng trước 20 backend — nếu nó chết, mất tất. Cách xử lý thực tế: hai LB active-passive với &lt;code&gt;keepalived&lt;/code&gt;/VRRP chia sẻ virtual IP, hoặc active-active sau một anycast IP / cloud LB (ELB, GCLB) là managed và tự HA qua nhiều AZ. Đừng chỉ redundant backend mà quên chính lớp balancer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 6: retry storm khuếch đại lỗi.&lt;/strong&gt; LB set &lt;code&gt;proxy_next_upstream error timeout&lt;/code&gt; để failover request khi backend lỗi. Nếu tất cả backend đang overloaded và trả 503, LB retry sang backend khác cũng 503, cuối cùng mỗi request gốc thành N request nội bộ — traffic tự nhân N, backend càng chết nhanh. Cách hạn: giới hạn &lt;code&gt;proxy_next_upstream_tries&lt;/code&gt;, chỉ retry trên lỗi idempotent (không retry POST), và kết hợp với circuit breaker ở tầng app (Envoy có outlier detection tự eject node lỗi liên tục ra khỏi pool).&lt;/p&gt;

&lt;h2&gt;
  
  
  Cách debug và monitor
&lt;/h2&gt;

&lt;p&gt;Triệu chứng "LB đang route vào xác": error rate 5xx tăng nhưng chỉ ở tỷ lệ 1/N (N = số backend), latency phân bố bimodal — phần lớn request nhanh, một cụm timeout đúng bằng &lt;code&gt;proxy_read_timeout&lt;/code&gt;. Nếu nhìn tổng thể có thể trông như "spike nhẹ", nhưng breakdown per-upstream sẽ lộ ra một backend gánh 100% lỗi.&lt;/p&gt;

&lt;p&gt;Metric bắt buộc, đo theo per-backend (không phải chỉ tổng):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backend health status — up/down state. HAProxy phơi qua stats socket (&lt;code&gt;echo "show stat" | socat stdio /var/run/haproxy.sock&lt;/code&gt;) và trang stats HTTP; Envoy phơi qua &lt;code&gt;/clusters&lt;/code&gt; admin endpoint và Prometheus &lt;code&gt;envoy_cluster_membership_healthy&lt;/code&gt;; ALB/NLB có CloudWatch metric &lt;code&gt;HealthyHostCount&lt;/code&gt; và &lt;code&gt;UnHealthyHostCount&lt;/code&gt; per target group.&lt;/li&gt;
&lt;li&gt;Request distribution — request per second per backend. Skew lớn (một node nhận gấp mấy lần các node khác dù cùng weight) là dấu hiệu sticky-session bám cứng, hoặc một node bị mark down mà không ai để ý.&lt;/li&gt;
&lt;li&gt;Per-upstream latency và error rate — nginx log &lt;code&gt;$upstream_addr&lt;/code&gt;, &lt;code&gt;$upstream_status&lt;/code&gt;, &lt;code&gt;$upstream_response_time&lt;/code&gt;; HAProxy có &lt;code&gt;%si&lt;/code&gt; (server IP) và &lt;code&gt;%Tr&lt;/code&gt; (response time) trong log format. Không có phần &lt;code&gt;upstream_*&lt;/code&gt; trong log thì gần như không debug được LB.&lt;/li&gt;
&lt;li&gt;Active connections per backend — với least-connections cần metric này để verify LB đang thấy đúng con số; nếu LB nghĩ node A có nhiều conn trong khi thực tế app A idle, có thể là connection leak trong LB hoặc client giữ keepalive không đóng.&lt;/li&gt;
&lt;li&gt;Health check success rate — flap rate cao (node lên xuống liên tục) tệ hơn hẳn "down thẳng"; thường do check interval quá ngắn hoặc threshold quá nhạy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tool để đọc trực tiếp: HAProxy stats page hoặc &lt;code&gt;haproxy_exporter&lt;/code&gt; cho Prometheus; nginx &lt;code&gt;stub_status&lt;/code&gt; module + &lt;code&gt;nginx-prometheus-exporter&lt;/code&gt;; Envoy admin &lt;code&gt;/stats/prometheus&lt;/code&gt;; AWS Console — target group — &lt;code&gt;Targets&lt;/code&gt; tab để xem lý do unhealthy (thường là "Health checks failed with these codes: [502]"); &lt;code&gt;kubectl describe endpoints &amp;lt;svc&amp;gt;&lt;/code&gt; để xem endpoint controller đã eject pod chưa; &lt;code&gt;kubectl get endpointslices&lt;/code&gt; cho cluster mới hơn.&lt;/p&gt;

&lt;p&gt;Rule phòng ngừa: (1) mọi backend phải expose &lt;code&gt;/healthz&lt;/code&gt; khác &lt;code&gt;/&lt;/code&gt; — không dùng root làm health check, vì root có thể được cache/rewrite ngoài ý muốn; (2) alert khi bất kỳ backend nào ở trạng thái down quá vài phút; (3) alert khi distribution skew (max/min RPS per backend) vượt ngưỡng trong pool được cho là đồng nhất; (4) trong staging phải &lt;code&gt;kill -9&lt;/code&gt; một backend và verify LB drain đúng — không chỉ đọc config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoff
&lt;/h2&gt;

&lt;p&gt;L4 cho throughput cao và latency thấp (không parse payload, không terminate TLS), đổi lại mất khả năng route theo path/header, không retry được idempotent, khó observe. L7 cho routing thông minh, retry, TLS termination, WAF, đổi lại thêm chi phí CPU và một chút latency per request. Chọn theo protocol: gRPC/HTTP nên L7 để hưởng retry và load-aware routing; MySQL/Postgres/Kafka/Redis nên L4 vì L7 không hiểu protocol nội bộ. Health check nông (TCP) rẻ nhưng không phát hiện app dead-but-listening; health check sâu (đầy đủ dependency) bắt được nhiều lỗi hơn nhưng dễ tạo correlated failure khi một dependency phụ flap. Round-robin đơn giản, không cần state; least-connections xử lý tốt workload có phương sai, đổi lại phải track connection count (LB cần thấy chính xác, khó khi có nhiều LB instance không share state). Consistent hashing tối ưu cache locality, đổi lại phân bố không đều bằng round-robin. LB tự nó phân tải và HA cho backend, nhưng chính nó là chokepoint — phải HA bằng redundant LB + VRRP/anycast, nếu không thì chỉ đang dời SPOF từ backend sang LB.&lt;/p&gt;

&lt;p&gt;Rule of thumb: L7 cho HTTP-facing, L4 cho protocol khác; least-connections (hoặc power-of-two-choices) mặc định trừ khi có lý do cụ thể chọn khác; active health check trên &lt;code&gt;/readyz&lt;/code&gt; chỉ chứa dependency critical trực tiếp; luôn có ít nhất 2 LB instance chia virtual IP hoặc dùng managed LB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Câu hỏi phỏng vấn
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Thiết kế load balancer cho một API service 3 instance. Nên dùng thuật toán nào, health check ra sao, và làm sao để chính LB không thành single point of failure?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Chọn L7 vì đây là HTTP API — cần retry idempotent request, terminate TLS, và log per-upstream để debug. Thuật toán mặc định &lt;code&gt;least-connections&lt;/code&gt; (hoặc &lt;code&gt;power-of-two-choices&lt;/code&gt; nếu dùng Envoy) vì request API thường có phương sai thời gian xử lý; round-robin chỉ tối ưu khi request đồng đều. Health check phải là active, gõ &lt;code&gt;/readyz&lt;/code&gt; theo interval vài giây, threshold vài lần fail liên tiếp trước khi mark down và vài lần pass liên tiếp trước khi mark up (tránh flap). &lt;code&gt;/readyz&lt;/code&gt; chỉ trả 200 khi app thật sự phục vụ được — kiểm DB connection pool và cache reachable, nhưng không transitive tới các downstream service không critical, tránh correlated failure kéo cả pool xuống khi một dependency phụ flap. Cấu hình slow-start để node mới ramp weight từ 0 tránh cold-start lag, giới hạn &lt;code&gt;next_upstream_tries&lt;/code&gt; và chỉ retry lỗi idempotent để không tạo retry storm. Chính LB phải redundant: hai instance HAProxy/nginx active-passive share virtual IP qua keepalived/VRRP, hoặc dùng cloud managed LB (ALB, GCLB) tự HA qua nhiều AZ. Điểm ăn điểm là chỉ ra hai failure mode production thật: (1) health check quá nông nghĩa là LB vẫn route vào backend đã crash-nhưng-port-mở, biểu hiện là 1/N request 5xx dù dashboard tổng thể nhìn "gần OK"; (2) LB tự nó là chokepoint, redundant backend mà không redundant LB thì chỉ dời SPOF một tầng — phải giải quyết cả hai mới coi là HA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on
&lt;/h2&gt;

&lt;p&gt;Dựng một pool 3 backend + 1 HAProxy trên Docker, giết một backend, quan sát LB drain, rồi so sánh với config không có health check để thấy failure mode "route vào xác".&lt;/p&gt;

&lt;p&gt;Tạo backend Node.js đơn giản có &lt;code&gt;/healthz&lt;/code&gt; điều khiển được:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; lb-lab &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;lb-lab
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; server.js &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;JS&lt;/span&gt;&lt;span class="sh"&gt;'
const http = require('http');
const id = process.env.ID || 'x';
let healthy = true;
http.createServer((req, res) =&amp;gt; {
  if (req.url === '/healthz') {
    res.writeHead(healthy ? 200 : 503).end(healthy ? 'ok' : 'down');
  } else if (req.url === '/kill') {
    healthy = false; res.end('marked unhealthy');
  } else if (req.url === '/revive') {
    healthy = true; res.end('back');
  } else {
    res.end('hello from ' + id + '&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;');
  }
}).listen(8080, () =&amp;gt; console.log('backend ' + id + ' on 8080'));
&lt;/span&gt;&lt;span class="no"&gt;JS

&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Dockerfile &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;DOCKER&lt;/span&gt;&lt;span class="sh"&gt;'
FROM node:20-alpine
WORKDIR /app
COPY server.js .
CMD ["node", "server.js"]
&lt;/span&gt;&lt;span class="no"&gt;DOCKER

&lt;/span&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; lb-backend &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo HAProxy config có active health check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; haproxy.cfg &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;CFG&lt;/span&gt;&lt;span class="sh"&gt;'
global
    log stdout format raw local0
defaults
    mode http
    timeout connect 2s
    timeout client  10s
    timeout server  10s
    option httplog
frontend fe
    bind *:80
    default_backend be
backend be
    balance leastconn
    option httpchk GET /healthz
    http-check expect status 200
    default-server inter 2s fall 2 rise 2 slowstart 10s
    server b1 b1:8080 check
    server b2 b2:8080 check
    server b3 b3:8080 check
frontend stats
    bind *:8404
    stats enable
    stats uri /
&lt;/span&gt;&lt;span class="no"&gt;CFG
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chạy pool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network create lbnet
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; b1 &lt;span class="nt"&gt;--network&lt;/span&gt; lbnet &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;b1 lb-backend
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; b2 &lt;span class="nt"&gt;--network&lt;/span&gt; lbnet &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;b2 lb-backend
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; b3 &lt;span class="nt"&gt;--network&lt;/span&gt; lbnet &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;b3 lb-backend
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; lb &lt;span class="nt"&gt;--network&lt;/span&gt; lbnet &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:80 &lt;span class="nt"&gt;-p&lt;/span&gt; 8404:8404 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  haproxy:2.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bắn traffic đều và xem phân phối:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 30&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:8080/&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;span class="c"&gt;# kỳ vọng: xấp xỉ 10 request mỗi backend&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mở stats page ở &lt;code&gt;http://localhost:8404/&lt;/code&gt; — xem cột &lt;code&gt;Status&lt;/code&gt; (UP/DOWN), &lt;code&gt;LastChk&lt;/code&gt;, &lt;code&gt;Sessions&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Giờ ép một backend "chết ở tầng app" (port vẫn mở, &lt;code&gt;/healthz&lt;/code&gt; trả 503):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;b2 wget &lt;span class="nt"&gt;-qO-&lt;/span&gt; http://localhost:8080/kill
&lt;span class="c"&gt;# đợi vài giây cho HAProxy fail 2 check liên tiếp&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 30&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:8080/&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;span class="c"&gt;# kỳ vọng: b2 biến mất, b1 và b3 chia đôi traffic; không có 5xx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So sánh với version không có health check: sửa &lt;code&gt;haproxy.cfg&lt;/code&gt; bỏ &lt;code&gt;option httpchk&lt;/code&gt;, &lt;code&gt;http-check expect&lt;/code&gt;, và &lt;code&gt;check&lt;/code&gt; ở cuối mỗi dòng &lt;code&gt;server&lt;/code&gt;, rồi &lt;code&gt;docker restart lb&lt;/code&gt;. Kill lại &lt;code&gt;b2&lt;/code&gt; và bắn traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;b2 wget &lt;span class="nt"&gt;-qO-&lt;/span&gt; http://localhost:8080/kill
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 30&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; localhost:8080/&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;span class="c"&gt;# kỳ vọng: ~1/3 request trả 503 (rơi vào b2), 2/3 trả 200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Đây là chính kịch bản "route traffic tới backend chết" — LB phân tải thành công, nhưng vào cả xác. Bật lại health check và verify &lt;code&gt;curl&lt;/code&gt; không còn thấy 503 nào.&lt;/p&gt;

&lt;p&gt;Cuối cùng, dựng LB HA để thấy tại sao phải redundant chính LB: chạy hai container HAProxy giống nhau (&lt;code&gt;lb1&lt;/code&gt;, &lt;code&gt;lb2&lt;/code&gt;), đặt một tầng phía trước (một nginx tối giản round-robin giữa &lt;code&gt;lb1:80&lt;/code&gt; và &lt;code&gt;lb2:80&lt;/code&gt;, hoặc trên máy thật là &lt;code&gt;keepalived&lt;/code&gt; share virtual IP). Kill &lt;code&gt;lb1&lt;/code&gt;, verify traffic vẫn qua &lt;code&gt;lb2&lt;/code&gt;. Đó là mô hình tối thiểu để LB không thành SPOF; production nên dùng managed LB (ALB/NLB/GCLB) để không phải tự vận hành lớp này.&lt;/p&gt;

</description>
      <category>scalability</category>
      <category>loadbalancing</category>
      <category>trafficdistribution</category>
      <category>availability</category>
    </item>
    <item>
      <title>SARGability — Index-friendly SQL</title>
      <dc:creator>Nghề không nhiều chẳng sắc</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:40:17 +0000</pubDate>
      <link>https://dev.to/nguoidungai/sargability-index-friendly-sql-20d7</link>
      <guid>https://dev.to/nguoidungai/sargability-index-friendly-sql-20d7</guid>
      <description>&lt;h1&gt;
  
  
  SARGable predicate: vì sao &lt;code&gt;DATE(created_at) = ...&lt;/code&gt; giết index và bảng 50M row phải Seq Scan
&lt;/h1&gt;

&lt;p&gt;Một predicate được gọi là &lt;strong&gt;SARGable&lt;/strong&gt; (Search ARGument Able) khi planner có thể ánh xạ nó thẳng xuống cấu trúc index — cụ thể với B-Tree là ánh xạ vào key của leaf entry để đi tree hoặc scan range trên sibling. Khi cột bị &lt;strong&gt;bọc trong function&lt;/strong&gt; hay bị ép kiểu (cast) ở vế trái của WHERE, giá trị mà planner cần so khớp không còn là giá trị lưu trong index nữa; nó phải tính function cho từng row, tức là &lt;strong&gt;Seq Scan&lt;/strong&gt;. Đây là dạng "index có sẵn nhưng không dùng" gặp thường xuyên nhất trong production Postgres, và cũng là loại chậm khó nhìn nhất trong code review vì query "trông có filter". Việc sửa thường không phải thêm index — mà là &lt;strong&gt;viết lại predicate&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cơ chế hoạt động
&lt;/h2&gt;

&lt;p&gt;Postgres B-Tree lưu leaf entry theo giá trị &lt;strong&gt;nguyên bản&lt;/strong&gt; của cột (đúng hơn: theo output của opclass đã dùng lúc &lt;code&gt;CREATE INDEX&lt;/code&gt;). Planner nhìn WHERE clause, cố match từng predicate về dạng &lt;code&gt;indexed_column OP constant&lt;/code&gt; để tạo &lt;code&gt;Index Cond&lt;/code&gt;; dạng nào không match sẽ rớt xuống &lt;code&gt;Filter&lt;/code&gt; sau khi row đã được đọc. Với &lt;code&gt;WHERE created_at &amp;gt;= $1 AND created_at &amp;lt; $2&lt;/code&gt;, planner có thể đi thẳng vào leaf tại &lt;code&gt;$1&lt;/code&gt; rồi quét ngang tới &lt;code&gt;$2&lt;/code&gt;. Với &lt;code&gt;WHERE DATE(created_at) = $1&lt;/code&gt;, nó không có cách nào biết &lt;code&gt;DATE(created_at)&lt;/code&gt; bằng gì nếu không đọc từng row và tính — vì index lưu &lt;code&gt;timestamptz&lt;/code&gt;, không lưu &lt;code&gt;date&lt;/code&gt; của nó.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Bảng events 50M row, có index (btree) trên created_at&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;events_created_at_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- SARGable: predicate là range trên chính cột đã index&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-01'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="s1"&gt;'2026-06-02'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Index Scan using events_created_at_idx  (rows=... loops=1)&lt;/span&gt;
&lt;span class="c1"&gt;--   Index Cond: ((created_at &amp;gt;= ...) AND (created_at &amp;lt; ...))&lt;/span&gt;

&lt;span class="c1"&gt;-- Non-SARGable: cột bị bọc bởi function&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Seq Scan on events  (rows=... loops=1)&lt;/span&gt;
&lt;span class="c1"&gt;--   Filter: (date(created_at) = '2026-06-01'::date)&lt;/span&gt;
&lt;span class="c1"&gt;--   Rows Removed by Filter: 49_xxx_xxx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hai điều kiện logic tương đương, plan khác nhau hoàn toàn. Postgres &lt;em&gt;có&lt;/em&gt; hỗ trợ &lt;strong&gt;expression index&lt;/strong&gt; (&lt;code&gt;CREATE INDEX ON events (DATE(created_at))&lt;/code&gt;) để lưu chính giá trị của biểu thức và match lại được predicate — theo Postgres docs mục "Indexes on Expressions". Nhưng expression index chỉ khớp khi &lt;strong&gt;biểu thức trong WHERE và biểu thức trong CREATE INDEX giống hệt nhau về chuẩn hoá&lt;/strong&gt; (cùng function, cùng cast, cùng collation). Đổi từ &lt;code&gt;DATE(created_at)&lt;/code&gt; sang &lt;code&gt;created_at::date&lt;/code&gt; vẫn có thể miss index nếu chưa index đúng biểu thức đó — planner không "hiểu ngữ nghĩa", nó match theo cây biểu thức đã được canonicalize.&lt;/p&gt;

&lt;p&gt;Nguyên tắc rút ra: &lt;strong&gt;cột được index phải xuất hiện trần trụi ở vế trái&lt;/strong&gt; của phép so sánh; mọi biến đổi phải đẩy sang vế phải (constant, hoặc tham số). Đó là định nghĩa thực dụng của SARGable trong Postgres.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vấn đề gặp trong production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 1: &lt;code&gt;DATE()&lt;/code&gt;/&lt;code&gt;date_trunc()&lt;/code&gt; bọc cột timestamp.&lt;/strong&gt; Đây là mẫu phổ biến nhất từ code báo cáo và filter theo ngày.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Sai: bọc cột → không dùng được index trên created_at&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Đúng: viết lại thành range half-open, giữ cột trần&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-01'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="s1"&gt;'2026-06-02'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cách viết range còn xử lý đúng cả timezone và tiết kiệm không dùng function per-row. Nếu thực sự cần query theo ngày và không sửa được app, tạo expression index: &lt;code&gt;CREATE INDEX ON orders ((created_at AT TIME ZONE 'UTC')::date)&lt;/code&gt; — nhưng phải khớp &lt;em&gt;hệt&lt;/em&gt; biểu thức trong WHERE.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 2: &lt;code&gt;LOWER(email)&lt;/code&gt; cho case-insensitive lookup.&lt;/strong&gt; Rất hay xuất hiện ở table &lt;code&gt;users&lt;/code&gt; với query login.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Sai: LOWER() ở vế trái → Seq Scan trên bảng users triệu row&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hai lựa chọn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Cách 1: expression index (giữ nguyên query app)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;users_email_lower_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;-- Cách 2: đổi kiểu cột sang citext (case-insensitive text)&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;citext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;users_email_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Query giữ nguyên WHERE email = $1, so sánh tự động case-insensitive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chọn &lt;code&gt;citext&lt;/code&gt; nếu app luôn muốn ngữ nghĩa case-insensitive; chọn expression index nếu chỉ vài query cần.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 3: implicit cast ở vế trái.&lt;/strong&gt; Cột kiểu &lt;code&gt;bigint&lt;/code&gt; bị so sánh với text, hoặc ngược lại, khiến Postgres phải cast cột.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Sai: cast tường minh cột sang text để so string&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'12345'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Postgres đôi khi cast constant sang kiểu cột (SARGable giữ nguyên), nhưng nếu vế trái có cast tường minh thì index không match. Bắt lỗi này thường phải nhìn &lt;code&gt;EXPLAIN&lt;/code&gt; cho ra &lt;code&gt;Filter: ((user_id)::text = '12345'::text)&lt;/code&gt; — dấu ngoặc quanh &lt;code&gt;(user_id)::text&lt;/code&gt; là chỉ báo cột đã bị cast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 4: số học trên cột.&lt;/strong&gt; &lt;code&gt;WHERE price * 1.1 &amp;gt; 100&lt;/code&gt; hay &lt;code&gt;WHERE created_at + interval '30 days' &amp;lt; now()&lt;/code&gt; đều đẩy tính toán vào từng row.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Sai: cộng interval vào cột&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;-- Đúng: chuyển sang vế phải&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vế phải &lt;code&gt;now() - interval '30 days'&lt;/code&gt; được đánh giá &lt;strong&gt;một lần&lt;/strong&gt; thành constant tại thời điểm plan, index trên &lt;code&gt;created_at&lt;/code&gt; dùng được ngay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 5: &lt;code&gt;LIKE '%foo%'&lt;/code&gt; và leading wildcard.&lt;/strong&gt; B-Tree phục vụ prefix &lt;code&gt;LIKE 'foo%'&lt;/code&gt; (với opclass phù hợp) nhưng không phục vụ leading wildcard &lt;code&gt;'%foo'&lt;/code&gt; hay &lt;code&gt;'%foo%'&lt;/code&gt; — không có prefix để đi tree.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Sai: leading wildcard, index trên name không giúp&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%widget%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Đúng cho substring/fuzzy: GIN + pg_trgm&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_trgm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;products_name_trgm_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;gin_trgm_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;ILIKE&lt;/span&gt; &lt;span class="s1"&gt;'%widget%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure mode 6: &lt;code&gt;OR&lt;/code&gt; bắc cầu qua nhiều cột.&lt;/strong&gt; &lt;code&gt;WHERE user_id = $1 OR email = $2&lt;/code&gt; khiến planner khó dùng chung một index; nó có thể chọn &lt;code&gt;BitmapOr&lt;/code&gt; nếu có index trên cả hai cột, nhưng nếu chỉ có index trên &lt;code&gt;user_id&lt;/code&gt; thì cả câu Seq Scan. Cách sửa gọn nhất: viết lại thành &lt;code&gt;UNION ALL&lt;/code&gt; hai câu, mỗi câu là SARGable đơn lẻ.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cách debug và monitor
&lt;/h2&gt;

&lt;p&gt;Triệu chứng: một query "trông đơn giản" chậm bất thường khi bảng lớn dần; latency P99 tăng tuyến tính theo số row bảng chứ không theo số row trả về; CPU của Postgres cao mà buffer hit rate vẫn tốt (vì đang scan sequential mà data đã trong cache).&lt;/p&gt;

&lt;p&gt;Công cụ để dò:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt;&lt;/strong&gt; — dấu hiệu cần bắt: &lt;code&gt;Seq Scan&lt;/code&gt; kèm &lt;code&gt;Filter: (some_func(col) = ...)&lt;/code&gt;, hoặc &lt;code&gt;Rows Removed by Filter&lt;/code&gt; lớn hơn nhiều lần &lt;code&gt;rows&lt;/code&gt; cuối. So predicate xuất hiện ở &lt;code&gt;Index Cond:&lt;/code&gt; (tốt) hay &lt;code&gt;Filter:&lt;/code&gt; (rơi ra sau khi đọc row) — predicate rơi vào &lt;code&gt;Filter&lt;/code&gt; là dấu hiệu không SARGable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pg_stat_user_indexes&lt;/code&gt;&lt;/strong&gt; — cột &lt;code&gt;idx_scan&lt;/code&gt;. Nếu một index được kỳ vọng dùng cho query nóng mà &lt;code&gt;idx_scan&lt;/code&gt; gần bằng 0 sau khi workload đã chạy, khả năng cao query bên app đang viết non-SARGable. Đối chiếu với &lt;code&gt;pg_stat_user_tables.seq_scan&lt;/code&gt; của bảng đó: &lt;code&gt;seq_scan&lt;/code&gt; cao + &lt;code&gt;idx_scan&lt;/code&gt; thấp trên cùng bảng là chỉ báo mạnh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pg_stat_statements&lt;/code&gt;&lt;/strong&gt; — sort theo &lt;code&gt;total_exec_time&lt;/code&gt; hoặc &lt;code&gt;mean_exec_time&lt;/code&gt;. Query top thường lộ ra &lt;code&gt;DATE()&lt;/code&gt;, &lt;code&gt;LOWER()&lt;/code&gt;, &lt;code&gt;::text&lt;/code&gt; bọc cột. Đây là input chính để refactor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;auto_explain&lt;/code&gt;&lt;/strong&gt; — bật &lt;code&gt;auto_explain.log_min_duration&lt;/code&gt; để log plan của query chậm vào Postgres log; giúp bắt câu ad-hoc mà &lt;code&gt;pg_stat_statements&lt;/code&gt; chỉ giữ dạng normalized.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule phòng ngừa ở tầng code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code review bắt mọi WHERE clause có function/cast ở vế trái. Trong Python/Node ORM, cẩn thận helper kiểu &lt;code&gt;Order.filter(created_at__date=today)&lt;/code&gt; — nó thường generate ra &lt;code&gt;DATE(created_at) = ...&lt;/code&gt;. Ưu tiên helper dạng &lt;code&gt;__range&lt;/code&gt; hoặc &lt;code&gt;__gte/__lt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Với cột case-insensitive, quyết định &lt;em&gt;một lần&lt;/em&gt; ở schema: &lt;code&gt;citext&lt;/code&gt; hoặc expression index — đừng để nửa app dùng &lt;code&gt;LOWER()&lt;/code&gt; nửa app dùng &lt;code&gt;citext&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Với cột &lt;code&gt;timestamptz&lt;/code&gt;, thống nhất chuẩn hoá về UTC ở app, filter luôn dạng range half-open &lt;code&gt;[start, end)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tradeoff
&lt;/h2&gt;

&lt;p&gt;Hai hướng xử lý non-SARGable: &lt;strong&gt;viết lại SQL&lt;/strong&gt; hoặc &lt;strong&gt;thêm expression index&lt;/strong&gt;. Viết lại SQL rẻ hơn về mọi mặt (không tốn storage, không cost write, không cần deploy migration), nhưng đòi hỏi sửa code app hoặc ORM, và mọi call site phải đồng bộ. Expression index giữ nguyên query nhưng thêm một index thật vào bảng — mỗi INSERT/UPDATE trên cột base phải update index này; nếu expression phức tạp (function chậm, immutable-only), cost write tăng đáng kể. Ngoài ra expression index chỉ match khi biểu thức trong WHERE &lt;strong&gt;giống hệt&lt;/strong&gt; biểu thức lúc &lt;code&gt;CREATE INDEX&lt;/code&gt; (sau canonicalize) — dễ bị "index có mà không dùng" nếu app đổi cách viết. Với cột &lt;code&gt;updated_at&lt;/code&gt;/&lt;code&gt;created_at&lt;/code&gt; write-heavy, viết lại SQL luôn thắng expression index. Với legacy code không sửa được và một biểu thức ổn định (&lt;code&gt;LOWER(email)&lt;/code&gt;), expression index là lựa chọn đúng. Quy tắc thực tế: &lt;strong&gt;mặc định đổi SQL cho SARGable; chỉ thêm expression index khi không thể sửa call site và biểu thức là stable + immutable&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Câu hỏi phỏng vấn
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Query &lt;code&gt;SELECT * FROM orders WHERE DATE(created_at) = '2026-06-01'&lt;/code&gt; chạy chậm dù đã có index &lt;code&gt;btree(created_at)&lt;/code&gt;. Vì sao, và có mấy cách sửa?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;B-Tree lưu leaf entry theo giá trị &lt;code&gt;timestamptz&lt;/code&gt; nguyên bản của &lt;code&gt;created_at&lt;/code&gt;; planner match predicate về dạng &lt;code&gt;col OP const&lt;/code&gt; để tạo &lt;code&gt;Index Cond&lt;/code&gt;. Ở đây vế trái là &lt;code&gt;DATE(created_at)&lt;/code&gt; — một biểu thức trên cột, không phải cột trần — nên không có &lt;code&gt;Index Cond&lt;/code&gt; khớp được, planner rơi xuống &lt;code&gt;Seq Scan&lt;/code&gt; với &lt;code&gt;DATE(created_at) = ...&lt;/code&gt; nằm trong &lt;code&gt;Filter&lt;/code&gt;, tức là đọc hết bảng rồi mới lọc. Đây là non-SARGable predicate: cột bị bọc function ở vế trái. Có hai cách sửa. Cách một, viết lại thành range half-open &lt;code&gt;created_at &amp;gt;= '2026-06-01' AND created_at &amp;lt; '2026-06-02'&lt;/code&gt;; predicate lúc này SARGable, dùng &lt;code&gt;Index Scan&lt;/code&gt; trên index sẵn có, cost giảm từ O(N) xuống O(log N + k). Cách hai, tạo &lt;strong&gt;expression index&lt;/strong&gt; &lt;code&gt;CREATE INDEX ON orders (DATE(created_at))&lt;/code&gt;; khi ấy leaf lưu chính giá trị &lt;code&gt;date&lt;/code&gt;, predicate cũ match được — đổi lại phải trả cost write cho index mới và biểu thức trong WHERE phải khớp hệt biểu thức khi tạo index. Điểm ăn điểm là nêu được cả ba: cơ chế (index lưu giá trị cột nguyên bản, function ở vế trái phá match), hậu quả production (Seq Scan bảng lớn → P99 tăng theo size chứ không theo kết quả), và trade-off giữa viết lại SQL (rẻ, ưu tiên) và expression index (giữ nguyên app, đắt hơn write) — mặc định chọn cách một.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on
&lt;/h2&gt;

&lt;p&gt;Mục tiêu: dựng một bảng đủ lớn, tái hiện non-SARGable → Seq Scan, đo &lt;code&gt;EXPLAIN&lt;/code&gt;, refactor sang SARGable, so sánh, rồi thử luôn expression index để thấy trade-off.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; pg-sarg &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pw &lt;span class="nt"&gt;-p&lt;/span&gt; 5433:5432 postgres:16
&lt;span class="nb"&gt;sleep &lt;/span&gt;5

docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; pg-sarg psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="sh"&gt;'
CREATE TABLE events (
  id          bigserial PRIMARY KEY,
  user_id     bigint      NOT NULL,
  email       text        NOT NULL,
  created_at  timestamptz NOT NULL
);

INSERT INTO events (user_id, email, created_at)
SELECT (random()*1000000)::bigint,
       'user' || (random()*1000000)::bigint || '@Example.COM',
       now() - (random() * interval '365 days')
FROM generate_series(1, 5000000);

CREATE INDEX events_created_at_idx ON events (created_at);
CREATE INDEX events_email_idx      ON events (email);
ANALYZE events;
&lt;/span&gt;&lt;span class="no"&gt;SQL
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chạy phiên bản non-SARGable và ghi plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; pg-sarg psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM events
WHERE DATE(created_at) = (now() - interval '10 days')::date;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kỳ vọng: &lt;code&gt;Seq Scan on events&lt;/code&gt;, &lt;code&gt;Filter: (date(created_at) = ...)&lt;/code&gt;, &lt;code&gt;Rows Removed by Filter&lt;/code&gt; gần bằng row count bảng, thời gian hàng trăm ms tới vài giây.&lt;/p&gt;

&lt;p&gt;Refactor sang SARGable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; pg-sarg psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM events
WHERE created_at &amp;gt;= (now() - interval '10 days')::date
  AND created_at &amp;lt;  (now() - interval '9 days')::date;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kỳ vọng: &lt;code&gt;Index Scan&lt;/code&gt; (hoặc &lt;code&gt;Bitmap Index Scan&lt;/code&gt;) trên &lt;code&gt;events_created_at_idx&lt;/code&gt;, &lt;code&gt;Index Cond&lt;/code&gt; chứa hai vế range, thời gian sub-millisecond tới vài ms.&lt;/p&gt;

&lt;p&gt;Thử case &lt;code&gt;LOWER(email)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; pg-sarg psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM events WHERE LOWER(email) = 'user42@example.com';"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seq Scan như dự đoán. Thêm expression index và rerun:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; pg-sarg psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
CREATE INDEX events_email_lower_idx ON events (LOWER(email));
ANALYZE events;
EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM events WHERE LOWER(email) = 'user42@example.com';"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plan chuyển sang &lt;code&gt;Index Scan using events_email_lower_idx&lt;/code&gt;. Kiểm chứng cost write: đo TPS insert trước và sau khi có expression index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/insert.sql &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
INSERT INTO events (user_id, email, created_at)
VALUES ((random()*1000000)::bigint,
        'user' || (random()*1000000)::bigint || '@Example.COM',
        now());
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;docker &lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/insert.sql pg-sarg:/tmp/insert.sql

docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; pg-sarg psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"DROP INDEX events_email_lower_idx;"&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;pg-sarg pgbench &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /tmp/insert.sql &lt;span class="nt"&gt;-c&lt;/span&gt; 8 &lt;span class="nt"&gt;-j&lt;/span&gt; 2 &lt;span class="nt"&gt;-T&lt;/span&gt; 20 postgres

docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; pg-sarg psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"CREATE INDEX events_email_lower_idx ON events (LOWER(email));"&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;pg-sarg pgbench &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /tmp/insert.sql &lt;span class="nt"&gt;-c&lt;/span&gt; 8 &lt;span class="nt"&gt;-j&lt;/span&gt; 2 &lt;span class="nt"&gt;-T&lt;/span&gt; 20 postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TPS lần hai sẽ thấp hơn lần đầu — đây là chi phí thật của expression index, đối trọng để cân nhắc so với việc refactor query.&lt;/p&gt;

&lt;p&gt;Kiểm tra index usage tổng thể:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; pg-sarg psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
SELECT indexrelname, idx_scan, idx_tup_read
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan DESC;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Index nào &lt;code&gt;idx_scan = 0&lt;/code&gt; dù có query kỳ vọng dùng nó → soi lại call site xem WHERE có bọc cột không. Dọn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; pg-sarg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>advancedquery</category>
      <category>sargability</category>
      <category>indexfriendlysql</category>
      <category>indexes</category>
    </item>
  </channel>
</rss>
