Azure API Management(APIM) でバックエンド呼び出し時に「The underlying connection was closed…」が出る場合、接続を再利用しようとした瞬間にサーバー側で切断されていたケースが典型です。本記事では、APIM の retry ポリシーで同一リクエストを再送する設定と、失敗が続くときの実務チェックポイントを解説します。
「接続維持エラー」が起きる仕組み(keep-alive と接続再利用)
エラーメッセージに含まれる 「A connection that was expected to be kept alive was closed by the server」 は、APIM がバックエンドへ送る HTTP 接続を「使い回そう(Keep-Alive/コネクションプール)」としたときに、バックエンド側がすでにその接続を閉じていた…という状況を強く示唆します。
APIM は高いスループットを出すために、バックエンドへの接続を毎回新規作成するのではなく、一定の範囲で再利用します。しかしバックエンドのロードバランサー、アプリ基盤、WAF、プロキシ、アプリ自体が「アイドル(無通信)になった接続」をタイムアウトで黙って切断していると、APIM 側では次のリクエストで“まだ生きているはず”の接続を使おうとして失敗します。
| タイミング | APIM 側で起きていること | バックエンド側の状態 | 見える結果 |
|---|---|---|---|
| 負荷が落ち着き、しばらく通信がない | 過去に使った接続をプールに保持 | アイドルタイムアウト等で接続を終了(サイレントクローズ) | 表面上は何も起きない |
| 次のリクエストが到着 | プールから既存接続を再利用しようとする | すでに接続は閉じられている | APIM で「connection was closed」系の例外 |
| リトライが有効な場合 | 新規接続を張り直して再送 | 新しい接続では正常に処理できる | 最終的に 200/201 等で成功することがある |
つまり、このエラーは「APIM の設定ミス」というよりも、“接続再利用”と“バックエンド側のアイドル切断”が噛み合ったときに起きる一時的な接続障害として扱うのが現実的です。そこで推奨される対策が retry(リトライ)ポリシーです。
結論:BackendConnectionFailure を条件にした retry + buffer-request-body が基本形
APIM の公式シナリオでいう Scenario 5(BackendConnectionFailure) に該当する場合、まずは BackendConnectionFailure のときだけリトライする構成が基本になります。ポイントは次の 2 つです。
- retry の条件を「BackendConnectionFailure」に絞る(何でもかんでも再送しない)
- 同一リクエストを再送するために buffer-request-body=”true” を必ず付ける
<policies>
<inbound>
<!-- ここに他のポリシー -->
</inbound>
<backend>
<retry condition="@(context.LastError?.Reason == "BackendConnectionFailure")"
count="3"
interval="1"
first-fast-retry="true">
<forward-request buffer-request-body="true" />
</retry>
</backend>
<outbound>
<!-- ここに他のポリシー -->
</outbound>
<on-error>
<!-- ここに他のポリシー -->
</on-error>
</policies>
この形にしておくと、最初の試行で keep-alive の再利用に失敗しても、リトライで新しい接続を張り直し、同じリクエスト(ボディを含む)をバックエンドへ再送できます。
retry ポリシーの各パラメータを「意味で」理解する
retry は「とりあえず回数を増やす」だけだと効果が出ないことがあります。まずは各属性が何をしているかを押さえると、原因に対して適切なチューニングがしやすくなります。
| 属性 | 役割 | 実務での考え方 |
|---|---|---|
| condition | リトライ対象のエラー条件 | 接続系の一時障害だけに限定する。HTTP 4xx/業務エラーまで再送すると事故(重複処理)になりやすい。 |
| count | 最大リトライ回数 | まず 3 回程度で様子見。短時間スパイクやアイドル切断なら 1〜2 回目で復帰することが多い。継続障害なら回数を増やしても成功しない。 |
| interval | リトライ間隔(秒) | 1 秒は「すぐ張り直せる」前提の設定。バックエンドが詰まり気味なら 2〜3 秒に伸ばして“呼吸”させる。 |
| first-fast-retry | 最初のリトライを即時に行うか | keep-alive の再利用失敗は「張り直せばすぐ成功」が多いため true が相性良い。逆にバックエンド過負荷時は false の方が優しい場合もある。 |
重要なのは、retry は「エラーの発生をゼロにする」仕組みではなく、失敗した後に自動で立て直して“最終的に成功させる確率を上げる”仕組みだという点です。APIM のトレースやログには、1 回目の失敗として例外が残ることがありますが、それ自体は設計上の前提に含めます。
同じリクエストを再送するための必須設定:buffer-request-body=”true”
POST/PUT/PATCH など、ボディを持つリクエストを再送したい場合、<forward-request buffer-request-body=”true” /> は必須です。これがないと、APIM が一度読み取ったリクエストボディを保持できず、リトライ時に「同じ内容」を送り直せません。
ただし、ボディをバッファするということは、APIM 側でメモリ(または内部バッファ)に内容を保持するということでもあります。巨大な JSON、ファイルアップロード、長大なフォームデータなどでは、性能・コスト・制限に影響する可能性があります。
| リクエストの種類 | buffer-request-body の重要度 | 注意点 |
|---|---|---|
| GET/HEAD(通常ボディなし) | 低い | ボディがないため再送の難易度は低い。とはいえ条件を絞らない全リトライは避ける。 |
| POST/PUT/PATCH(JSON など) | 非常に高い | 未指定だと“再送したつもりでもボディが空”になり得る。必ず true にして意図通り再送する。 |
| 大きなボディ(ファイル等) | 要検討 | バッファリングが重い場合がある。アップロード API は別経路に分ける、チャンク化する、クライアント側でリトライする等の設計も検討する。 |
「再送できればOK」ではない:重複実行(idempotency)を必ず考える
APIM 側で同じリクエストを再送できるようになっても、バックエンド処理が 非冪等(同じリクエストを 2 回実行すると 2 回分の副作用が出る)場合は注意が必要です。典型例は「注文作成」「決済」「メール送信」「ポイント付与」などで、リトライにより重複が起きると重大な事故になります。
接続維持エラーのような一時障害は、APIM がバックエンドからのレスポンスを受け取る前に切れている可能性があります。つまりバックエンド側では すでに処理が完了していたのに、APIM が結果を受け取れずにリトライするというケースがあり得ます。これを踏まえて、次のような「重複を潰す仕組み」をセットで用意すると安全です。
| 対策 | やること | 効果 |
|---|---|---|
| Idempotency-Key を導入 | クライアント→APIM→バックエンドへ一意キーを渡し、バックエンドが同一キーの重複実行を抑止する | POST の二重発火を現実的に防げる |
| 自然な冪等化(PUT/UPSERT) | 「作成」ではなく「このIDの状態をこうする」に寄せる設計に変更する | リトライしても結果が一貫しやすい |
| サーバー側の重複検知 | 注文番号・トランザクションIDなど業務キーでユニーク制約をかける | 最後の砦として重複を止められる |
| リトライ条件を限定 | BackendConnectionFailure など“通信の入口”で失敗したものだけ再送する | アプリ内部で起きた業務エラーまで再送する事故を減らす |
3回リトライしても失敗する場合に見るべきポイント
retry ポリシーが入っているのに最終的に失敗する場合、原因は「ポリシーの書き方」以外にあることがほとんどです。現場で切り分けを早めるために、次の順で確認すると迷いにくくなります。
APIM のトレースで context.LastError.Reason を確認する
まずは「本当に BackendConnectionFailure が出ているか」を確認します。APIM のトレースでは、失敗時の Reason が表示されます。ここが想定と違う場合、条件式が合っていないためリトライが発火していない、またはリトライすべきでない別の問題が起きています。
| よくある Reason(例) | 意味のイメージ | 次のアクション |
|---|---|---|
| BackendConnectionFailure | 接続再利用失敗、接続確立の一時失敗など | retry 条件の対象。buffer-request-body を有効にして再送。 |
| RequestTimeout | バックエンド応答が間に合わない | バックエンドの処理時間・タイムアウト整合・スロットリングを確認。短間隔リトライは悪化要因になり得る。 |
| ConnectError(環境により名称は異なる) | ネットワーク的に接続できない | 継続障害の可能性が高い。DNS、FW、VNet、バックエンドの稼働状況を優先して見る。 |
上の表はあくまで代表例です。実際の値は環境・構成によって揺れることがあるため、トレースに出た値を一次情報として扱うのが確実です。
retry の配置場所が <backend> になっているか
retry は <backend> セクションの中に置くのが基本です。<inbound> や <on-error> に置くと、「バックエンド呼び出しを再実行する」という意図とズレた動きになったり、期待通りのタイミングで forward-request が評価されなかったりします。
<policies>
<inbound>...</inbound>
<backend>
<retry ...>
<forward-request buffer-request-body="true" />
</retry>
</backend>
<outbound>...</outbound>
<on-error>...</on-error>
</policies>
バックエンド/ロードバランサーのアイドルタイムアウトを疑う
APIM が再利用しようとする接続が“黙って切られる”背景には、バックエンド手前のロードバランサーやアプリ基盤の設定が関係していることが多いです。次のような観点で確認します。
- アイドルタイムアウトが短すぎないか(数十秒〜数分で切っていないか)
- Keep-Alive 設定(サーバーが積極的に close していないか)
- 特定の経路(WAF/プロキシ/Service Mesh)だけで切断が増えていないか
- TLS 終端や HTTP/2⇔HTTP/1.1 変換の境目で不整合が起きていないか
この手の設定は「APIM だけ」触っても改善しないことがあります。retry はあくまで“つなぎ直し”であり、毎回同じ場所で切られるなら、バックエンド側のタイムアウト・上限・負荷を調整しない限り成功率は上がりません。
バックエンド過負荷やスロットリングも同時に確認する
接続維持エラーに見えて、実際はバックエンドが詰まっていて接続確立が不安定になっているケースもあります。次のサインがあれば、リトライ回数を増やすより先に負荷要因を取る方が近道です。
- 同時刻に 5xx が増える/CPU・メモリが張り付く
- バックエンド側ログに接続拒否・キュー溢れ・スレッド枯渇が出る
- リトライするほどレイテンシが伸び、最終的に全体が遅くなる
チューニング例:回数と間隔を“状況別”に変える
基本形の count=”3″ / interval=”1″ は、keep-alive の再利用失敗に対しては有効なことが多い一方、負荷スパイクやバックエンドの復帰待ちには短すぎる場合があります。状況別に考え方を整理すると、過剰リトライや無駄な遅延を避けられます。
| 状況 | 症状 | チューニングの方向性 |
|---|---|---|
| アイドル切断が主因 | 最初の 1 回だけ失敗し、その後は成功しやすい | first-fast-retry=”true”、interval は短め(0〜1 秒)で良い。回数は 2〜3 回で十分なことが多い。 |
| 短時間のスパイク(数秒) | 数秒間だけ接続が不安定 | count を増やし、interval を 2〜3 秒に。バックエンド回復の“間”を作る。 |
| 継続障害(数分以上) | 何度やっても失敗する | APIM のリトライで救えない。フェイルオーバー、キューイング、クライアントへ適切なエラー返却に切り替える。 |
例えば「短時間スパイク」寄りなら、次のように回数と間隔を少し増やす選択肢があります。
<backend>
<retry condition="@(context.LastError?.Reason == "BackendConnectionFailure")"
count="5"
interval="2"
first-fast-retry="true">
<forward-request buffer-request-body="true" />
</retry>
</backend>
ただし、リトライを増やすほど クライアントから見た待ち時間は伸びます。ユーザー体験やタイムアウト(クライアント側・中間プロキシ側)との整合も含めて、現実的な上限を決めることが大切です。
より確実にするための実装パターン(現場で使える小技)
バックエンドへ渡す相関IDを統一し、原因追跡を楽にする
リトライが入ると、バックエンド側ログでは「同じ処理が複数回来た」ように見えます。そこで、APIM で相関ID(Correlation ID)を付与し、バックエンドまで渡しておくと、1 つのクライアント要求が何回試行されたかを追いやすくなります。
<inbound>
<set-header name="x-correlation-id" exists-action="override">
<value>@(context.RequestId)</value>
</set-header>
</inbound>
このヘッダーをバックエンドでログ出力するだけでも、「リトライで救えているのか」「毎回同じ場所で落ちているのか」の判断が速くなります。
リトライ対象を“接続系だけ”に絞り、業務エラーの再送を避ける
「とにかく成功率を上げたい」気持ちで retry 条件を広げると、業務的に危険な再送が混ざります。例えば、バックエンドが 400 を返しているのに再送しても結果は変わらず、無駄に負荷だけ増えます。条件はまず BackendConnectionFailure のような通信の入口に限定し、必要性がはっきりしたときだけ段階的に広げるのが安全です。
どうしても回避したい場合の代替案:Connection: close
根本原因が「アイドル切断された接続の再利用」であることが明確で、かつスループットよりも安定性を優先する場合、バックエンドへの要求ヘッダーに Connection: close を付けて“毎回接続を閉じる”方向に寄せる手もあります(接続再利用を減らすため)。ただし、新規接続のオーバーヘッドでレイテンシ・CPU・TLS ハンドシェイクが増える可能性があるため、最終手段として扱い、性能試験の上で採用可否を決めるのが無難です。
運用で差がつく:再発防止のための観測ポイント
接続維持エラーは「たまにしか起きない」ことが多く、再現が難しいタイプのトラブルです。だからこそ、発生時に必要な情報が自動で集まる状態にしておくと、次の対応が劇的に楽になります。
| 観測対象 | 見る指標・ログ | 分かること |
|---|---|---|
| APIM 側 | トレース、失敗時の Reason、バックエンド接続関連の失敗回数 | APIM が何を理由に失敗したか(条件式が合っているか) |
| バックエンド側 | アクセスログ、アプリログ、スロットリングログ、再起動履歴 | 実際にリクエストが到達していたか/処理が完了していたか |
| ロードバランサー/プロキシ | アイドルタイムアウト、接続数、リセット(RST) | どこで接続が切られているかの当たり |
| クライアント側 | タイムアウト設定、再試行設定、同時接続数 | APIM がリトライする前にクライアントが諦めていないか |
まとめ
- 「The underlying connection was closed…」は、keep-alive で再利用した接続がサーバー側で閉じられていたときに起きやすい。
- APIM 側の基本対策は、BackendConnectionFailure を条件にした retry。
- 同一リクエスト(ボディ付き)を再送するなら buffer-request-body=”true” が必須。
- 3回リトライしても失敗するなら、Reason の値、retry の配置、バックエンドのアイドルタイムアウトや負荷を順に疑う。
- リトライは万能ではないため、冪等性(重複実行)と観測(トレース/相関ID)をセットで設計する。

コメント