
はじめに
こんにちは、株式会社 Techouse のクラウドハウス事業部でバックエンドエンジニアをしている hab_aki です。
先日開催された RubyKaigi 2026 に参加してきました。本記事では、3 日目に行われた Misaki Shioi(しおい)(@coe401_)さんのセッションについて紹介させていただきます。タイトルは The Less-Told Story of Socket Timeouts です。
普段、私たちが何気なく使っている Net::HTTP や Mechanize などの HTTP クライアントの裏側では、ソケットのタイムアウト機能が静かに動いています。本セッションでは、そのタイムアウト機能が Ruby 2.0 から 4.0 までの約 10 年をかけて、どのように進化し、現在に至ったのかという開発の歴史が語られました。
Ruby 2.0 — connect_timeout の誕生(2011年)
物語の出発点は 2011 年、1 つの小さな提案にまで遡ります。
ソケット接続時にタイムアウトを簡潔に指定したいという動機から、TCP ソケットを扱うクラスにタイムアウト機能を追加する提案が行われました。
当初は TCPSocket.new への実装が提案されました。しかし、Socket.tcp 側に持たせることで柔軟な設定を可能にしたいという議論を経て、最終的には Socket.tcp にタイムアウトが追加されました。
Socket.tcp(host, port, connect_timeout: 5)
この変更は 2013 年 2 月にリリースされた Ruby 2.0 に取り込まれ、ソケット接続のタイムアウト管理は飛躍的に書きやすくなりました。
ここで一度、サーバーに接続するまでの流れを整理しておきます。クライアントは次の 2 ステップを経て、サーバーに接続されます。
- 名前解決 — ドメイン名を IP アドレスに変換する
- 接続試行 — 名前解決で得た IP アドレスに対して TCP コネクションを確立する
2.0 で導入されたのは、このうち 接続試行のみ を対象とするタイムアウトでした。
Ruby 2.7〜3.3 — resolv_timeout の苦難
2019 年に 1 つの提案がされました(#15553)。
内容は、Addrinfo.getaddrinfo に timeout を、Socket.tcp に resolv_timeout を追加するというものです。Timeout ライブラリに頼らず、ソケット関連 API そのもので名前解決のタイムアウトを指定できるようにする狙いです。
Addrinfo.getaddrinfo("example.com", 80, timeout: 1) Socket.tcp("example.com", 80, resolv_timeout: 1)
API としてはシンプルなこの提案でした。しかし、ここにはいくつかの壁が存在しました。
Ruby 2.7 — getaddrinfo_a(3) での実装
最初の壁は、Ruby が内部で名前解決に使っている C 言語の関数 getaddrinfo(3) でした。
getaddrinfo(3) は DNS の応答待ちで 処理そのものをブロック する仕様です。一度この関数に入ると、Ruby のスレッドや Timeout ライブラリでは中断できません。
そこで採用されたのが、非同期版の getaddrinfo_a(3) でした。内部のバックグラウンドスレッドで getaddrinfo(3) を実行し、呼び出し側はその完了を待たずに済むというものです。これによりメインスレッド側でタイムアウトを判定できるようになり、Ruby 2.7 で resolv_timeout はリリースされました。
Ruby 3.0 — fork ハングバグの発覚と revert
ここまでは順調に見えた resolv_timeout ですが、2020 年 10 月、Ruby 3.0 のリリース直前に、思わぬバグが報告されます(#17220)。
getaddrinfo_a(3) を使った状態で fork を実行すると、子プロセスで処理がハングしてしまう のです。
議論の末、Ruby 3.0 リリースでは、getaddrinfo_a(3) に関する変更は採用されませんでした。
これにより、resolv_timeout 引数や Addrinfo.getaddrinfo の timeout 引数は API として残ったまま、実際には機能しない という状態になりました。「引数を受け付けるが何も起こらない」挙動が、その後数年続くことになります。
Ruby 3.3 — 名前解決の中断が可能に
長らく停滞していたこの問題に、ようやく転機が訪れたのは Ruby 3.3 でした(#19965)。
getaddrinfo_a(3) を用いずに getaddrinfo(3) と pthread を組み合わせる方法で、名前解決の処理を中断することが可能になりました。
ただし、「中断可能になった」だけ で、resolv_timeout のタイムアウト機能自体はまだこの時点では動かないままでした。
Ruby 3.4 — Happy Eyeballs Version 2 導入によるタイムアウト管理の複雑化
さらに、Ruby 3.4 では、 Happy Eyeballs Version 2 (HEv2, FastFallback) が追加されたことで、タイムアウト管理が新たな複雑さを抱えることになります。
Ruby 3.4 では Socket.tcp / TCPSocket.new に HEv2 が追加されました。実装は fast_fallback という名前で公開されています。これにより IPv6 / IPv4 の名前解決と接続試行が並行で進むようになり、デュアルスタック環境での接続が高速化されます。
この HEv2 の中身については、Shioi さんが昨年の RubyKaigi 2025 で発表されています。
HEv2 では、内部に 2 種類の待機タイマーを持ち、IPv4 と IPv6 の名前解決・接続試行を 並行 で進めます。
- Resolution Delay(デフォルト 50ms): IPv4 / IPv6 の名前解決を並行に開始したあと、片方が先に応答した場合に、もう片方の応答をこのミリ秒数だけ待つ。IPv6 を不当に切り捨てないための公平性タイマー。
- Connection Attempt Delay(デフォルト 250ms): 複数の宛先 IP アドレスへの接続試行を、このミリ秒数ずつ間隔を空けて起動する。瞬間的な接続集中を避け、ネットワークやクライアントへの負荷を抑えるための間隔タイマー。
さらに HEv2 では、名前解決と接続試行のフェーズが オーバーラップ します。たとえば IPv4 アドレスだけ先に解決された場合、IPv6 の名前解決を待ちながら、並行して IPv4 への接続試行を始めることがあります。「名前解決はここまで、接続はここから」というきれいな段階分けが成り立たないのです。
この状況下で、ユーザーが resolv_timeout と connect_timeout を組み合わせても、処理全体の制限時間は予測できません。各フェーズに上限を設けても、フェーズのオーバーラップや内部タイマーの加算により、実際の経過時間は単純な合計と一致しません。「全体で X 秒以内に決着させたい」と書く手段が、Ruby 3.4 の API には存在しないのです。
こうして、resolv_timeout と connect_timeout を組み合わせても 処理全体の制限時間を正確に制御できない という新たな課題が生まれます。
Ruby 4.0 — open_timeout の導入と完全な解決
そこで提案されたのが open_timeout でした(#21347)。
名前解決から接続確立までの 全体 を 1 つの値で制御できるキーワード引数です。
Socket.tcp("ruby-lang.org", 80, open_timeout: 1)
HEv2 の各タイマーと open_timeout の指定時刻を比較し、より早いタイミングでタイムアウトエラーを発生させる実装になりました。これにより「全体の上限時間を確実に切る」仕様がついに実現されました。
道中で見つかった 5 年もののバグ
open_timeout の実装を進める過程で、衝撃的な事実に気付きます。
実は Ruby 3.4 の時点で、HEv2 無効時 のタイムアウトが 動いていなかった のです。具体的には Addrinfo.getaddrinfo(timeout:) や Socket.tcp(resolv_timeout:) が該当します。原因は C 関数に引数として渡されていなかったためです。
Ruby 4.0 では引数の追加により、ようやくこの問題が解消されます。Socket.tcp / Addrinfo.getaddrinfo / TCPSocket.new のすべてで名前解決タイムアウトが Ruby 3.0 以来初めて 機能するようになりました。
こうして、Ruby 2.7 で議論が始まった理想は、長い時を経てようやく実現しました。
感想
セッションを聞く前は Socket の Timeout の実装は簡単に実装できそうだと感じていました。しかし実際は、C 言語のレイヤーやライブラリ設計、並行処理など複雑な要素が絡み合っており、難題であることに気づきました。
また、機能しない API の引数だけが残されてしまった Ruby 3.0 のエピソードや、長年気付かれなかったバグに気づいたエピソードは、ソフトウェア開発のリアルさを感じさせてくれました。
普段、私たちが Ruby を気軽に使えているのは、こうした地道な改善の積み重ねがあったからこそだということを、改めて認識する機会になりました。
Techouseでは、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。