Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Testing rate limit, retry, idempotency

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn đang test một API đặt vé xem phim của CGV vào tối thứ Sáu. Người dùng bấm nút "Thanh toán", mạng 4G chập chờn, app không nhận được phản hồi nên tự động gửi lại request. Nếu backend xử lý cả hai request như hai giao dịch riêng, khách hàng bị trừ tiền hai lần cho một ghế. Đó chính xác là loại lỗi mà idempotency (tính bất biến khi lặp lại) sinh ra để phòng chống. Cùng lúc đó, nếu hàng nghìn người cùng spam nút thanh toán, hệ thống phải có rate limit (giới hạn tần suất) để không sập, và client phải biết retry (thử lại) một cách lịch sự thay vì dồn dập.

Ba khái niệm rate limit, retry và idempotency luôn đi cùng nhau như một bộ ba. Rate limit là cơ chế server tự bảo vệ. Retry là cách client phản ứng khi bị từ chối hoặc gặp lỗi tạm thời. Idempotency là hợp đồng đảm bảo retry không gây tác hại. Là một QA/tester API, bạn không chỉ kiểm tra "happy path" mà phải chủ động chứng minh rằng hệ thống hành xử đúng dưới áp lực và khi request bị lặp. Trong bài này, chúng ta sẽ dùng Postman để thiết kế các test tự động cho cả ba cơ chế đó — đọc header giới hạn, mô phỏng backoff, và xác nhận rằng gửi hai lần vẫn chỉ tạo ra một kết quả.

Khái niệm cốt lõi

Rate limit và các header chuẩn

Rate limit giới hạn số request một client được phép gửi trong một khoảng thời gian (ví dụ 100 request/phút). Khi vượt ngưỡng, server trả về mã trạng thái 429 Too Many Requests — được định nghĩa trong RFC 6585. Cùng với đó, server thường gửi kèm các header cho client biết tình trạng hạn mức:

X-RateLimit-Limit:     100        // tổng hạn mức trong cửa sổ thời gian
X-RateLimit-Remaining: 23         // số request còn lại
X-RateLimit-Reset:     1717050000 // thời điểm (Unix timestamp) hạn mức reset
Retry-After:           30         // (khi 429) chờ bao nhiêu giây rồi thử lại

Lưu ý một điểm hay gây nhầm: chưa có chuẩn tuyệt đối cho tên header. Nhiều API dùng tiền tố X-RateLimit-, nhưng bản draft RFC mới (IETF RateLimit-) bỏ tiền tố X-. GitHub API dùng X-RateLimit-*, trong khi một số gateway lại dùng RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset. Header Retry-After có thể là số giây (30) hoặc một HTTP-date (Wed, 30 Jun 2026 12:00:00 GMT). Là tester, bạn phải kiểm tra đúng tên và đúng định dạng mà tài liệu API của mình quy định, đừng giả định.

Retry và exponential backoff

Khi client gặp lỗi tạm thời (429, hoặc 5xx như 502/503), việc thử lại ngay lập tức là phản tác dụng — nó chỉ làm server đã quá tải càng thêm quá tải. Chiến lược đúng là exponential backoff (lùi theo cấp số nhân): lần 1 chờ 1 giây, lần 2 chờ 2 giây, lần 3 chờ 4 giây, lần 4 chờ 8 giây... Thêm vào đó là jitter (nhiễu ngẫu nhiên) để tránh hiệu ứng "thundering herd" — tất cả client cùng retry đúng một thời điểm.

Không phải lỗi nào cũng nên retry. Lỗi 4xx do client sai (400 Bad Request, 401 Unauthorized, 422 Validation) thì retry vô nghĩa vì gửi lại vẫn sai. Chỉ retry với lỗi được xem là "tạm thời": 429, 500, 502, 503, 504, hoặc lỗi mạng (timeout, connection reset). Là tester, bạn cần kiểm tra rằng client tôn trọng Retry-After và không retry những lỗi không nên retry.

Idempotency — chìa khóa để retry an toàn

Một thao tác idempotent nghĩa là thực hiện nó một lần hay nhiều lần đều cho cùng một kết quả trên trạng thái hệ thống. Theo bản chất HTTP: GET, PUT, DELETE được thiết kế idempotent, còn POST thì không — mỗi POST thường tạo một tài nguyên mới. Đây chính là chỗ nguy hiểm: nếu retry một POST tạo đơn hàng, bạn có thể tạo hai đơn.

Giải pháp phổ biến là Idempotency-Key: client sinh một khóa duy nhất (thường là UUID) và gửi kèm trong header. Server lưu khóa này; nếu nhận lại cùng khóa trong một khoảng thời gian, nó trả về kết quả đã lưu thay vì xử lý lại. Stripe là ví dụ kinh điển với header Idempotency-Key. Nhiều cổng thanh toán Việt Nam cũng có cơ chế tương đương thông qua một mã giao dịch duy nhất (VNPay dùng vnp_TxnRef, MoMo dùng orderId/requestId) — bản chất chính là idempotency key mang tên khác.

Tình huống thực tế

Ví dụ 1: Tiki và cơn bão Sale 12.12

Một team QA tại một sàn TMĐT giả định (gọi là "ShopViet") chuẩn bị cho đợt Sale 12.12. API POST /api/v2/orders có rate limit 60 request/phút cho mỗi user token. Trong buổi test, tester chỉ gửi lần lượt từng request và thấy mọi thứ đều 200 OK, nên kết luận "ổn". Đến ngày Sale thật, app mobile của người dùng — do timeout 10 giây — tự retry, và hệ thống nhận được lượng POST orders gấp 3 lần dự kiến. Kết quả: hàng loạt đơn trùng, kho bị trừ sai, khách khiếu nại bị trừ tiền hai lần.

Diễn giải: Lỗi ở đây không phải server thiếu rate limit, mà là team QA không test hành vi lặp và không kiểm tra idempotency của endpoint POST. Sau sự cố, team thêm header Idempotency-Key và viết test Postman gửi cùng một key hai lần, khẳng định lần thứ hai trả về đúng order_id của lần đầu chứ không tạo đơn mới.

Bài học: Test "gửi một lần thành công" là chưa đủ. Với mọi endpoint làm thay đổi trạng thái (tạo đơn, trừ tiền), phải test kịch bản retry và chứng minh idempotency.

Ví dụ 2: Grab và bài toán rate limit theo tier

Một fintech Đông Nam Á cung cấp API tra cứu số dư ví. Họ có hai gói: gói Free giới hạn 100 request/giờ, gói Business 10.000 request/giờ. Team QA cần đảm bảo rate limit áp đúng theo từng gói. Họ viết một collection Postman chạy vòng lặp gửi 105 request với token gói Free, kiểm tra rằng request thứ 101 trở đi trả về 429 kèm Retry-After, và header X-RateLimit-Remaining giảm dần chính xác từ 99 về 0.

Trong lúc test, họ phát hiện một bug: X-RateLimit-Reset trả về sai múi giờ (server dùng UTC nhưng tính toán theo giờ Việt Nam GMT+7), khiến hạn mức "reset" trễ 7 tiếng. Nhờ test tự động so sánh timestamp, họ bắt được lỗi trước khi lên production.

Bài học: Rate limit không chỉ là "có trả 429 không", mà còn là các con số trong header có chính xác và nhất quán không. Test tự động rất mạnh ở việc kiểm tra tính chính xác từng bước của bộ đếm.

Ví dụ 3: Idempotency ở cổng thanh toán VNPay sandbox

Một team tích hợp VNPay cho website bán khóa học. Họ test luồng tạo giao dịch bằng cách gửi cùng một vnp_TxnRef (mã đơn hàng) hai lần liên tiếp để mô phỏng trường hợp người dùng bấm "Thanh toán" hai lần. Đúng chuẩn, VNPay phải nhận diện vnp_TxnRef trùng và không tạo giao dịch mới. Team viết test Postman xác nhận response code trả về là mã "đơn hàng đã tồn tại" chứ không phải tạo mới thành công.

Bài học: Với cổng thanh toán, mã giao dịch chính là idempotency key. Việc test trùng mã là bắt buộc để tránh rủi ro nghiêm trọng nhất trong fintech: khách bị trừ tiền hai lần cho một giao dịch.

Hướng dẫn từng bước

Bước 1: Test rate limit headers trong tab Tests

Với request bất kỳ tới endpoint có rate limit, thêm assertion đọc và kiểm tra các header. Đặt đoạn code sau vào tab Tests (hoặc Post-response trong Postman phiên bản mới):

// Kiểm tra header rate limit tồn tại và hợp lệ
pm.test("Có đủ header rate limit", function () {
    pm.response.to.have.header("X-RateLimit-Limit");
    pm.response.to.have.header("X-RateLimit-Remaining");
});

const remaining = Number(pm.response.headers.get("X-RateLimit-Remaining")); const limit = Number(pm.response.headers.get("X-RateLimit-Limit"));

pm.test("Remaining hợp lệ và không vượt Limit", function () { pm.expect(remaining).to.be.at.least(0); pm.expect(remaining).to.be.at.most(limit); });

// Lưu remaining vào biến để so sánh ở request kế tiếp pm.collectionVariables.set("prev_remaining", remaining);

Bước 2: Chứng minh bộ đếm giảm dần

Gửi cùng endpoint hai lần liên tiếp. Ở lần thứ hai, so sánh remaining hiện tại với giá trị đã lưu:

const current = Number(pm.response.headers.get("X-RateLimit-Remaining"));
const prev = Number(pm.collectionVariables.get("prev_remaining"));

pm.test("Remaining giảm sau mỗi request", function () { pm.expect(current).to.be.below(prev); }); pm.collectionVariables.set("prev_remaining", current);

Bước 3: Test hành vi 429 và Retry-After

Dùng Collection Runner chạy request nhiều iteration để vượt ngưỡng, rồi kiểm tra khi bị chặn:

if (pm.response.code === 429) {
    pm.test("429 có kèm Retry-After", function () {
        pm.response.to.have.header("Retry-After");
    });
    const retryAfter = Number(pm.response.headers.get("Retry-After"));
    pm.test("Retry-After là số dương hợp lý", function () {
        pm.expect(retryAfter).to.be.above(0);
        pm.expect(retryAfter).to.be.below(3600); // dưới 1 giờ
    });
}

Bước 4: Mô phỏng retry với exponential backoff

Postman không có cơ chế retry tự động sẵn, nhưng bạn có thể dùng pm.sendRequest trong tab Tests để thử lại với backoff. Đây là pattern hữu ích cho endpoint hay trả 503:

function callWithBackoff(attempt) {
    const maxAttempts = 4;
    const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s

pm.sendRequest(pm.request, function (err, res) { if (!err && res.code === 200) { pm.test("Thành công sau retry", () => pm.expect(res.code).to.eql(200)); return; } if (attempt < maxAttempts && (res.code === 429 || res.code >= 500)) { const jitter = Math.random() * 300; // thêm nhiễu setTimeout(() => callWithBackoff(attempt + 1), delay + jitter); } else { pm.test("Đã hết lượt retry", () => pm.expect(attempt).to.be.below(maxAttempts)); } }); }

Lưu ý: setTimeout trong sandbox Postman có giới hạn, và cách chuyên nghiệp hơn để test retry ở quy mô lớn là dùng Newman kết hợp script Node.js. Nhưng cho việc kiểm tra thủ công một luồng, pattern trên đủ dùng.

Bước 5: Test idempotency với Idempotency-Key

Đây là bước quan trọng nhất. Trong tab Pre-request Script, sinh một UUID và giữ cố định qua các lần gửi:

// Chỉ sinh key mới nếu chưa có (để retry dùng lại đúng key)
if (!pm.collectionVariables.get("idem_key")) {
    pm.collectionVariables.set("idem_key", pm.variables.replaceIn("{{$guid}}"));
}

Thêm header Idempotency-Key: {{idem_key}} vào request. Gửi lần đầu, lưu order_id:

// Tab Tests, lần gửi đầu
const body = pm.response.json();
pm.collectionVariables.set("first_order_id", body.order_id);

Gửi lần hai với cùng key, xác nhận idempotency:

const body = pm.response.json();
pm.test("Idempotency: cùng key trả về cùng order_id", function () {
    pm.expect(body.order_id).to.eql(pm.collectionVariables.get("first_order_id"));
});
pm.test("Không tạo đơn trùng", function () {
    // Server nên trả 200/201 với đơn cũ, không phải tạo mới
    pm.expect(pm.response.code).to.be.oneOf([200, 201]);
});

Lỗi thường gặp & mẹo

Nhầm tên header rate limit. Đừng viết cứng X-RateLimit-Remaining nếu API của bạn dùng RateLimit-Remaining (không có X-). Luôn đọc tài liệu API và dùng đúng tên. Mẹo: log toàn bộ header một lần bằng console.log(pm.response.headers.toObject()) để biết chính xác server trả gì.

Retry cả những lỗi không nên retry. Retry một 400 hay 422 chỉ tốn tài nguyên vô ích và có thể che giấu bug thật. Chỉ retry 429 và 5xx. Trong test, hãy khẳng định rõ: "với 400, client KHÔNG được retry".

Quên jitter, gây thundering herd. Nếu tất cả client retry đúng thời điểm Retry-After, server lại bị dội một đợt đồng loạt. Luôn cộng thêm một khoảng ngẫu nhiên nhỏ vào thời gian chờ.

Sinh Idempotency-Key mới ở mỗi lần retry. Đây là lỗi phá vỡ toàn bộ ý nghĩa idempotency. Key phải giữ nguyên xuyên suốt một logic-request và các lần retry của nó. Mẹo đã nêu ở Bước 5: chỉ sinh key khi biến chưa tồn tại.

Không dọn state giữa các lần chạy test. Nếu bạn lưu idem_key vào collection variable và không xóa, lần chạy test sau sẽ dùng lại key cũ và cho kết quả sai lệch. Thêm bước reset ở đầu collection hoặc dùng pm.collectionVariables.unset("idem_key") sau khi test xong.

Tin vào timestamp Reset mà không kiểm tra múi giờ. Như ví dụ Grab, sai múi giờ giữa UTC và GMT+7 là bug âm thầm rất khó phát hiện nếu không có assertion so sánh cụ thể. So X-RateLimit-Reset với Date.now()/1000 và kiểm tra khoảng chênh lệch có hợp lý không.

Mẹo về Retry-After dạng HTTP-date. Nếu server trả Retry-After là một ngày giờ HTTP chứ không phải số giây, hãy parse bằng new Date(headerValue).getTime() rồi tính hiệu với hiện tại, đừng ép kiểu Number() (sẽ ra NaN).

Bài tập thực hành

  • Test bộ đếm rate limit: Chọn một public API có rate limit (ví dụ GitHub API GET /users/{user} — dùng X-RateLimit-*). Viết test đọc Limit, Remaining, Reset, gửi 3 request liên tiếp và khẳng định Remaining giảm dần đúng 1 đơn vị mỗi lần.
  • Bắt lỗi 429: Dùng Collection Runner với một API sandbox có rate limit thấp, cấu hình chạy đủ số iteration để vượt ngưỡng. Viết assertion: khi code là 429 thì phải có Retry-After và giá trị của nó là số dương hợp lệ.
  • Chứng minh idempotency: Với một endpoint POST tạo tài nguyên (có thể dùng mock server hoặc VNPay sandbox), gửi hai lần với cùng Idempotency-Key (hoặc cùng vnp_TxnRef). Khẳng định lần hai trả về cùng ID/mã giao dịch của lần đầu và KHÔNG tạo bản ghi mới.
  • Chứng minh không-idempotency là bug: Gửi hai POST với hai key khác nhau và xác nhận chúng tạo hai tài nguyên riêng biệt — để hiểu rõ ranh giới giữa hành vi đúng và sai.
  • Retry có chọn lọc: Viết một script phân loại: với danh sách response code mẫu [200, 400, 429, 500, 503], log ra code nào "nên retry" và code nào "không nên", đúng theo nguyên tắc đã học.

Tóm tắt

Rate limit, retry và idempotency là bộ ba không thể tách rời khi test API dưới điều kiện thực tế. Rate limit bảo vệ server, thể hiện qua mã 429 và các header X-RateLimit-Limit/Remaining/Reset cùng Retry-After — tester phải kiểm tra cả sự tồn tại lẫn độ chính xác của các con số này. Retry là cách client phản ứng lịch sự với lỗi tạm thời, dùng exponential backoff cộng jitter, và chỉ áp dụng cho 429/5xx chứ không cho lỗi client 4xx. Idempotency là hợp đồng đảm bảo retry không gây tác hại: dùng Idempotency-Key (hoặc mã giao dịch như vnp_TxnRef) để cùng một thao tác lặp lại vẫn chỉ tạo một kết quả.

Trong Postman, bạn dùng tab Tests để đọc và assert header rate limit, dùng Collection Runner để đẩy hệ thống vượt ngưỡng và bắt 429, dùng Pre-request Script để cố định idempotency key, và dùng pm.sendRequest để mô phỏng backoff. Nguyên tắc cốt lõi cần nhớ: đừng bao giờ dừng lại ở "gửi một lần thành công". Với mọi endpoint thay đổi trạng thái, hãy luôn tự hỏi "nếu request này chạy hai lần thì sao?" — và viết test để trả lời câu hỏi đó. Đó là ranh giới giữa một tester bình thường và một tester ngăn được sự cố khách bị trừ tiền hai lần.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi