---
title: "API GatewayのWebSocket API：$connectとDynamoDBで接続管理を実装する手順と上限値"
url: "https://www.issoh.co.jp/tech/details/18489/"
published: 2026-10-18
updated: 2026-10-18
categories: ["AWS"]
publisher: "株式会社一創"
---

# API GatewayのWebSocket API：$connectとDynamoDBで接続管理を実装する手順と上限値

Amazon API GatewayのWebSocket APIは、接続の維持をAWSに任せ、メッセージごとにLambdaを呼び出す仕組みです。サーバーを持たずにチャットや通知を作れる反面、接続IDの保存先は自分で用意しなければなりません。この記事では、$connectと$disconnectのルートでDynamoDBに接続IDを出し入れする実装を、CLIとPythonのコードで組み立てます。接続2時間・アイドル10分・フレーム32KBの上限、切断イベントの取りこぼしへの備え、wscatでの確認手順、採用してよい条件までが、この記事で解説する具体的な対象です。ゲートウェイそのものの役割は[APIゲートウェイとは？役割・機能とリバースプロキシ／サービスメッシュとの違い・導入判断を解説](https://www.issoh.co.jp/tech/details/13210/)で整理しています。

## まとめ：API GatewayのWebSocket APIで接続管理を組むときの結論

API GatewayのWebSocket APIは、接続の受け付けと維持をAWSが担い、アプリ側は「接続IDの台帳」と「送り返す処理」だけを書く構成です。台帳はDynamoDBの1テーブルで足ります。$connectで書き込み、$disconnectで消し、送信時は`post_to_connection`で1件ずつ届けます。

実装で外せないのは3点です。$disconnectは届かないことがあるので、TTL属性で古い行を自動削除し、送信時の`GoneException`でも行を消します。接続は2時間で必ず切れ、10分無通信でも切れるため、クライアントへの再接続とハートビートの組み込みが必要です。ブラウザのWebSocketは独自ヘッダーを送れないので、認証トークンはクエリ文字列で$connectに渡します。

採用の目安は、利用者数が読めず、常駐サーバーの運用要員を置きたくない双方向通信です。1万人規模へ同じメッセージを頻繁に配る用途や、2時間を超える常時接続が前提の用途は、コンテナで自前のWebSocketサーバーを持つほうが素直に組めます。

## API GatewayのWebSocket API｜ルート・接続ID・コールバックURL

REST APIやHTTP APIはリクエストごとに完結しますが、WebSocket APIは接続を保持したまま、届いたメッセージをルートに振り分けます。まず3つの部品を押さえます。

### $connect・$disconnect・$defaultとカスタムルートの4種の役割

[ルートの作成手順を説明した公式ドキュメント](https://docs.aws.amazon.com/apigateway/latest/developerguide/websocket-api-develop-routes.html)によると、あらかじめ用意されたルートは`$connect`・`$disconnect`・`$default`の3つで、これにカスタムルートを足せます。$connectは接続の確立時、$disconnectは切断後、$defaultはどのルートにも一致しないメッセージが来たときに呼ばれます。

カスタムルートのキーに`$`は付けられません。`sendmessage`や`join`のように、業務の操作名をそのまま付けます。ハンドシェイクやフレームの仕組みそのものは[WebSocketとは？仕組み・ハンドシェイクからNode.js実装・運用の判断まで実装者向けに解説](https://www.issoh.co.jp/tech/details/15815/)で扱っており、本記事はAPI Gateway側の設定に絞ります。

### ルート選択式$request.body.actionでメッセージを振り分ける仕組み

どのルートに渡すかは、API単位で1つ設定する「ルート選択式」で決まります。`$request.body.action`と設定すると、クライアントが送ったJSONの`action`の値がルートキーとの照合対象です。`{"action":"sendmessage","message":"hi"}`なら`sendmessage`ルートが動きます。

JSONでないメッセージや一致しない値は$defaultへ回り、$defaultも無ければエラーが返ります。公式ドキュメントは、評価結果が配列やオブジェクトになると文字列化されるため、単一の値を指す式にするよう勧めています。

### 接続ID宛てに@connections APIでサーバーから送り返す経路と権限

サーバーからクライアントへ送るときは、接続IDを指定して`@connections`というAPIを呼びます。[@connectionsの使い方を説明した公式ドキュメント](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-how-to-call-websocket-api-connections.html)には、送信がPOST、状態確認がGET、強制切断がDELETEと書かれています。

この呼び出しはIAMによる認可の対象で、SigV4署名が必要です。SDKの`ApiGatewayManagementApi`クライアントを使えば署名は自動で付きます。エンドポイントは`https://{api-id}.execute-api.{region}.amazonaws.com/{stage}`で、カスタムドメインを使う場合はステージ名を外します。

## WebSocket APIの上限値｜接続2時間・アイドル10分・フレーム32KBの制約

設計の前提になるのはクォータです。どれも引き上げられない値が多く、後から構成を変えにくい部分なので、先に一覧で確認します。

### クォータ表で確認する接続時間・メッセージサイズ・新規接続数の上限

[WebSocket APIのクォータ表](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-execution-service-websocket-limits-table.html)から、実装に響く項目を2026年10月時点の値で抜き出しました。

| 項目              | 既定値       | 引き上げ | 実装への影響     |
| --------------- | --------- | ---- | ---------- |
| 接続時間            | 2時間       | 不可   | 再接続処理が必須   |
| アイドル接続タイムアウト    | 10分       | 不可   | 定期送信で切断を防ぐ |
| フレームサイズ         | 32KB      | 不可   | 超過は1009で切断 |
| メッセージペイロード      | 128KB     | 不可   | 大きな本文はS3経由 |
| 新規接続数（Region単位） | 500件／秒    | 可    | 一斉再接続の山に注意 |
| 統合タイムアウト        | 50ミリ秒〜29秒 | 不可   | 重い処理は非同期化  |
| ルート数            | 300       | 可    | 操作名の粒度を絞る  |

同時接続数そのものには上限がありません。表の注記では、毎秒500件で2時間つなぎ続けた場合に最大3,600,000接続を扱えると説明されています。統合タイムアウトの29秒はREST APIと違って引き上げられず、ほかのAPI種別との比較は[API Gatewayのタイムアウト：29秒の上限を引き上げる手順と504の切り分け・非同期化](https://www.issoh.co.jp/tech/details/18479/)にまとめています。

### アイドル10分で切れる前にクライアントからハートビートを送る実装

ブラウザのWebSocketには、スクリプトからpingフレームを送るメソッドがありません。[WHATWGのWebSockets仕様](https://websockets.spec.whatwg.org/)は、pingとpongを「現在APIには公開していない」と明記しています。そのため、10分より短い間隔でアプリ層の小さなメッセージを送って接続を保ちます。

```
// API Gatewayのアイドル10分より短い5分間隔で、アプリ層のpingを送る
const url = "wss://abcdef123.execute-api.ap-northeast-1.amazonaws.com/production"
  + "?token=" + encodeURIComponent(idToken);
const ws = new WebSocket(url);
const timer = setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ action: "ping" }));
}, 5 * 60 * 1000);
ws.addEventListener("close", () => clearInterval(timer)); // 2時間の上限でも閉じるので、ここで再接続を予約する
```

このメッセージは課金対象です。[料金ページ](https://aws.amazon.com/api-gateway/pricing/)は制御フレームのping・pongを数えないとしていますが、アプリ層のJSONは1件として数えられます。5分間隔なら1接続あたり1時間12件で、東京の単価に当てはめた試算は[API Gatewayの料金を東京リージョンの実額で計算する：REST・HTTP・WebSocketの単価と付帯課金](https://www.issoh.co.jp/tech/details/18477/)で確かめられます。

## CLIによるWebSocket API作成とLambda・$connectの統合手順

ここからは東京リージョン（ap-northeast-1）にAPIを作ります。アカウントIDやAPI IDは例示の値なので、自分の環境の値に置き換えてください。

### create-apiでWEBSOCKETプロトコルのAPIとルート選択式を作成する

[WebSocket APIの作成手順](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-create-empty-api.html)どおり、`--protocol-type WEBSOCKET`とルート選択式を指定します。作成後にプロトコルは変えられず、エンドポイントはRegionalだけ、利用可能なTLSのバージョンは1.2と1.3だけです。接続IDを入れるDynamoDBテーブルも同時に作り、TTLを有効にしておきます。

```
aws apigatewayv2 create-api --name ws-chat --protocol-type WEBSOCKET \
  --route-selection-expression '$request.body.action' --region ap-northeast-1

aws dynamodb create-table --table-name ws-connections \
  --attribute-definitions AttributeName=connectionId,AttributeType=S \
  --key-schema AttributeName=connectionId,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

aws dynamodb update-time-to-live --table-name ws-connections \
  --time-to-live-specification "Enabled=true, AttributeName=expiresAt"
```

出力の`ApiId`を控えます。ルート選択式をシングルクォートで囲むのは、シェルが`$request`を変数として展開しないようにするためです。

### create-integrationとcreate-routeによる接続統合

Lambda関数を作った後、統合を作成してルートの`--target`に指定します。API GatewayからLambdaを呼ぶ許可も忘れずに付けます。

```
aws apigatewayv2 create-integration --api-id abcdef123 --integration-type AWS_PROXY \
  --integration-uri arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:111122223333:function:ws-connect/invocations

aws apigatewayv2 create-route --api-id abcdef123 --route-key '$connect' \
  --target integrations/xyz789

aws lambda add-permission --function-name ws-connect --statement-id apigw-ws \
  --action lambda:InvokeFunction --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:ap-northeast-1:111122223333:abcdef123/*"

aws apigatewayv2 create-stage --api-id abcdef123 --stage-name production --auto-deploy
```

`xyz789`はcreate-integrationが返す`IntegrationId`です。$disconnectとsendmessageも同じ手順で統合とルートを作ります。引数の詳細は[create-routeのCLIリファレンス](https://docs.aws.amazon.com/cli/latest/reference/apigatewayv2/create-route.html)と[create-integrationのCLIリファレンス](https://docs.aws.amazon.com/cli/latest/reference/apigatewayv2/create-integration.html)に載っています。

## LambdaとDynamoDBで接続IDを保存・削除・配信するPythonコードの書き方

Lambdaは3本に分けます。接続の登録、削除、配信です。どれもLambdaプロキシ統合で受け、イベントの`requestContext`から接続IDを取り出します。

### $connectで接続IDとユーザーIDをDynamoDBへ書き込むハンドラー

[$connectと$disconnectの挙動を説明した公式ドキュメント](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-route-keys-connect-disconnect.html)によると、$connectの統合が終わるまで接続は確立せず、失敗すれば接続そのものが張られません。書き込みに失敗したら200以外を返し、台帳に載らない接続を作らないようにします。

```
import os
import time

import boto3

table = boto3.resource("dynamodb").Table(os.environ["TABLE_NAME"])
TTL_SECONDS = 2 * 60 * 60 + 300  # 接続上限の2時間に5分の余裕を足す


def lambda_handler(event, context):
    ctx = event["requestContext"]
    qs = event.get("queryStringParameters") or {}
    # Lambdaオーソライザーが返したcontextはrequestContext.authorizerに入る
    user_id = (ctx.get("authorizer") or {}).get("userId", "anonymous")
    table.put_item(Item={
        "connectionId": ctx["connectionId"],
        "userId": user_id,
        "roomId": qs.get("room", "lobby"),
        "expiresAt": int(time.time()) + TTL_SECONDS,
    })
    return {"statusCode": 200}
```

接続IDは可変長の文字列で、長さや形式が変わる可能性があると公式ドキュメントに書かれています。正規表現で形式を検査したり、列の長さを固定したりしないでください。

### $disconnectの取りこぼしに備えTTL属性で古い接続を自動削除する

$disconnectのハンドラーは、接続IDをキーに`delete_item`を呼ぶだけです。ただし同じ公式ドキュメントは、$disconnectを「best-effort」のイベントとし、配信を保証しないと書いています。端末の電源断や回線切れでは、行が台帳に残ることがあります。

そこで、$connectで書いた`expiresAt`を[DynamoDBのTTL](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html)に使います。値はエポック秒のNumber型で、期限切れの行は通常数日以内に削除され、書き込みスループットを消費しません。削除は即時ではないので、配信側でも期限切れの行を読み飛ばします。

### post\_to\_connectionの配信とGoneException時の削除処理

sendmessageルートのLambdaは、台帳から宛先を読み、[boto3のpost\_to\_connection](https://docs.aws.amazon.com/boto3/latest/reference/services/apigatewaymanagementapi/client/post%5Fto%5Fconnection.html)で1件ずつ送ります。この関数は`GoneException`・`LimitExceededException`・`PayloadTooLargeException`・`ForbiddenException`を返しえます。

```
import json
import os
import time

import boto3
from boto3.dynamodb.conditions import Key

table = boto3.resource("dynamodb").Table(os.environ["TABLE_NAME"])


def lambda_handler(event, context):
    ctx = event["requestContext"]
    # カスタムドメインで公開している場合はステージ名を付けない
    endpoint = f"https://{ctx['domainName']}/{ctx['stage']}"
    api = boto3.client("apigatewaymanagementapi", endpoint_url=endpoint)
    body = json.loads(event.get("body") or "{}")
    data = json.dumps({"message": body.get("message", "")}, ensure_ascii=False).encode("utf-8")

    # roomIdをパーティションキーにしたGSI（room-index）で同じ部屋の接続だけを引く
    items = table.query(IndexName="room-index",
                        KeyConditionExpression=Key("roomId").eq(body.get("room", "lobby")))["Items"]
    now = int(time.time())
    for item in items:
        if item["expiresAt"] < now:
            continue  # TTLの削除待ちの行は送らない
        try:
            api.post_to_connection(ConnectionId=item["connectionId"], Data=data)
        except api.exceptions.GoneException:
            # すでに切れている接続は台帳から消す
            table.delete_item(Key={"connectionId": item["connectionId"]})
    return {"statusCode": 200}
```

GSIは事前に`roomId`をキーにして作っておきます。テーブル全体を`scan`する書き方は公式チュートリアルにもありますが、接続数に比例して読み取り量と処理時間が伸びる書き方です。1部屋の人数が数百を超える配信では、宛先を分割してSQSに積み、複数のLambdaで並列に送る構成に切り替えます。

### Lambdaのexecute-api:ManageConnections付与方針

配信用Lambdaには、@connectionsを呼ぶ`execute-api:ManageConnections`が必要です。[WebSocket APIのIAM認可の公式ドキュメント](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-control-access-iam.html)に、@connectionsのARN形式が示されています。

```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "execute-api:ManageConnections",
      "Resource": "arn:aws:execute-api:ap-northeast-1:111122223333:abcdef123/production/POST/@connections/*"
    },
    {
      "Effect": "Allow",
      "Action": ["dynamodb:Query", "dynamodb:DeleteItem"],
      "Resource": [
        "arn:aws:dynamodb:ap-northeast-1:111122223333:table/ws-connections",
        "arn:aws:dynamodb:ap-northeast-1:111122223333:table/ws-connections/index/room-index"
      ]
    }
  ]
}
```

権限が足りないと`ForbiddenException`になります。$connect用のLambdaには`dynamodb:PutItem`、$disconnect用には`dynamodb:DeleteItem`だけを付け、関数ごとにロールを分けます。

## wscatで接続・送信・切断を試し認証付きの$connectを確かめる手順

デプロイしたら、ブラウザを作る前にコマンドラインで動きを確かめます。npmで入れられる`wscat`が手軽です。

### wscatで2端末から接続しsendmessageルートの配信を確認する作業

[公式のチャットアプリのチュートリアル](https://docs.aws.amazon.com/ja%5Fjp/apigateway/latest/developerguide/websocket-api-chat-app.html)と同じ流れで、ターミナルを2つ開いて両方から接続します。

```
npm install -g wscat
wscat -c "wss://abcdef123.execute-api.ap-northeast-1.amazonaws.com/production?room=lobby&token=TEST"
Connected (press CTRL+C to quit)
> {"action": "sendmessage", "room": "lobby", "message": "hello"}
< {"message": "hello"}
```

片方から送ったメッセージがもう片方にも届けば、ルートと配信は動いています。Ctrl+Cで切った後にDynamoDBの行が消えていれば、$disconnectも届いたと判断できる状態です。消えていない場合は、ルートの統合と`add-permission`の付け漏れを先に疑います。

### ブラウザはヘッダーを付けられないためクエリ文字列でトークンを渡す

認可を掛けられるのは$connectだけで、[Lambdaオーソライザーの公式ドキュメント](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-lambda-auth.html)もREQUEST型を$connectに付ける前提で書かれています。この設定はAPI全体に効き、$connectを通らない接続は存在しないため、ほかのルートもまとめて守られます。

問題はトークンの渡し方です。WHATWG仕様のコンストラクタは`url`と`protocols`しか受け取らず、`Authorization`ヘッダーを足せません。ブラウザからはクエリ文字列で渡し、オーソライザーのIDソースにクエリ文字列を指定します。トークンが無いか無効なら、接続は401で拒否されます。REST APIやHTTP APIでのオーソライザーの選び方は[API Gatewayの認証：Cognito・JWT・Lambdaオーソライザー・IAM認可の使い分けと設定例](https://www.issoh.co.jp/tech/details/18493/)で解説しています。

クエリ文字列はアクセスログやCDNのログに残りやすいので、渡すのは有効期限が数分の使い捨てトークンにします。ログに何が出るかは[API Gatewayのログ設定：実行ログとアクセスログをCloudWatchに出す手順とX-Rayでの障害調査](https://www.issoh.co.jp/tech/details/18485/)の書式で確認できます。

## API GatewayのWebSocketを採用する条件と見送るべき構成の判断基準

ここまでの上限と実装の手間を踏まえ、採用の可否を条件で言い切ります。

### 接続数が読めず運用要員を置けない双方向通信なら採用してよい条件

採用してよいのは、次の条件がそろうときです。社内の通知、問い合わせチャット、進捗の押し出し配信がこれに当たります。

- 同時接続数が日や時間帯で大きく揺れ、常駐サーバーの台数を決めにくい
- 1件のメッセージが128KB以内に収まり、配信先が部屋単位で数百人以内
- 2時間ごとの再接続を、クライアント側で吸収できる
- WebSocketサーバーの監視やスケール設定を担う運用要員を置かない

この条件なら、使った分だけの課金で始められ、接続の維持やTLS終端を自分で持たずに済みます。サーバー側から一方向に流すだけで足りるなら、WebSocketよりSSEのほうが単純です。

### 2時間を超える常時接続や大量配信が主なら自前WebSocketを選ぶ場面

見送るべきなのは、1万人規模の購読者へ同じメッセージを頻繁に配る用途、128KBを超えるデータを流し続ける用途、2時間を超えて切れない接続が業務要件になっている用途です。API Gatewayの配信は接続IDごとの呼び出しなので、購読者数に比例してLambdaの実行時間と課金が増えます。コンテナで自前のWebSocketサーバーを持ち、Pub/Subで台数を増やす構成のほうが安く、速く配れます。

一方向の配信だけなら、[SSEとWebSocketの違いと使い分け｜WebRTC・WebTransportまで性能比較【2026年10月】](https://www.issoh.co.jp/tech/details/4539/)で比較したSSEも候補になります。リアルタイム通信を既存の業務システムやAPIとつなぐ設計で迷ったら、[API開発・システム連携の受託開発](https://www.issoh.co.jp/service/system/api/)でご相談いただけます。

## よくある質問

API GatewayのWebSocket APIを実装するときによく聞かれる質問を5つ取り上げます。

### API GatewayのWebSocketは同時に何接続まで扱えますか？

同時接続数そのものには上限が設けられていません。制約になるのは新規接続数で、既定ではアカウント・Region単位で毎秒500件です。公式のクォータ表は、毎秒500件で2時間つなぎ続けると最大3,600,000接続になると説明しています。障害後の一斉再接続でこの毎秒500件に当たりやすいので、クライアントの再接続には乱数の待ち時間を入れて山をならします。

### WebSocketの接続が2時間や10分で切れるのはなぜですか？

API GatewayのWebSocket APIには、接続時間2時間とアイドル接続タイムアウト10分の上限があり、どちらも引き上げられません。10分の無通信で切れる場合は、5分程度の間隔でアプリ層のメッセージを送ってください。2時間の上限は避けられないため、closeイベントで再接続し、再接続後に未読のメッセージを取り直す仕組みをクライアント側に持たせます。

### $disconnectが呼ばれずDynamoDBに接続IDが残るのはなぜですか？

公式ドキュメントが$disconnectをbest-effortのイベントとし、配信を保証していないためです。端末の電源断や通信断では呼ばれないことがあります。対策は2つです。$connectで有効期限をエポック秒で書き込んでDynamoDBのTTLで消すことと、送信時に`GoneException`が返った接続IDをその場で削除することです。

### WebSocket APIの認証はどう実装すればよいですか？

$connectルートにLambdaオーソライザー（REQUEST型）かIAM認可を設定します。認可を掛けられるのは接続時だけで、その設定の適用範囲はAPI全体です。ブラウザのWebSocketはヘッダーを追加できないため、トークンはクエリ文字列で渡し、オーソライザーのIDソースにクエリ文字列を指定します。ログに残る前提で、有効期限の短いトークンを使ってください。

### サーバー側からクライアントへメッセージを送るには何が必要ですか？

接続IDと、`execute-api:ManageConnections`の権限を持つ実行ロールが必要です。SDKの`ApiGatewayManagementApi`クライアントを、`https://{ドメイン}/{ステージ}`をエンドポイントにして作り、`post_to_connection`を呼びます。カスタムドメインを使う場合はステージ名を外す指定が必要です。接続が切れていれば`GoneException`が返ります。

## 関連記事

- [APIゲートウェイとは？役割・機能とリバースプロキシ／サービスメッシュとの違い・導入判断を解説](https://www.issoh.co.jp/tech/details/13210/)：APIゲートウェイの役割と導入判断。
- [WebSocketとは？仕組み・ハンドシェイクからNode.js実装・運用の判断まで実装者向けに解説](https://www.issoh.co.jp/tech/details/15815/)：プロトコルの仕組みと自前サーバーでの実装。
- [API Gatewayの料金を東京リージョンの実額で計算する：REST・HTTP・WebSocketの単価と付帯課金](https://www.issoh.co.jp/tech/details/18477/)：メッセージと接続時間の単価で月額を試算する手順。
- [API Gatewayのタイムアウト：29秒の上限を引き上げる手順と504の切り分け・非同期化](https://www.issoh.co.jp/tech/details/18479/)：API種別ごとの統合タイムアウトの上限。
- [AWS サーバーレス ハンズオン｜Lambda・DynamoDB・API GatewayでREST APIを作る手順【2026年版】](https://www.issoh.co.jp/tech/details/2818/)：同じ部品でREST APIを作る手順。

---

出典: [API GatewayのWebSocket API：$connectとDynamoDBで接続管理を実装する手順と上限値](<https://www.issoh.co.jp/tech/details/18489/>)（株式会社一創）
