状況
ストリームに対して XADD を呼び出すと、Redisが以下のエラーを返します:
ERR The ID specified in XADD is equal or smaller than the target stream top item ID
これはほぼ確実に3つのいずれかが原因です。既に使用されたIDを明示的に渡している、システムクロックが逆行した、またはライブストリームに過去のイベントを再投入しようとしている、のどれかです。いずれも修正可能です。
RedisがWriteを拒否する理由
ストリームエントリのIDは <ミリ秒>-<シーケンス> という形式に従っており、新しいエントリは必ず直前のものより厳密に大きくなければなりません。これは仕様上の欠陥ではなく、意図的な設計です。現在の最上位IDと同じかそれ以下のIDを渡すと、Redisはそのwriteを即座に拒否します。サイレントな上書きも、エントリのスキップも行われません。
デバッグを進める前に、現在の最上位IDを確認してください:
# ストリームの最後のエントリを取得
XREVRANGE mystream + - COUNT 1
# またはストリームのメタデータを確認
XINFO STREAM mystream
last-generated-id と挿入しようとしているIDを比較してください。同じかそれ以下であれば、原因が特定できたことになります。
簡単な修正:IDの手動指定をやめる
明示的なIDが必須でなければ、* による自動生成に切り替えましょう。Redisが一意の <ms>-<seq> IDを自動的に割り当てるため、このエラーは完全に解消されます。
# こちらではなく(明示的ID — 危険):
XADD mystream 1720000000000-0 event_type login user_id 42
# こちらを使う(自動ID — 安全):
XADD mystream * event_type login user_id 42
たった1文字の変更で、ほとんどのケースで問題が解決します。
明示的なIDが必要な場合:部分的な自動インクリメントを使う
プロデューサー間の順序付けのためにIDのミリ秒部分を制御したいこともありますが、シーケンス番号を手動で管理するのは手間がかかります。Redis 7.0にはその中間点があります。タイムスタンプのみを渡し、シーケンスはRedisに任せる方法です。
# このミリ秒のシーケンス番号をRedisが選択する
XADD mystream 1720000000000-* event_type login user_id 42
<ms>-* 形式を使うと、2つのプロデューサーが同じミリ秒タイムスタンプを衝突なく共有できます。Redisがシーケンスを自動的にインクリメントするため、手動での追跡も競合も発生しません。
クロックスキュー — 気づきにくい原因
NTPの同期、VMのライブマイグレーション、コンテナの再起動など、これらのいずれもシステムクロックを逆行させる可能性があります。それが起きると、ストリームには補正後の時刻より「未来」のIDがすでに記録されています。その後の XADD * は最後のエントリより小さいタイムスタンプを生成するため、writeが失敗し始めます。
素早く診断するには:
# ストリームが認識している最後のIDを確認
XINFO STREAM mystream
# 確認項目:last-generated-id
# 現在のサーバー時刻を確認
TIME
last-generated-id のタイムスタンプが TIME の返す値より進んでいる場合、クロックスキューが原因です。
**修正方法:**Redis 6.2以降では、自動生成IDに対してこの問題がうまく処理されます。(現在より小さい)現在のクロック時刻を使う代わりに、Redisは最後のIDのタイムスタンプを再利用してシーケンスをインクリメントし続けます。そのため XADD mystream * は引き続き動作します。ただし明示的なIDにはこの保護が適用されないため、それらは依然として失敗します。
Redis 6.2未満の場合の選択肢は、クロックが自然に追いつくのを待つか、最後の正常なタイムスタンプを使って部分的な自動インクリメント(<ms>-*)を使用することです。
既存ストリームへの過去イベントの再投入
多くのチームが陥るシナリオがあります。2週間前のタイムスタンプを持つ古いイベントでストリームをバックフィルしようとしているが、そのストリームにはすでに今日のエントリがある場合です。新しいIDの後に古いIDを挿入することはできません。Redisはそれを許可しません。
解決策は3つあります:
- 別のストリームに書き込む — ライブイベントは
mystreamに、バックフィルはmystream:historicalに分けます。読み取り時にXREADまたはアプリケーションロジックでマージします。 - ストリームを順番通りに再作成する — ストリームが新しく捨てても問題なければ、削除してイベントを時系列順に再投入します:
DEL mystream
# 最も古いものから順に明示的なIDで再投入
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
- タイムスタンプを前にずらす — 時刻の正確さが重要でない再投入であれば、古いタイムスタンプを現在の最上位IDより後にずらします。時系列の正確さは失われますが、writeは通るようになります。
複数プロデューサー間の競合状態
複数のサービスがそれぞれのローカルクロックを基に独自の明示的IDを生成すると、衝突が起きるのは時間の問題です。同じミリ秒に2つのプロデューサーが書き込もうとした場合、後から書き込んだ方がエラーになります。1ms遅れるだけでも発生し得ます。
<ms>-* 部分的自動インクリメント(Redis 7.0以降)か、完全な * 自動生成に切り替えてください。Redis 7.0未満では、シーケンスフィールドにプロデューサー固有のプレフィックスを仕込む方法もありますが、すぐに複雑になります。7.0へのアップグレードがよりすっきりした解決策です。
動作確認
修正を適用したら、writeが正常に行われているか確認してください:
# テストエントリを書き込む
XADD mystream * test ok
# 書き込まれたか確認
XREVRANGE mystream + - COUNT 1
# 有効なIDを持つ新しいエントリが返されるはず
# ストリームの長さが増えたか確認
XLEN mystream
成功した XADD はエラーではなく、"1720012345678-0" のようなID文字列を返します。クライアントライブラリを使っている場合は、返されたIDがnullでないこと、そしてエラーハンドラが静かになっていることを確認してください。
クイックリファレンス
- 自動ID(最も安全):
XADD key * field value - 部分的自動インクリメント(Redis 7.0以降):
XADD key <ms>-* field value - NTP後のクロックスキュー:Redis 6.2以降では自動IDで対応可能。明示的なIDは避ける
- 過去データの再投入:別ストリームを使うか、時系列順に再投入する
- 複数プロデューサーの衝突:完全な明示的IDではなく
*または<ms>-*を使う

