Fix lỗi Redis Streams 'ERR The ID specified in XADD is equal or smaller than the target stream top item ID'

intermediate🔴 Redis2026-07-07| Redis 5.0+ trên Linux/macOS/Windows, mọi thư viện client (redis-py, ioredis, Jedis, v.v.)

Error Message

ERR The ID specified in XADD is equal or smaller than the target stream top item ID
#redis#streams#xadd

Tình huống

Bạn gọi XADD trên một stream và Redis trả về lỗi:

ERR The ID specified in XADD is equal or smaller than the target stream top item ID

Lỗi này hầu như luôn xuất phát từ một trong ba nguyên nhân: bạn đang truyền tường minh một ID đã được dùng, đồng hồ hệ thống bị nhảy lùi, hoặc bạn đang phát lại các sự kiện lịch sử vào một stream đang hoạt động. Cả ba đều có thể khắc phục được.

Tại sao Redis từ chối ghi

ID của entry trong stream theo định dạng <milliseconds>-<sequence>, và mỗi entry mới phải có giá trị lớn hơn entry cuối cùng một cách tuyệt đối. Đây không phải quirk — đây là thiết kế có chủ đích. Truyền vào một ID bằng hoặc nhỏ hơn ID đỉnh hiện tại, Redis sẽ từ chối ghi ngay lập tức. Không có ghi đè âm thầm, không có bỏ qua entry.

Kiểm tra ID đỉnh hiện tại trước khi debug tiếp:

# Lấy entry cuối cùng trong stream
XREVRANGE mystream + - COUNT 1

# Hoặc kiểm tra metadata của stream
XINFO STREAM mystream

So sánh last-generated-id với ID bạn đang cố chèn vào. Bằng hoặc nhỏ hơn? Bạn đã xác định được nguyên nhân.

Cách sửa nhanh: ngừng chỉ định ID thủ công

Không có yêu cầu bắt buộc phải dùng ID tường minh? Hãy chuyển sang tự động sinh ID với *. Redis sẽ tự gán một ID <ms>-<seq> duy nhất, và lỗi này sẽ biến mất hoàn toàn.

# Thay vì làm thế này (ID tường minh — nguy hiểm):
XADD mystream 1720000000000-0 event_type login user_id 42

# Hãy làm thế này (auto ID — an toàn):
XADD mystream * event_type login user_id 42

Chỉ thay một ký tự. Vấn đề biến mất trong hầu hết các trường hợp.

Nếu bạn cần ID tường minh: dùng partial auto-increment

Đôi khi bạn muốn kiểm soát phần millisecond của ID — để đảm bảo thứ tự giữa nhiều producer — nhưng theo dõi số sequence thủ công lại rất phiền phức. Redis 7.0 có một giải pháp trung gian: chỉ truyền vào timestamp và để Redis xử lý sequence.

# Redis tự chọn sequence number cho millisecond này
XADD mystream 1720000000000-* event_type login user_id 42

Định dạng <ms>-* cho phép hai producer dùng chung cùng một millisecond timestamp mà không xung đột. Redis tự tăng sequence. Không cần theo dõi thủ công, không có conflict.

Clock skew — nguyên nhân khó phát hiện

Đồng bộ NTP, live migration VM, khởi động lại container — bất kỳ điều nào trong số này đều có thể đẩy đồng hồ hệ thống của bạn chạy lùi. Khi điều đó xảy ra, stream đã lưu các ID từ "tương lai" so với thời gian đã được hiệu chỉnh. Mỗi lần XADD * tiếp theo sẽ tạo ra timestamp nhỏ hơn entry cuối cùng, khiến các lệnh ghi bắt đầu thất bại.

Chẩn đoán nhanh:

# Kiểm tra ID cuối cùng mà stream ghi nhận
XINFO STREAM mystream
# Xem: last-generated-id

# Kiểm tra thời gian hiện tại của server
TIME

Nếu last-generated-id có timestamp lớn hơn giá trị TIME trả về, clock skew chính là thủ phạm.

Cách khắc phục: Redis 6.2+ xử lý trường hợp này một cách linh hoạt với các auto-generated ID. Thay vì dùng thời gian đồng hồ hiện tại (đang nhỏ hơn), Redis sẽ tái sử dụng timestamp của ID cuối và tiếp tục tăng sequence. Vì vậy XADD mystream * vẫn hoạt động bình thường. Tuy nhiên, ID tường minh sẽ không được bảo vệ — những trường hợp đó vẫn sẽ thất bại.

Với Redis < 6.2, các lựa chọn của bạn là: chờ đồng hồ tự bắt kịp, hoặc dùng partial auto-increment (<ms>-*) với timestamp tốt cuối cùng đã biết.

Phát lại sự kiện lịch sử vào stream đang tồn tại

Đây là tình huống khiến nhiều team mắc bẫy: bạn đang backfill một stream với các sự kiện cũ — chẳng hạn timestamp từ hai tuần trước — nhưng stream đã có các entry từ hôm nay. Bạn không thể chèn ID cũ hơn vào sau ID mới hơn. Redis không cho phép điều đó.

Ba hướng giải quyết:

  • Ghi vào stream riêng biệt — giữ mystream cho sự kiện trực tiếp và mystream:historical cho backfill. Gộp lại khi đọc bằng XREAD hoặc logic ứng dụng.
  • Tạo lại stream theo thứ tự — nếu stream đủ mới để có thể bỏ đi, hãy xóa và phát lại sự kiện theo thứ tự thời gian:
DEL mystream
# Phát lại sự kiện từ cũ nhất đến mới nhất với ID tường minh
XADD mystream 1719000000000-0 event_type purchase amount 99
XADD mystream 1719000001000-0 event_type refund amount 99
XADD mystream 1720000000000-0 event_type login user_id 42
  • Dịch chuyển timestamp về phía trước — với các lần phát lại không nhạy cảm về thời gian, hãy đẩy timestamp cũ vượt qua ID đỉnh hiện tại. Bạn mất đi độ chính xác về mặt thời gian, nhưng các lệnh ghi sẽ thành công.

Race condition giữa nhiều producer

Nhiều service tự sinh ID tường minh — mỗi service dựa vào đồng hồ cục bộ của mình — là một vụ xung đột đang chờ xảy ra. Hai producer cùng một millisecond: ai ghi sau sẽ nhận lỗi. Chỉ cần chậm hơn 1ms cũng có thể kích hoạt lỗi này.

Hãy chuyển sang partial auto-increment <ms>-* (Redis 7.0+) hoặc dùng luôn * để tự động sinh ID hoàn toàn. Với Redis < 7.0, bạn có thể ghép một prefix riêng cho từng producer vào trường sequence, nhưng cách này nhanh chóng trở nên rối rắm. Nâng cấp lên 7.0 là hướng đi gọn gàng hơn.

Xác minh

Sau khi áp dụng cách khắc phục, hãy xác nhận rằng các lệnh ghi đang hoạt động đúng:

# Ghi một entry thử nghiệm
XADD mystream * test ok

# Xác nhận nó đã được ghi vào
XREVRANGE mystream + - COUNT 1
# Nên trả về entry mới của bạn với một ID hợp lệ

# Kiểm tra độ dài stream đã tăng lên
XLEN mystream

Một lệnh XADD thành công sẽ trả về chuỗi ID như "1720012345678-0", không phải lỗi. Nếu bạn đang dùng client library, hãy kiểm tra rằng ID được trả về không phải null và error handler của bạn đã im lặng.

Tham khảo nhanh

  • Auto ID (an toàn nhất): XADD key * field value
  • Partial auto-increment (Redis 7.0+): XADD key <ms>-* field value
  • Clock skew sau NTP: auto ID xử lý được trong Redis 6.2+; tránh dùng ID tường minh
  • Phát lại dữ liệu lịch sử: dùng stream riêng biệt hoặc phát lại theo thứ tự thời gian
  • Xung đột nhiều producer: dùng * hoặc <ms>-*, không dùng ID tường minh đầy đủ

Related Error Notes