- 設計
タイムアウト設計の値の決め方|接続・読み取りの違いと多段の計算例
タイムアウト設計とは、呼び出しの各区間で「どこまで待つか」を決め、層どうしの値がかみ合うようにそろえることです。 値は内側(DBや外部API)から積み上げ、外側の層ほど長くします。外側が内側より短いと、内側がまだ処理している途中で外側が先に切り、利用者には504が返ります。
この記事では、5層の値を積み上げる計算式と検算コード、わざと逆転させた再現、設計書のテンプレート、言語・製品別の設定コードを、実行結果と一緒に示します。リトライの間隔の決め方は扱わず、総時間の予算をどう配るかに絞ります。
確認環境(2026-10-06):Linux、Python 3.11.15+requests 2.34.2、OpenJDK 21、Node.js 22.22.2、nginx 1.24.0、PostgreSQL 16.15。各設定の定義は、requests・nginx・PostgreSQL 16・Java 21 API・MDN・AWS(Application Load Balancer)の公式ドキュメントを同日に開いて確認しました。
この記事の結論
- 外側の層ほど長くし、内側の合計を上回る値にします。
- 内側の合計は (接続+読み取り)× 試行回数 + 待機の合計。リトライを足すと、1回の上限の何倍にもなります。
- 読み取りタイムアウトは総時間の上限ではありません。 次のデータが届くまでの間隔です。総時間は全体期限(デッドライン)で別に決めます。
- リトライは全体期限の残り時間の中で行い、残りが無ければ次の試行をやめます。
- 値と根拠は層別の一覧表に書き、試行回数を変えるときは表を計算し直します。
タイムアウトの値を決める式と5層の早見表
外側の層のタイムアウト >= Σ 内側の呼び出し[(接続 + 読み取り) × 試行回数 + 待機の合計]
+ その層自身の処理時間 + 余裕
| 層 | 設定する値 | 記入例 | 内側から見た必要な下限 |
|---|---|---|---|
| 1 ブラウザ | 全体期限 | 12秒 | LB 10+余裕1=11秒 |
| 2 LB(nginx) | 読み取りタイムアウト | 10秒 | アプリ 8+余裕1=9秒 |
| 3 アプリ | リクエスト全体の期限 | 8秒 | DB 3.0+外部API 4.3+自分の処理 0.5=7.8秒 |
| 4 外部API呼び出し | 接続0.5秒、読み取り1.5秒、2回、待機0.3秒 | 合計4.3秒 | (最も内側) |
| 5 DB | 接続1秒、文の上限2秒 | 合計3.0秒 | (最も内側) |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「タイムアウト設計の値の決め方|接続・読み取りの違いと多段の計算例」
接続・読み取り・アイドル・全体期限の違い
| 種類 | 何を待つ時間か | 時計が動き出すとき | 公式ドキュメントでの説明 |
|---|---|---|---|
| 接続タイムアウト | 相手との接続が確立するまで | 接続を始めたとき | requests:リモートのマシンへの接続を確立するまで待つ秒数。nginx の proxy_connect_timeout:接続確立のタイムアウトで、通常75秒を超えられない |
| 読み取りタイムアウト | 次のデータが届くまでの間隔 | データを待ち始めるたび | requests:サーバーが送るバイトとバイトの間に待つ秒数。nginx の proxy_read_timeout:連続する2回の読み取り操作の間にだけ設定され、応答全体の転送時間ではない |
| アイドルタイムアウト | 通信のない接続を開いたままにする時間 | 最後の通信が終わったとき | ALB:接続のアイドルタイムアウトの既定は60秒(1〜4000秒)。PostgreSQL の idle_session_timeout、idle_in_transaction_session_timeout |
| 全体期限(デッドライン) | リクエスト開始から完了までの総時間 | リクエストを始めたとき | Java の HttpRequest.Builder.timeout:指定時間内に応答を受け取れなければ HttpTimeoutException。AbortSignal.timeout:指定ミリ秒後に自動で中断 |
| こういうとき | 使うもの |
|---|---|
| 相手が落ちている・届かないことを早く知りたい | 接続タイムアウトを短く(読み取りとは別に設定) |
| 処理が長い相手を、応答が来る限り待ちたい(大きなダウンロードなど) | 読み取りタイムアウト |
| 利用者を待たせる時間に上限を付けたい | 全体期限 |
| リトライ込みで総時間を守りたい | 全体期限+残り時間に合わせた1回ごとのタイムアウト |
| 使っていない接続を片付けたい | アイドルタイムアウト |
アイドルタイムアウトだけは向きが逆で、呼ばれる側(アプリ)を、呼ぶ側(LB)より長くします。AWSのドキュメントは、アプリケーションのアイドルタイムアウトをロードバランサーの値より大きくすることを勧めていて、そうでない場合にロードバランサーが閉じかけの接続へリクエストを送り、502を返すことがあると説明しています。
5層の値を積み上げる計算例(Pythonで検算)
内側から順に、次の手順で決めます。
- 最も内側(DB、外部API)の1回の上限を、応答時間の計測値から決める。
- 試行回数と待機を掛けて、呼び出し先ごとの合計を出す。
- アプリが直列に呼ぶものを足し、自分の処理時間を加えて、アプリの全体期限を決める。
- LBはアプリの全体期限+余裕、ブラウザはLB+余裕にする。
"""5層のタイムアウトを内側から積み上げて、外側が内側を上回っているかを確かめる。単位は秒。"""
def call_budget(connect, read, tries=1, waits=()):
"""1つの呼び出し先に使う最大時間 = (接続 + 読み取り) × 試行回数 + 待機の合計"""
assert len(waits) == max(tries - 1, 0), "待機は 試行回数 - 1 個"
return (connect + read) * tries + sum(waits)
def check(settings):
db = call_budget(settings["db_connect"], settings["db_statement"])
api = call_budget(settings["api_connect"], settings["api_read"],
settings["api_tries"], settings["api_waits"])
app_need = db + api + settings["app_own"] # アプリが最悪の場合に使う時間
rows = [
("5 DB", "接続 + 文の上限", db, None),
("4 外部API", "(接続 + 読み取り) × 試行 + 待機", api, None),
("3 アプリ", "全体期限", settings["app_deadline"], app_need),
("2 LB", "読み取りタイムアウト", settings["lb_read"], settings["app_deadline"] + settings["margin"]),
("1 ブラウザ", "全体期限", settings["browser"], settings["lb_read"] + settings["margin"]),
]
print(f"{'層':<10}{'設定':<26}{'値':>6}{'必要な下限':>10} 判定")
ok = True
for layer, name, value, need in rows:
if need is None:
print(f"{layer:<10}{name:<26}{value:>6.1f}{'-':>10} (内側の合計)")
else:
judge = "OK" if value >= need else "逆転"
ok &= value >= need
print(f"{layer:<10}{name:<26}{value:>6.1f}{need:>10.1f} {judge}")
print("=> すべての層で 外側 >= 内側の合計 + 余裕" if ok else "=> 逆転している層がある")
print()
base = dict(db_connect=1, db_statement=2,
api_connect=0.5, api_read=1.5, api_tries=2, api_waits=(0.3,),
app_own=0.5, app_deadline=8, margin=1, lb_read=10, browser=12)
print("[A] 積み上げた設定")
check(base)
print("[B] 外部APIの試行を3回・読み取り3秒に増やしただけの設定")
check({**base, "api_read": 3, "api_tries": 3, "api_waits": (0.5, 1.0)})
実行結果
[A] 積み上げた設定
層 設定 値 必要な下限 判定
5 DB 接続 + 文の上限 3.0 - (内側の合計)
4 外部API (接続 + 読み取り) × 試行 + 待機 4.3 - (内側の合計)
3 アプリ 全体期限 8.0 7.8 OK
2 LB 読み取りタイムアウト 10.0 9.0 OK
1 ブラウザ 全体期限 12.0 11.0 OK
=> すべての層で 外側 >= 内側の合計 + 余裕
[B] 外部APIの試行を3回・読み取り3秒に増やしただけの設定
層 設定 値 必要な下限 判定
5 DB 接続 + 文の上限 3.0 - (内側の合計)
4 外部API (接続 + 読み取り) × 試行 + 待機 12.0 - (内側の合計)
3 アプリ 全体期限 8.0 15.5 逆転
2 LB 読み取りタイムアウト 10.0 9.0 OK
1 ブラウザ 全体期限 12.0 11.0 OK
=> 逆転している層がある
[B]は、外部APIの読み取りを3秒・試行を3回に変えただけです。1回の上限は3.5秒でも、合計は (0.5+3)×3+1.5=12.0秒になり、アプリの全体期限8秒に収まりません。リトライの設定を変えたら、外側の層まで計算し直す必要があることが分かります。
予算から試行回数を逆算する
外側の期限が先に決まっている場合は、配れる予算から試行回数を出します。
試行回数 = floor( (予算 + 待機) ÷ (1回の上限 + 待機) )
例:予算 4.5秒、1回の上限 2.0秒(接続0.5+読み取り1.5)、待機 0.3秒
(4.5 + 0.3) ÷ (2.0 + 0.3) = 2.08… → 2回(合計 4.3秒)
3回にすると 2.0×3 + 0.3×2 = 6.6秒で予算を超える
わざと逆転させて504になる再現
次の3つをローカルで起動して確かめました。
| 役割 | 実体 | ポート | 設定 |
|---|---|---|---|
| LB | nginx 1.24.0 | 10061 | proxy_read_timeout 5秒 |
| アプリ | Python(下の app.py) | 10062 | 外部APIを 読み取り3秒 × 3回、待機 0.5秒・1.0秒 で呼ぶ |
| 外部API | Python(下の slow_api.py) | 10063 | 応答に10秒かかる |
アプリが最悪の場合に使う時間は 3×3+0.5+1.0=10.5秒で、LBの5秒より長い逆転した設定です。
nginx.conf(http ブロックの抜粋)
server {
listen 127.0.0.1:10061;
location / {
proxy_pass http://127.0.0.1:10062;
proxy_connect_timeout 1s;
proxy_send_timeout 5s;
proxy_read_timeout 5s;
}
}
slow_api.py
"""外部APIの代わりになる、わざと遅いサーバー。/?delay=秒 で応答を遅らせる。"""
import sys
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
delay = float(parse_qs(urlparse(self.path).query).get("delay", ["0"])[0])
time.sleep(delay)
body = b'{"stock": 3}'
try:
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
except (BrokenPipeError, ConnectionResetError):
pass # 呼び出し側が先に切断した
def log_message(self, *args):
pass
ThreadingHTTPServer(("127.0.0.1", int(sys.argv[1])), Handler).serve_forever()
app.py
"""アプリ層。外部APIを「1回の上限 × 試行回数 + 待機」で呼ぶ。
環境変数:
API_URL 呼び出し先
CONNECT_T 接続タイムアウト(秒)
READ_T 読み取りタイムアウト(秒)
TRIES 試行回数
WAITS 試行の間の待機(秒、カンマ区切り)
DEADLINE このリクエスト全体の期限(秒)。0 なら期限なし
"""
import json
import os
import sys
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import requests
API_URL = os.environ["API_URL"]
CONNECT_T = float(os.environ.get("CONNECT_T", "1"))
READ_T = float(os.environ.get("READ_T", "3"))
TRIES = int(os.environ.get("TRIES", "3"))
WAITS = [float(x) for x in os.environ.get("WAITS", "0.5,1.0").split(",")]
DEADLINE = float(os.environ.get("DEADLINE", "0"))
def call_api(start):
for i in range(TRIES):
read_t = READ_T
if DEADLINE:
remaining = DEADLINE - (time.monotonic() - start)
if remaining <= 0.05:
print(f" 期限切れのため {i + 1}回目は呼ばない", flush=True)
break
read_t = min(READ_T, remaining) # 残り時間を超えて待たない
try:
r = requests.get(API_URL, timeout=(CONNECT_T, read_t))
return r.json()
except requests.exceptions.Timeout as e:
print(f" {i + 1}回目: {type(e).__name__}({time.monotonic() - start:.1f}秒経過)", flush=True)
if i < TRIES - 1:
time.sleep(WAITS[i])
return None
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
start = time.monotonic()
print("受付 /reserve", flush=True)
data = call_api(start)
status, body = (200, data) if data else (503, {"error": "在庫APIが時間内に応答しませんでした"})
raw = json.dumps(body, ensure_ascii=False).encode()
try:
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
print(f"応答 {status}({time.monotonic() - start:.1f}秒)", flush=True)
except (BrokenPipeError, ConnectionResetError):
print(f"応答 {status} を返そうとしたが、相手は切断済み({time.monotonic() - start:.1f}秒)", flush=True)
def log_message(self, *args):
pass
ThreadingHTTPServer(("127.0.0.1", int(sys.argv[1])), Handler).serve_forever()
起動コマンド
python3 slow_api.py 10063 &
API_URL='http://127.0.0.1:10063/?delay=10' CONNECT_T=1 READ_T=3 TRIES=3 WAITS=0.5,1.0 DEADLINE=0 \
python3 -u app.py 10062 &
実行結果:利用者には5秒で504、アプリは10.5秒まで動き続ける
$ curl -s -o /dev/null -w "HTTP %{http_code} %{time_total}秒\n" http://127.0.0.1:10061/reserve
HTTP 504 5.001285秒
nginx のエラーログ
2026/10/06 01:34:13 [error] 6419#6419: *1 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 127.0.0.1, server: , request: "GET /reserve HTTP/1.1", upstream: "http://127.0.0.1:10062/reserve", host: "127.0.0.1:10061"
アプリのログ
受付 /reserve
1回目: ReadTimeout(3.0秒経過)
2回目: ReadTimeout(6.5秒経過)
3回目: ReadTimeout(10.5秒経過)
応答 503 を返そうとしたが、相手は切断済み(10.5秒)
利用者には5秒で504が返り、そのあともアプリは外部APIを2回呼び直し、10.5秒後に応答を返そうとして、相手がすでに切断していることに気づいています。逆転していると、だれも受け取らない処理に時間と接続を使い続けることになります。
整えた設定:全体期限4秒の中でリトライする
アプリに全体期限4秒を持たせ、1回の読み取りを1.5秒、試行を2回、待機を0.5秒にします。合計は 1.5×2+0.5=3.5秒で、LBの5秒に収まります。
API_URL='http://127.0.0.1:10063/?delay=10' CONNECT_T=1 READ_T=1.5 TRIES=2 WAITS=0.5 DEADLINE=4 \
python3 -u app.py 10062 &
$ curl -s -w "\nHTTP %{http_code} %{time_total}秒\n" http://127.0.0.1:10061/reserve
{"error": "在庫APIが時間内に応答しませんでした"}
HTTP 503 3.518017秒
受付 /reserve
1回目: ReadTimeout(1.5秒経過)
2回目: ReadTimeout(3.5秒経過)
応答 503(3.5秒)
外部APIが遅い状況は同じですが、3.5秒でアプリ自身が503と説明つきの本文を返しています。利用者が受け取るのはLBの504ではなくアプリが用意したエラー応答になり、画面側で案内を出し分けられます。
app.py の call_api では、1回ごとの読み取りタイムアウトを「設定値」と「全体期限の残り時間」の小さいほうにしています。これが総時間の予算配分の中心になる処理です。
層別タイムアウト設計書のテンプレート
空のテンプレートです。行は呼び出しの区間ごとに増やします。
| 層 | 区間 | 項目 | 設定名 | 既定値 | 設定値 | 設定箇所 | 根拠 |
|---|---|---|---|---|---|---|---|
| 1 | 利用者 → 入口 | ||||||
| 2 | 入口 → アプリ | ||||||
| 3 | アプリ | ||||||
| 4 | アプリ → 外部API | ||||||
| 5 | アプリ → DB |
記入例(備品予約システム)
| 層 | 区間 | 項目 | 設定名 | 既定値 | 設定値 | 設定箇所 | 根拠 |
|---|---|---|---|---|---|---|---|
| 1 | ブラウザ → LB | 全体期限 | fetch の AbortSignal.timeout | なし(指定しなければ待ち続ける) | 12秒 | フロントのAPI呼び出し共通関数 | LBの10秒+余裕1秒を上回る値。画面の待ち時間の上限として合意した値 |
| 2 | LB → アプリ | 接続タイムアウト | proxy_connect_timeout | 60秒 | 1秒 | nginx.conf の location | 同じネットワーク内の接続は通常すぐ確立するため短くする |
| 2 | LB → アプリ | 読み取りタイムアウト | proxy_read_timeout | 60秒 | 10秒 | nginx.conf の location | アプリの全体期限8秒+余裕1秒=9秒を上回る値 |
| 3 | アプリ | リクエスト全体の期限 | アプリ内のデッドライン | なし(自分で実装) | 8秒 | アプリの共通処理 | DB 3.0+外部API 4.3+自分の処理 0.5=7.8秒を上回る値 |
| 4 | アプリ → 外部API | 接続タイムアウト | requests の timeout の1つ目 | なし | 0.5秒 | APIクライアントの共通設定 | 接続先は同じリージョン内という前提の値 |
| 4 | アプリ → 外部API | 読み取りタイムアウト | requests の timeout の2つ目 | なし | 1.5秒 | APIクライアントの共通設定 | 相手の応答時間の計測値(99パーセンタイル)に余裕を足した値 |
| 4 | アプリ → 外部API | 試行回数と待機 | アプリ内のリトライ設定 | — | 2回、待機0.3秒 | APIクライアントの共通設定 | (0.5+1.5)×2+0.3=4.3秒で予算内 |
| 5 | アプリ → DB | 接続タイムアウト | 接続文字列の connect_timeout | — | 1秒 | DB接続設定 | 同じネットワーク内の接続のため短くする |
| 5 | アプリ → DB | 文の実行時間の上限 | statement_timeout | 0(無効) | 2秒 | ALTER ROLE(アプリ用ロール) | 画面系クエリの計測値に余裕を足した値。バッチ用ロールは別の値 |
| 5 | アプリ → DB | ロック待ちの上限 | lock_timeout | 0(無効) | 1秒 | ALTER ROLE(アプリ用ロール) | statement_timeout より短くして原因を区別できるようにする |
| 5 | アプリ → DB | トランザクション中の無通信 | idle_in_transaction_session_timeout | 0(無効) | 30秒 | ALTER ROLE(アプリ用ロール) | 開いたままのトランザクションを残さないため |
「既定値」の列は、今回開いた公式ドキュメントで確認できたものを記入しています。「設定値」は説明用の例です。
表計算ソフトに貼る場合のタブ区切りです。
層 区間 項目 設定名 既定値 設定値 設定箇所 根拠
1 ブラウザ → LB 全体期限 fetch の AbortSignal.timeout なし(指定しなければ待ち続ける) 12秒 フロントのAPI呼び出し共通関数 LBの10秒+余裕1秒を上回る値。画面の待ち時間の上限として合意した値
2 LB → アプリ 接続タイムアウト proxy_connect_timeout 60秒 1秒 nginx.conf の location 同じネットワーク内の接続は通常すぐ確立するため短くする
2 LB → アプリ 読み取りタイムアウト proxy_read_timeout 60秒 10秒 nginx.conf の location アプリの全体期限8秒+余裕1秒=9秒を上回る値
3 アプリ リクエスト全体の期限 アプリ内のデッドライン なし(自分で実装) 8秒 アプリの共通処理 DB 3.0+外部API 4.3+自分の処理 0.5=7.8秒を上回る値
4 アプリ → 外部API 接続タイムアウト requests の timeout の1つ目 なし 0.5秒 APIクライアントの共通設定 接続先は同じリージョン内という前提の値
4 アプリ → 外部API 読み取りタイムアウト requests の timeout の2つ目 なし 1.5秒 APIクライアントの共通設定 相手の応答時間の計測値(99パーセンタイル)に余裕を足した値
4 アプリ → 外部API 試行回数と待機 アプリ内のリトライ設定 — 2回、待機0.3秒 APIクライアントの共通設定 (0.5+1.5)×2+0.3=4.3秒で予算内
5 アプリ → DB 接続タイムアウト 接続文字列の connect_timeout — 1秒 DB接続設定 同じネットワーク内の接続のため短くする
5 アプリ → DB 文の実行時間の上限 statement_timeout 0(無効) 2秒 ALTER ROLE(アプリ用ロール) 画面系クエリの計測値に余裕を足した値。バッチ用ロールは別の値
5 アプリ → DB ロック待ちの上限 lock_timeout 0(無効) 1秒 ALTER ROLE(アプリ用ロール) statement_timeout より短くして原因を区別できるようにする
5 アプリ → DB トランザクション中の無通信 idle_in_transaction_session_timeout 0(無効) 30秒 ALTER ROLE(アプリ用ロール) 開いたままのトランザクションを残さないため
言語・製品別の設定コード
Python(requests):接続と読み取りを分けて指定する
requests は、timeout を指定しなければタイムアウトしません(公式ドキュメント)。タプルで(接続, 読み取り)を分けて渡します。
import time
import requests
def try_get(label, url, timeout):
start = time.monotonic()
try:
r = requests.get(url, timeout=timeout)
result = f"HTTP {r.status_code}"
except requests.exceptions.ConnectTimeout:
result = "ConnectTimeout(接続が確立できなかった)"
except requests.exceptions.ReadTimeout:
result = "ReadTimeout(接続後、応答を待つ間に時間切れ)"
print(f"{label}: {result} / {time.monotonic() - start:.1f}秒")
SLOW = "http://127.0.0.1:10063/?delay=3"
try_get("1. 応答に3秒かかる相手に (接続1, 読み取り5)", SLOW, (1, 5))
try_get("2. 応答に3秒かかる相手に (接続1, 読み取り2)", SLOW, (1, 2))
try_get("3. 応答しないアドレスに (接続1, 読み取り5) ", "http://10.255.255.1:81/", (1, 5))
try_get("4. 数値1つ timeout=2 は両方に2秒 ", SLOW, 2)
1. 応答に3秒かかる相手に (接続1, 読み取り5): HTTP 200 / 3.0秒
2. 応答に3秒かかる相手に (接続1, 読み取り2): ReadTimeout(接続後、応答を待つ間に時間切れ) / 2.0秒
3. 応答しないアドレスに (接続1, 読み取り5) : ConnectTimeout(接続が確立できなかった) / 1.0秒
4. 数値1つ timeout=2 は両方に2秒 : ReadTimeout(接続後、応答を待つ間に時間切れ) / 2.0秒
読み取りタイムアウトが「間隔」であることは、1秒おきに1バイトずつ返すサーバーで確かめられます。
"""読み取りタイムアウトは「全体の時間」ではなく「次のデータが届くまでの間隔」。"""
import socket
import threading
import time
import requests
def drip_server(port):
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", port)); s.listen(1)
conn, _ = s.accept(); conn.recv(4096)
conn.sendall(b"HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\n")
for ch in b"abcdef": # 1バイトずつ、1秒おきに送る
time.sleep(1); conn.sendall(bytes([ch]))
conn.close(); s.close()
threading.Thread(target=drip_server, args=(10064,), daemon=True).start()
time.sleep(0.3)
start = time.monotonic()
r = requests.get("http://127.0.0.1:10064/", timeout=(1, 2))
print(f"読み取りタイムアウト2秒で、全体 {time.monotonic() - start:.1f}秒かかる応答を受信できた: {r.text}")
読み取りタイムアウト2秒で、全体 6.0秒かかる応答を受信できた: abcdef
Java(java.net.http.HttpClient):接続は Client、全体は Request に付ける
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.time.Duration;
public class TimeoutDemo {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(1)) // 接続タイムアウト
.build();
// 応答しないアドレスへの接続
long t0 = System.nanoTime();
try {
client.send(HttpRequest.newBuilder(URI.create("http://10.255.255.1:81/")).build(),
HttpResponse.BodyHandlers.ofString());
} catch (HttpTimeoutException e) {
System.out.printf("接続: %s: %s / %.1f秒%n", e.getClass().getName(), e.getMessage(),
(System.nanoTime() - t0) / 1e9);
}
for (int timeoutSec : new int[] {5, 2}) {
HttpRequest request = HttpRequest.newBuilder(URI.create("http://127.0.0.1:10063/?delay=3"))
.timeout(Duration.ofSeconds(timeoutSec)) // リクエスト全体の上限
.GET()
.build();
long start = System.nanoTime();
try {
HttpResponse<String> res = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.printf("timeout=%d秒: HTTP %d %s", timeoutSec, res.statusCode(), res.body());
} catch (HttpTimeoutException e) {
System.out.printf("timeout=%d秒: %s: %s", timeoutSec, e.getClass().getName(), e.getMessage());
}
System.out.printf(" / %.1f秒%n", (System.nanoTime() - start) / 1e9);
}
}
}
接続: java.net.http.HttpConnectTimeoutException: HTTP connect timed out / 1.0秒
timeout=5秒: HTTP 200 {"stock": 3} / 3.0秒
timeout=2秒: java.net.http.HttpTimeoutException: request timed out / 2.0秒
接続の時間切れは HttpConnectTimeoutException、リクエストの時間切れは HttpTimeoutException と、例外で区別できます。Java 21 のAPIドキュメントによると、HttpRequest.Builder の timeout を設定しない場合は無期限に待ちます。
JavaScript(fetch):AbortSignal.timeout で全体期限を付ける
const url = "http://127.0.0.1:10063/?delay=3";
for (const ms of [5000, 2000]) {
const start = performance.now();
try {
const res = await fetch(url, { signal: AbortSignal.timeout(ms) }); // 全体の期限
console.log(`timeout=${ms}ms: HTTP ${res.status} ${await res.text()}`);
} catch (e) {
console.log(`timeout=${ms}ms: ${e.name}: ${e.message}`);
}
console.log(` ${((performance.now() - start) / 1000).toFixed(1)}秒`);
}
timeout=5000ms: HTTP 200 {"stock": 3}
3.1秒
timeout=2000ms: TimeoutError: The operation was aborted due to timeout
2.0秒
MDNによると、時間切れのときは TimeoutError、利用者の操作などほかの理由による中断は AbortError になります。キャンセルボタンと組み合わせるときは AbortSignal.any に両方のシグナルを渡します。
nginx:接続・送信・読み取りの3つ
location / {
proxy_pass http://127.0.0.1:10062;
proxy_connect_timeout 1s; # 既定 60s
proxy_send_timeout 10s; # 既定 60s。連続する2回の書き込みの間隔
proxy_read_timeout 10s; # 既定 60s。連続する2回の読み取りの間隔
}
上流サーバーを複数並べている場合、proxy_next_upstream の既定は error timeout で、タイムアウト時に次のサーバーへリクエストを渡します。proxy_next_upstream_tries と proxy_next_upstream_timeout の既定は0(制限なし)なので、LB自身の再試行も合計時間に含めて考えます。
PostgreSQL:ロールに付けて、必要な処理だけ上書きする
SET statement_timeout = '2s';
SELECT pg_sleep(3);
ERROR: canceling statement due to statement timeout
Time: 2000.358 ms (00:02.000)
PostgreSQL 16 のドキュメントは、statement_timeout を postgresql.conf に設定すると全セッションに影響するため勧めていません。アプリ用のロールに設定し、長い処理はトランザクションの中だけ上書きします。
ALTER ROLE app_user SET statement_timeout = '3s';
ALTER ROLE app_user SET lock_timeout = '1s';
ALTER ROLE app_user SET idle_in_transaction_session_timeout = '30s';
SELECT rolname, rolconfig FROM pg_roles WHERE rolname = 'app_user';
BEGIN;
SET LOCAL statement_timeout = '10s'; -- このトランザクションだけ
SHOW statement_timeout;
COMMIT;
rolname | rolconfig
----------+--------------------------------------------------------------------------------
app_user | {statement_timeout=3s,lock_timeout=1s,idle_in_transaction_session_timeout=30s}
(1 row)
statement_timeout
-------------------
10s
(1 row)
ロール名は app_user に置き換えて表記しています。接続のタイムアウトは接続文字列の connect_timeout で指定します。応答しないアドレスを指定して確かめた結果です。
psql 'host=10.255.255.1 dbname=appdb connect_timeout=2' -c 'select 1'
psql: error: connection to server at "10.255.255.1", port 5432 failed: timeout expired
| 設定 | 対象 | 既定値 |
|---|---|---|
| statement_timeout | 1つの文の実行時間 | 0(無効) |
| lock_timeout | ロックの取得を待つ時間(取得の試みごと) | 0(無効) |
| idle_in_transaction_session_timeout | トランザクションを開いたまま通信のない時間 | 0(無効) |
| idle_session_timeout | トランザクションの外で通信のない時間 | 0(無効) |
単位を付けずに数値だけを書くと、ミリ秒として扱われます。
リトライとタイムアウトの関係:総時間の予算配分
| 決めること | 考え方 | 記入例 |
|---|---|---|
| 予算 | 外側の期限から、ほかの呼び出しと自分の処理を引いた残り | 8 − 3.0(DB)− 0.5(自分の処理)=4.5秒 |
| 1回の上限 | 接続+読み取り。相手の応答時間の計測値に余裕を足す | 0.5+1.5=2.0秒 |
| 試行回数 | floor((予算+待機) ÷ (1回の上限+待機)) | 2回 |
| 最後の試行 | 残り時間が1回の上限より短ければ、残り時間に縮める。ほとんど残っていなければ呼ばない | 残り0.05秒以下なら打ち切り |
| 期限の伝え方 | 内側の呼び出しへ、残り時間を引数やヘッダーで渡す | 残り秒数を関数の引数で渡す |
待機時間の伸ばし方(指数的に伸ばす、ばらつきを入れる)は間隔の設計の話題で、ここでは「待機の合計が予算に入っているか」だけを確かめます。
確かめて分かったこと
- 読み取りタイムアウト2秒でも、全体6秒かかる応答を受信できました。 1秒おきにデータが届く限り、読み取りタイムアウトは働きません。総時間の上限は別に必要です。
- 逆転した設定では、504を返したあともアプリが動き続けました。 5秒で504が返ったあと、アプリは10.5秒まで外部APIを呼び直していました。
- nginx のエラーログには「upstream timed out … while reading response header from upstream」と出ます。 504の原因がLBの読み取りタイムアウトかどうかは、この行で見分けられます。
- 1回の上限3.5秒の設定が、合計では12.0秒になりました。 試行3回と待機を足した結果で、1回の値だけを見ていると気づきにくい点です。
- ALTER ROLE で付けた値は、そのロールで新しく接続したセッションで効きます。 そのロールで接続し直すと3秒で打ち切られ、同じセッションで SET ROLE しただけでは statement_timeout は0のままでした。
- Java は接続と全体で例外の型が分かれます。 HttpConnectTimeoutException と HttpTimeoutException で、どちらの時間切れかをログに残せます。
つまずきやすい点と整え方
| つまずきやすい点 | こうするとよい |
|---|---|
| クライアントのタイムアウトを指定していない | 接続と読み取りを必ず指定する。requests も Java の HttpRequest も、指定しなければ待ち続ける |
| 全層に同じ値(30秒など)を入れている | 内側から積み上げ、外側ほど長くする |
| リトライ回数だけを増やした | 合計を計算し直し、外側の層の値も見直す |
| 読み取りタイムアウトを総時間の上限だと考えている | 全体期限を別に設ける |
| DBのタイムアウトを全体設定に入れた | アプリ用・バッチ用などロールごとに設定する |
| 504と503の区別がつかない | LBより先にアプリが時間切れを判断し、アプリのエラー応答を返す |
よくある質問
Q. 接続タイムアウトと読み取りタイムアウトの違いは何ですか? A. 接続タイムアウトは接続が確立するまでの時間、読み取りタイムアウトは接続後に次のデータが届くまでの間隔です。接続は短く、読み取りは相手の処理時間に合わせて決めます。
Q. タイムアウトの値は何秒にすればよいですか? A. 決まった正解の秒数は無く、内側の応答時間の計測値から積み上げて決めます。1回の上限を決め、試行回数と待機を掛け、外側の層はその合計に余裕を足した値にします。
Q. 504 Gateway Timeout が出るときは、どこを見ればよいですか? A. まずLBのエラーログで、上流の読み取りが時間切れになっていないかを確かめます。次に、アプリが最悪の場合に使う時間(リトライ込み)がLBの値より長くないかを計算します。
Q. リトライとタイムアウトはどちらを先に決めますか? A. 外側の期限と予算を先に決め、その中に収まる1回の上限と試行回数を決めます。試行回数を先に決めると、合計が外側の期限を超えやすくなります。
Q. 接続タイムアウトは何秒が目安ですか? A. 同じネットワーク内の相手なら1秒前後の短い値がよく使われます(例)。requests の公式ドキュメントは、TCPの再送の間隔が3秒であることから、3の倍数より少し大きい値にすることを勧めています。
まとめ
- 外側の層ほど長く。内側の合計に余裕を足した値を、外側の下限にする。
- 内側の合計=(接続+読み取り)× 試行回数+待機の合計。
- 読み取りタイムアウトは間隔。総時間は全体期限で決める。
- リトライは全体期限の残り時間の中で行い、1回ごとのタイムアウトを残り時間に合わせて縮める。
- 逆転すると504が返ったあとも内側が動き続ける。アプリが先に時間切れを判断して、自分のエラー応答を返す。
- アイドルタイムアウトは向きが逆。呼ばれる側を呼ぶ側より長くする。
- 値と根拠は層別の設計書に残し、設定を変えたら計算し直す。
参考・出典
すべて 2026-10-06 に確認しました。
- Requests「Advanced Usage — Timeouts」 https://requests.readthedocs.io/en/latest/user/advanced/
- nginx「Module ngx_http_proxy_module」 https://nginx.org/en/docs/http/ngx_http_proxy_module.html
- PostgreSQL 16「Client Connection Defaults」 https://www.postgresql.org/docs/16/runtime-config-client.html
- Java SE 21 API「HttpRequest.Builder」 https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/http/HttpRequest.Builder.html
- MDN「AbortSignal: timeout() static method」 https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static
- AWS「Edit attributes for your Application Load Balancer」 https://docs.aws.amazon.com/elasticloadbalancing/latest/application/edit-load-balancer-attributes.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「タイムアウト設計の値の決め方|接続・読み取りの違いと多段の計算例」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。