Go の http.Server でタイムアウトを設定する:ReadTimeout・WriteTimeout・IdleTimeout

http.ListenAndServe は手軽ですが、タイムアウトの設定ができません。本番環境では http.Server 構造体を使い、ReadTimeout・WriteTimeout・IdleTimeout を明示的に設定するのが鉄則です。

なぜタイムアウトが必要なのか

タイムアウトを設定しないサーバーは、スローロリス攻撃のような低速リクエストに対して脆弱になります。攻撃者がコネクションを開いたまま少しずつデータを送り続けると、サーバーのリソースが枯渇し、正常なリクエストを受け付けられなくなるのです。

Go の http.ListenAndServe はデフォルトですべてのタイムアウトがゼロ(無制限)に設定されています。開発中はそれで問題ありませんが、本番にそのまま出すのは危険です。

http.Server で 3 つのタイムアウトを設定する

http.Server 構造体には 3 種類のタイムアウトフィールドがあります。

package main

import (
	"fmt"
	"net/http"
	"time"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello!")
	})

	server := &http.Server{
		Addr:         ":8080",
		Handler:      mux,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  120 * time.Second,
	}

	fmt.Println("サーバー起動: http://localhost:8080")
	if err := server.ListenAndServe(); err != nil {
		fmt.Println("サーバーエラー:", err)
	}
}

http.ListenAndServe(":8080", mux) の代わりに server.ListenAndServe() を呼びます。サーバーの設定を構造体にまとめることで、タイムアウト以外にも TLS 設定やエラーログの出力先などを細かく制御できるようになります。

各タイムアウトの役割

3 つのタイムアウトはそれぞれ異なるフェーズを担当しています。

ReadTimeoutリクエストヘッダーとボディの読み取り完了までの時間
WriteTimeoutレスポンスの書き込み完了までの時間
IdleTimeoutKeep-Alive 接続で次のリクエストが来るまでの待機時間

ReadTimeout はクライアントがリクエストを送り終えるまでの制限時間です。大きなファイルをアップロードする API では、この値を長めに設定する必要があります。短すぎると、正常なリクエストまでタイムアウトしてしまいます。

WriteTimeout はサーバーがレスポンスを返し終えるまでの制限時間になります。重い処理を行うエンドポイントでは余裕を持たせましょう。ただし、長すぎるとスロークライアントがコネクションを占有し続ける原因になります。

IdleTimeout は Keep-Alive 接続に関する設定です。HTTP/1.1 ではデフォルトで接続が維持されるため、この値が短すぎると頻繁に再接続が発生し、長すぎるとアイドル接続がリソースを消費し続けます。

クライアントが接続

ReadTimeout 内にリクエスト送信完了

WriteTimeout 内にレスポンス返却完了

IdleTimeout 内に次のリクエストが来なければ切断

推奨値の目安

タイムアウトの適切な値はアプリケーションの特性によって変わりますが、一般的な Web API であれば以下が出発点になります。

タイムアウト推奨値備考
ReadTimeout5〜10 秒ファイルアップロードがあれば長めに
WriteTimeout10〜30 秒重い処理があれば長めに
IdleTimeout60〜120 秒リバースプロキシ側の設定と合わせる

リバースプロキシ(nginx など)を前段に置いている場合は、プロキシ側のタイムアウトとの整合性も考慮が必要です。Go 側の WriteTimeout がプロキシのタイムアウトより短いと、Go がレスポンスを返す前にプロキシが接続を切ってしまうことがあります。

ReadHeaderTimeout もある

Go 1.8 から ReadHeaderTimeout というフィールドも追加されています。これはリクエストヘッダーの読み取りだけに特化したタイムアウトです。

server := &http.Server{
	Addr:              ":8080",
	Handler:           mux,
	ReadHeaderTimeout: 3 * time.Second,
	ReadTimeout:       5 * time.Second,
	WriteTimeout:      10 * time.Second,
	IdleTimeout:       120 * time.Second,
}

スローロリス攻撃への防御には ReadHeaderTimeout がもっとも効果的です。ヘッダーの読み取りはボディに比べて短時間で完了するはずなので、3 秒程度で十分でしょう。

http.ListenAndServe

手軽だがタイムアウトがすべて無制限。開発やプロトタイプ向き。

http.Server

タイムアウトや TLS を細かく制御可能。本番運用では必須。

本番サーバーでは http.Server を使ってタイムアウトを設定する。これは Go の Web 開発における基本的なプラクティスとして覚えておいてください。