Node.jsでデータベースや外部サービス接続時の「Error: connect ETIMEDOUT」を修正する

intermediate💚 Node.js2026-04-02| Node.js (v14+)、Linux/macOS/Windows、MySQL・PostgreSQL・MongoDB・Redis・HTTPクライアント(axios、node-fetch)に対応

Error Message

Error: connect ETIMEDOUT <IP>:<PORT>
#nodejs#ネットワーク#タイムアウト#データベース#http

TL;DR

Node.jsプロセスがTCP接続リクエストを送信したのに、何も返ってきませんでした。レスポンスなし、タイムアウトが発動するまでただ沈黙するだけ。それがETIMEDOUTです。よくある原因は、ファイアウォールによるパケットの遮断、誤ったIPアドレス、データベースが実際には起動していない、またはサーバーが過負荷で応答できない状態です。まずtelnetまたはncを実行してネットワークの問題であることを確認してから、具体的な原因を特定してください。

このエラーの意味

完全なスタックトレースは次のようになります:

Error: connect ETIMEDOUT 203.0.113.42:5432
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1144:16) {
  errno: -110,
  code: 'ETIMEDOUT',
  syscall: 'connect',
  address: '203.0.113.42',
  port: 5432
}

Node.jsがポート5432にTCP SYNパケットを送信しましたが、SYN-ACKが返ってきませんでした。OSのタイムアウト時間(通常20〜75秒)が経過した後、接続を諦めました。

これはリモートホストが接続を能動的に拒否するECONNREFUSEDとは異なります。ETIMEDOUTの場合、パケットはただ消えてしまいます——ファイアウォール、デッドルート、または過負荷で応答できないサービスに吸い込まれるのです。デバッグ時にこの違いは重要です:ECONNREFUSEDはホストに到達できることを意味し、ETIMEDOUTは途中の何かがブロックしていることを意味します。

根本原因

  • ファイアウォールによるポートのブロック — AWS、GCP、および堅牢化されたLinuxサーバーで最も多い原因です。セキュリティグループ、UFW、iptablesはデフォルトでパケットをサイレントに破棄します。
  • 誤ったIPアドレスまたはホスト名 — サービスは稼働しているのに、間違ったアドレスを指定しています。.envのタイポで1時間無駄にすることもあります。
  • サービスが該当ポートでリッスンしていない — データベースがクラッシュしているか、0.0.0.0ではなく127.0.0.1にバインドされています。
  • ネットワークルーティングの問題 — VPNが切断された、VPCピアリングが設定されていない、またはホストへのルートが存在しない。
  • コネクションプールの枯渇 — 10本の接続がすべてビジー状態で、新しいリクエストがキューに積まれ、スロットを待ちながらタイムアウトします。

ステップ1 — コードを触る前に接続をテストする

タイムアウトの調整や接続ロジックの書き直しをする前に、まずネットワークの問題なのかアプリケーションの問題なのかを確認してください:

# TCP接続を直接テスト
telnet 203.0.113.42 5432

# telnetがない場合はncを使用
nc -zv 203.0.113.42 5432 -w 5

# DNSが正しいIPに解決されるか確認
nslookup your-db-host.example.com
dig your-db-host.example.com

確認すべき3つの結果:

  • 無限にハングする → ファイアウォールがパケットを破棄しています。ステップ2へ。
  • 接続が拒否される → ホストには到達できるがリッスンしているサービスがありません。ステップ3へ。
  • すぐに接続される → ネットワークは正常です。バグはNode.jsの設定にあります。ステップ4へ。

ステップ2 — ファイアウォールを開放する

クラウドのファイアウォールはパケットをサイレントに破棄します——それが外部から見たETIMEDOUTの正体です。

AWS EC2 / RDS

# セキュリティグループのインバウンドルールを確認
aws ec2 describe-security-groups --group-ids sg-xxxxxxxx

# RDSの場合: コンソール → RDS → 対象DB → 接続とセキュリティ → VPCセキュリティグループ

インバウンドルールで、アプリサーバーのIPまたはセキュリティグループからターゲットポートへのTCPを許可する必要があります。よくあるミス:開発環境では0.0.0.0/0を許可しているのに、本番環境でアプリサーバーのセキュリティグループIDを追加し忘れることです。

Linuxサーバー (UFW)

# 現在のルールを確認
sudo ufw status verbose

# ポートをグローバルに開放
sudo ufw allow 5432/tcp

# または特定のIPに限定(本番環境ではより安全)
sudo ufw allow from 10.0.1.50 to any port 5432

iptables

sudo iptables -L INPUT -n -v | grep 5432

ステップ3 — サービスが実際にリッスンしているか確認する

データベースサーバーにSSHで接続し、ポートにバインドされているものを確認します:

# どのプロセスがどのインターフェースでリッスンしているかを確認
ss -tlnp | grep 5432
# または
netstat -tlnp | grep 5432

# サービスの状態を確認
systemctl status postgresql
systemctl status mysql
systemctl status mongod

バインドされているアドレスがすべてを示しています。127.0.0.1:5432はサービスがローカル接続のみを受け付けることを意味します。0.0.0.0:5432はすべてのインターフェースに開放されていることを意味します(ファイアウォールルールは引き続き適用されます)。

127.0.0.1が表示されている場合は、サービスの設定を修正してください。PostgreSQLの場合は/etc/postgresql/*/main/postgresql.confを開き、以下の行を変更します:

listen_addresses = 'localhost'   # この行を
listen_addresses = '*'           # こちらに変更し、postgresqlを再起動

ステップ4 — Node.jsのタイムアウト設定を調整する

ネットワークは正常なのに負荷時にタイムアウトが発生する場合は、コネクションプールの枯渇が原因の可能性が高いです。新しいリクエストが空きスロットを待ってキューに積まれ、長時間待った後にETIMEDOUTで終了します。

pg(PostgreSQL)の場合

const { Pool } = require('pg');

const pool = new Pool({
  host: 'your-db-host',
  port: 5432,
  database: 'mydb',
  user: 'user',
  password: 'password',
  connectionTimeoutMillis: 10000,  // 空き接続を最大10秒待つ
  max: 20,                          // DBが対応できるならデフォルトの10から増やす
  idleTimeoutMillis: 30000,
});

mysql2の場合

const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: 'your-db-host',
  port: 3306,
  user: 'user',
  password: 'password',
  database: 'mydb',
  connectTimeout: 10000,
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});

axios(HTTPリクエスト)の場合

const axios = require('axios');

const client = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,  // 10秒
});

try {
  const response = await client.get('/endpoint');
} catch (err) {
  if (err.code === 'ETIMEDOUT' || err.code === 'ECONNABORTED') {
    console.error('リクエストがタイムアウトしました — ネットワークを確認するかタイムアウト値を増やしてください');
  }
  throw err;
}

ステップ5 — 一時的な障害に対してリトライロジックを追加する

すべてのETIMEDOUTが何かの故障を意味するわけではありません。短時間のネットワーク障害、ローリング再起動、コールドスタートの遅延などが、自然に解消される一時的なタイムアウトを引き起こすことがあります。指数バックオフを使ったシンプルなリトライでアラートなしにこれらを処理できます:

async function connectWithRetry(connectFn, retries = 3, delay = 2000) {
  for (let i = 0; i  setTimeout(res, delay * (i + 1)));
      } else {
        throw err;
      }
    }
  }
}

// 使用例
await connectWithRetry(() => pool.query('SELECT 1'));

2秒、4秒、6秒の間隔で最大3回リトライします。サービスの通常の回復時間に応じて調整してください。

修正の確認

変更を加えたら、再デプロイ前に実際に効果があったかを確認してください:

# アプリサーバーからの簡易TCPテスト
nc -zv your-db-host 5432 -w 5
# 期待される結果: Connection to your-db-host 5432 port [tcp/postgresql] succeeded!

# またはNode.jsで直接テスト
node -e "
const net = require('net');
const socket = net.createConnection({ host: 'your-db-host', port: 5432, timeout: 5000 });
socket.on('connect', () => { console.log('接続成功!'); socket.destroy(); });
socket.on('timeout', () => { console.log('タイムアウト'); socket.destroy(); });
socket.on('error', (err) => console.log('エラー:', err.message));
"

補足情報

ファイアウォールルールをデバッグする際、アプリサーバーのIPが許可されたCIDRブロック内に含まれるかどうかを確認する必要があることがよくあります。ToolCraftのサブネット計算ツールを使えばすぐに確認できます——CIDRとIPを貼り付けるだけで、一致するかどうかを即座に教えてくれます。完全にブラウザ上で動作し、データは外部に送信されません。10.0.0.0/16のようなAWSセキュリティグループルールを見て10.0.1.50が範囲内かどうか迷ったときに便利です。

クイックリファレンス

  • ETIMEDOUT — パケットは送信されたが応答なし(ファイアウォールまたはデッドルート)
  • ECONNREFUSED — ホストには到達できたが、ポートが接続を拒否した(サービスが停止中)
  • ENOTFOUND — DNS解決に失敗した(ホスト名が間違っている)
  • ECONNRESET — 接続が確立されたが、リモートによって突然切断された

Related Error Notes