Dev Study
AWSサービスの内部原理 コース

47. 可視性タイムアウトはロックである — 削除されない、見えなくなるだけ

Receiveしたメッセージがなぜ「消えない」のか、そして消費側が途中で死んでも失われないのはなぜかを、1本のメッセージのライフサイクルを図で追っていきます。

① Receiveは時限リースの取得

Producer
Consumer が ReceiveMessage
キューで待機可視状態
配信された瞬間にタイマー開始
不可視 (in-flight)キューに残ったまま消えない
取り出しではなく状態の切替
時限ロック成立他のコンシューマから見えない
  • 公式の言い回し: 「When you receive a message from an Amazon SQS queue, it remains in the queue but becomes temporarily invisible to other consumers.」— 受信してもキューに残る。消えるのではなく「一時的に他のコンシューマから不可視」になるだけ。
  • 「The visibility timeout starts as soon as a message is delivered to you.」— タイマーは配信された瞬間に始まる。
  • デフォルトの可視性タイムアウトは30秒(キュー単位。メッセージ単位でも設定可)。

Receiveは「メッセージを取り出す」操作ではなく、「このメッセージを一定時間だけ自分がリース(占有)する」操作。OS/分散システムで学ぶ期限付きロック(リース)そのもので、他の誰かが同じメッセージを同時に掴めなくするための可視性の切り替えにすぎない。

② 2フェーズがメッセージを失わせない

ReceiveMessageフェーズ1
結末は2つ
処理を実行in-flight のまま
Delete or タイムアウト満了
処理が成功
処理前にクラッシュDeleteが呼ばれない
DeleteMessageフェーズ2: キューから消滅
再び可視化キューに再出現
別のコンシューマが拾える
  • 削除は自動ではない。明示的に DeleteMessage を呼ぶ(一部SDKは処理成功時の自動削除をサポート)。
  • 公式: 「If you don't process and delete a message before the visibility timeout expires—due to application errors, crashes, or connectivity problems—the message becomes visible again in the queue.」
  • 「This ensures that messages aren't lost even if the initial processing fails.」— これが「メッセージを失わない」仕組みそのもの。

Receiveで消さず、処理完了後に明示的にDeleteする「2フェーズ構造」が肝。Delete前にコンシューマが死ねば、タイムアウト後にメッセージが再出現する。これはDynamoDB編・Lambda編で見た at-least-once(少なくとも1回)の正体そのもの — 「処理したのに再配信される」のは、処理成功からDeleteまでの間にクラッシュ(あるいは処理が可視性タイムアウトを超過)したケースだ。だから消費側は冪等に作る必要がある。

③ ハートビート延長と12時間の壁

ReceiveMessageT=0・デフォルト30秒のリース
繰り返し延命 — ただし
可視性を延長ChangeMessageVisibility
ハートビート: 例 20秒ごとに+30秒
超えるなら
12時間の絶対上限起点は最初のReceiveに固定
延長しても起点はリセットされない
Step Functionsタスク分割を公式が案内
  • 「Implement a heartbeat mechanism to periodically extend the visibility timeout」— ChangeMessageVisibility で処理中に周期的に延長するのがハートビートパターン。
  • 「the visibility timeout has a maximum limit of 12 hours from when the message is first received. Extending the timeout doesn't reset this 12-hour limit.」— 12時間が上限で、延長しても起点はリセットされない。
  • 逆方向の操作: VisibilityTimeout を0秒に指定すると即座に可視化される(「This immediately makes the message available for other consumers」)。「処理しない」と決めたら即返却できる。

リースは固定ではなく、ChangeMessageVisibility で延長も短縮もできる。処理が長引くならハートビートで延命し、逆に「これは処理できない」と判断したら0秒指定で即座に手放して他のコンシューマに委ねる。ただし延長には12時間という絶対の天井があり、その起点は最初のReceive時刻に固定される。

④ in-flight という枠 — リースの上限

in-flightReceive済み・Delete前
上限到達時の挙動はポーリング方式で違う
標準キュー約120,000件が上限
FIFOキューアクティブなグループ依存
ショートポーリングOverLimit エラーを返す
ロングポーリングエラーなしで新規を返さない
枠が空くまで待つ
  • 「in-flight messages are messages that have been received by a consumer but not yet deleted.」
  • 「For standard queues, there's a limit of approximately 120,000 in-flight messages, depending on queue traffic and message backlog」— 「約」12万件で、トラフィックやバックログに依存する。FIFOは「limits depend on active message groups」。
  • ショートポーリング時は OverLimit エラー、ロングポーリング時はエラーを返さず枠が空くまで新規メッセージを返さない。

リース(in-flight)は無制限には持てない。標準キューで約12万件という上限は、「Deleteが滞る=処理が詰まっている」ことを検知させる仕掛けでもある。Lambda編・API Gateway編で見たポーリング vs プッシュの議論と同じく、ここでもショート/ロングポーリングで上限到達時の振る舞いが分かれる。

⑤ 入口で隠す遅延キューとの対比

遅延キューDelaySeconds
可視性タイムアウト
期間の範囲
Send時に隠す入口で不可視化 (誰も見ていない)
Receive後に隠す消費後の作業中ロック
可視に戻る条件
0秒〜最大15分デフォルト(=最小)0秒
30秒〜最大12時間デフォルト30秒
遅延満了で可視に初めて配信可能になる
Deleteか満了で決着
  • 遅延キュー:「a message is hidden when it is first added to queue」/ 可視性タイムアウト:「a message is hidden only after it is consumed from the queue」— 公式が挙げる両者の違い。
  • DelaySeconds:「The default (minimum) delay for a queue is 0 seconds. The maximum is 15 minutes.」
  • 15分を超える遅延スケジューリングが必要なら EventBridge Scheduler が推奨、と公式が案内。

両者とも「一定時間メッセージを見えなくする」点は同じだが、隠すタイミングが正反対。遅延キューは配信そのものを先送りする「入口の隠蔽」、可視性タイムアウトは消費後の「作業中ロック」。混同しやすいが、隠す起点(Send時 vs Receive時)で見分けられる。

⑥ 再出現の終着点 — DLQ という隔離先

ReceiveReceiveCount +1
maxReceiveCount と比較
タイムアウト満了再び可視化=再出現
隔離の目的
以下: 再Receive先頭へ戻ってループ
超過: DLQへ移動隔離される
本流を守る毒メッセージの循環を停止
  • redrive policy が maxReceiveCount を指定:「The maxReceiveCount is the number of times a consumer can receive a message from a source queue before it is moved to a dead-letter queue.」設定可能な範囲は1〜1,000。
  • maxReceiveCount=1 なら1回の失敗でDLQ行き。公式は「set the maxReceiveCount high enough to allow for sufficient retries.」と案内。
  • DLQは同一アカウント・同一リージョン必須(「must be in the same AWS account and Region as the source queue」)。また元キューと同じ型にする必要がある(「a FIFO queue can only use a FIFO DLQ, and a standard queue can only use a standard DLQ」)。
  • 目的:「preventing them from repeatedly circulating in the main queue.」

②〜③で見た「再出現」は無限には続かない。再出現(=Receive)のたびに ReceiveCount が増え、maxReceiveCount を超えると DLQ(デッドレターキュー)へ隔離される。処理できない「毒メッセージ」が本流を延々と回り続けてin-flight枠やスループットを食い潰すのを防ぐ、ライフサイクルの終着点だ。こうしてSQSは「リース→処理→明示Delete」の2フェーズに、12時間の壁・in-flight上限・DLQという歯止めを組み合わせ、失わない配信(at-least-once)を成立させている。

公式ドキュメントで詳しく ↗