全体の紹介
この記事では、GAS → Discord Webhook 構成で発生した429 / 1015エラーをAWS Lambda Function URL を中継することで改善した事例を紹介します。
対象読者
- GASからDiscordに通知を飛ばしているが、なぜか不定期に429エラーが発生して困っている方
- 429エラーだけでなく、Cloudflareの「1015(Rate Limited)」エラーに遭遇した方
- サーバーレス(Lambda)を使った簡単なプロキシ・中継構成に興味がある方
1. 実際に作っていたプロダクトの紹介
所属するサークルでは、メンバーの自己紹介をGoogleスライドで管理する体制をとっており、その運用を少し楽にするためのシステムを作っています。
流れとしてはかなりシンプルで、メンバーがGoogleフォームから必要事項と自己紹介用のスライドを送信すると、その内容を全体向けの自己紹介スライドに自動で追加する、というものです。
さらに、追加処理が正常に完了した場合は、サークルのDiscordサーバーへ通知を飛ばすようにしています。
これによって、「誰の自己紹介が登録されたのか」「問題なく反映されたのか」を運営側がすぐ確認できるようになっています。
全体向けスライドへの追加は、基本的に末尾へ挿入するだけにしていて、その後の並び替えや最終的な権限付与などは人の手で行っており、完全な自動化はしていません。
技術的にはもっと自動化することもできるんですが、実際の運用を考えると、「機械に任せる部分」と「人が確認する部分」のバランスを取った運用をしています。


そんな仕組みを作っている中で、今回奇妙なことが起きました。
2. 不定期に返される429エラー
大きな問題、それはDiscord Webhookの429エラーです。いわゆるレート制限とも呼ばれるものですね。
フォームの条件やGASコードなど、一切条件を変えていないのにも関わらず、通知が正常に飛ぶときと失敗するときがある。非常に不安定な状態が続きました。
スライドの追加はどの条件下でも問題なく成功しているので、Webhookとの関係性を深く見ていく必要がありました。
最初はよくある原因を疑っていて、例えばUser-Agentを付与してみたり、送信タイミングを少しずつずらしてみたり、ネット上で見つかる「429対策」をいろいろ試しました。しかしながらこれらはほとんど効果がなく、問題解決には至りませんでした。
画像はGASでのログ。スライドの追加は成功しているのに通知が失敗するときの例です。
詳しく調べると、同様の問題に直面している方の記事やポストを発見しました。
調査した限りでは、Cloudflare 側の制限や共有IP環境の影響を受けていた可能性が高そうでした。
GASからリクエストを送る際、Google側が保有する共有IPアドレスを利用します。そのため、自分自身は数回しかWebhookを叩いていなくても、同じIPアドレスを共有している世界中の別のユーザーがDiscordへ大量のリクエストを送っていた場合、そのIP全体がCloudflare側で一時的にブロック(1015エラーなど)されてしまい、巻き込み事故を食らうという現象が起きていたと考えられます。
また、Discordのレート制限について調べていくと、単純な「1秒間に何回まで」の話だけではなく、429レスポンス時に retry_afterが返される仕組みや、Cloudflare側の一時ブロックなど、複数種類の制限が存在していることが分かりました。
特に厄介だったのが、「普通のレート制限」と「Cloudflare系の制限」の挙動がかなり異なることです。
通常の429であれば Retry-After ヘッダーを見て一定時間待てば再送できますが、Cloudflare側で制限された場合は、待機時間すら返ってこないケースもありました。
そのため、「少し待って再送すればOK」という単純な話ではなく、環境や送信元IPの状態によって突然Webhook全体が不安定になる、かなり扱いの難しい問題でした。
特に「一定時間待って再送する」という方法については、GASの実行時間制限の問題も発生します。そして何より、メンバーがフォームを提出してもすぐに通知が届かないのは致命的ですからね。
「リトライすればなんとかなる」というより、「そもそもこの構成でWebhookへ直接投げ続けるのが危ないのでは?」という方向に考えが変わっていきました。
3. Lambdaを経由してやってみる
調べると、Lambda Function URLを活用して解決できたという報告を見かけました。
まさに自分がハマっていた問題と同じだったので、早速実践してみました。
変更前の構成
変更前はGASのUrlFetchApp.fetchからDiscord Webhook URLに直接POSTしていました。
Google Form 送信
-> GAS 実行
-> Google Slides へスライド追加
-> Discord Webhook へ通知
変更後の構成
AWS Lambda Function URLを公開エンドポイントとして作成し、GASはそこへJSON をPOSTするだけにしました。
GAS
POST JSON
Authorization: Bearer <token>
Lambda Function URL
Bearer Token 検証
Discord Webhook URL へ POST
Discord
なお、今回はAPI Gatewayを使いませんでした。今回の用途では複雑なルーティングや認可機構が不要で、単一エンドポイントに JSONを投げられれば十分であると判断したためです。
4. Lambdaの設計
使用するにあたり、以下のように設定をしてみました。
- Node.js 22.x runtime を使用
- Function URL の認証タイプは
NONEに設定 - Lambda 内で
Bearer Tokenを検証 - Discord Webhook URL は環境変数で管理
- GAS から受け取った JSON をDiscordに転送
- Discord のレスポンスをログとして出力
- Discord が 2xx 以外を返した場合はLambdaも失敗扱いにする
使用した環境変数は以下の通りです。
DISCORD_WEBHOOK_URL (Discord WebhookのURL)
RELAY_TOKEN
5. GAS 側の変更
GAS 側では Discord Webhook URLを直接持たないように変更しました。
DISCORD_RELAY_URL (LambdaのURL)
DISCORD_WEBHOOK_NAME (Webhookで使う名前)
RELAY_TOKEN
SLIDES_TARGET_PRESENTATION_URL (スライドのURL)
GASでは、スクリプトプロパティとして機密情報を管理することができます。
6. 動作確認
Lambdaのテストイベントでは、Lambdaのレスポンスが statusCode: 200になりました。

Discord にテストメッセージが正常に投稿されています!

さらに Googleフォームから実際に送信し、Googleスライドへの追加、Discord通知、Lambda ログ出力まですべて正常に動作しました。
これをもって、GASがDiscord Webhookを直接叩かない構成へ移行することができました。めでたし!
料金面について
やはり料金は怖いですよね。今回の用途はかなり低頻度な通知なので、Lambda 自体の料金はほぼ無料枠に収まる想定でした。
ただ、Function URLは公開エンドポイントになるため、以下のようなことで実行回数が増えるリスクは存在します。
- URLやトークンの漏えい
- Botからの大量アクセス
- 想定外の連続実行
そのため、最低限ですが以下の対策を実施してみました。
-
RELAY_TOKENを十分長くランダムなものにする - Function URL やトークンを公開しない
- Lambdaの予約済み同時実行数を低めに設定する
- CloudWatch Logsを確認できるようにする
- CloudWatch Alarmを設定する
備えあれば憂いなし。ぜひやっておきましょう。
おわりに
GAS から Discord Webhookを直接叩く構成をやめ、Lambda Function URLを中継する形に変更して通知処理の成功率を上げることができました。実はAWSのサービスはそこまで触ったことがなく、今回Lambdaを本格的に触るきっかけにもなりました。
気を付けることは多いですが、便利なサービスがいっぱいあるので機会があれば触ってみます。同様の問題で困っている方の助けになれば幸いです。
同様の問題で検索している方向け
- GAS Discord Webhook 429
- GAS Discord 1015
- Discord Webhook Cloudflare
- GAS UrlFetchApp 429
- Discord Webhook Lambda relay
参考

