Cloudflare WorkersとUpstash Redisで作る、Webサイト独自の待合室(Waiting Room)
この記事では、Cloudflare WorkersとUpstash Redisを組み合わせて、Webサイト向けの待合室(Waiting Room)ページを実装する方法を解説します。
なぜ待合室が必要なのか?
Webサイトへの訪問者数が増えるのは本来喜ばしいことですが、必ずしもそうとは限りません。突発的な大量トラフィックはアプリケーションを容易に圧倒し、最悪の場合、サービス全体が停止してしまうこともあります。待合室は、トラフィック急増時にアクセスを制御し、リソースを保護するための有効なソリューションです。
Cloudflareには公式の「Waiting Room」機能がありますが、残念ながらBusinessプランおよびEnterpriseプラン限定の提供です。ご安心ください。この記事では、Cloudflare WorkersとUpstash Redisを使えば、どのような規模・種類のWebサイトでも待合室を構築できることをお見せします。
仕組みはどうなっている?
待合室の動作は、次の2つのパラメータで制御します。
- 最大セッション継続時間(Max Session Duration): 訪問者がサイト上でアイドル状態のままいられる時間
- 最大同時収容人数(Max Website Capacity): サイトが同時に受け入れられる訪問者数
訪問者がサイトにアクセスすると、まず一意のキー(セッションキーのようなもの)を生成し、Redisに保存します。このキーには有効期限として最大セッション継続時間を設定します。訪問者をサイトに入れる前に、Redisのdbsize(現在のキー総数)を確認し、その値が最大同時収容人数を超えている場合は、訪問者を待合室へ誘導します。
待合室は静的なHTMLページですが、30秒ごとに自動更新されます。つまり30秒に1回、空きが出ていれば訪問者はサイトへ入れる仕組みです。
また、生成した一意のキーはCookieとして訪問者のリクエストに付与されます。同じ訪問者からのリクエストには常に同じキーが含まれるため、キャパシティが満杯の状態でも「すでにサイト内にセッションを持つ訪問者かどうか」を判定できます。つまり、自分のキーがRedisのキースペースに存在する限り、たとえ満員でもサイトへの入場が許可されます。逆に、最大セッション継続時間を超えてアイドル状態が続くとキーはRedisから削除され、その後リクエストを送ったときにキャパシティが満杯であれば、待合室へ転送されるという流れです。
このロジックはCloudflare Workers上に実装し、RedisストアとしてはUpstashを使用します。ここからは、この技術選定の背景について説明します。
なぜCloudflare Workersなのか?
待合室の実装は、Webサイトへのすべてのリクエストを横取り(インターセプト)することになります。そのため、パフォーマンスオーバーヘッドは最小限である必要があります。Cloudflare WorkersはCloudflareのエッジインフラを活用するため、世界中どこでも最低レベルのレイテンシを実現できます。さらにAWS Lambdaと異なりコールドスタート問題がなく、サーバーレス技術なのでスケーラビリティも心配ありません。
なぜUpstash Redisなのか?
新しい訪問者を受け入れる前に、現在のサイトの利用状況(サイズ)を毎回確認する必要があります。しかしCloudflare Workersはステートレスなので、この情報は外部に保持しなければなりません。低レイテンシという点でRedisは最適な選択肢です。ただし、一般的なRedisサービスはTCPベースの接続を要求しており、Cloudflare Workersはこれをサポートしていません。そこで登場するのがUpstashです。UpstashはREST APIを内蔵した唯一のRedisサービスであり、グローバルレプリケーションのおかげで世界中どこでも低レイテンシを実現します。
ステップバイステップでの実装
以下、プロジェクトを段階的に実装していきます。クローンしてすぐに自分のサイトへ待合室をセットアップしたい方は、ソースコードのreadmeの手順に従ってください。
1. プロジェクトのセットアップ
wranglerを使ってプロジェクトを作成します。
wrangler generate waiting-room
続いて依存関係をインストールします。
npm install cookie upstash@redis
2. wrangler.tomlの更新
typeを更新します。
type = "webpack"
CloudflareアカウントIDを設定します。アカウントIDの確認方法はこちらを参照してください。
account_id = "REPLACE_HERE"
以下の変数を追加します。
[vars]
UPSTASH_REDIS_REST_TOKEN = "REPLACE_HERE"
UPSTASH_REDIS_REST_URL = "REPLACE_HERE"
TOTAL_ACTIVE_USERS = 10
SESSION_DURATION_SECONDS = 30
UpstashコンソールからGlobalデータベースを作成してください。RESTトークンとURLをコンソールからコピー&ペーストするだけです。なお、Redisデータベースは初期状態で空であり、このアプリケーション専用として使用する必要があります。
TOTAL_ACTIVE_USERSとSESSION_DURATION_SECONDSは、自身の要件に応じて適切に設定してください。
3. index.js
index.jsはCloudflare Workersの実装ファイルです。すべてのロジックをこの中に記述します。以下のコードをコピー&ペーストしてください。
import { parse } from "cookie";
import { Redis } from "@upstash/redis/cloudflare";
const redis = Redis.fromEnv();
addEventListener("fetch", (event) => {
event.respondWith(
handleRequest(event.request).catch(
(err) => new Response(err.stack, { status: 500 })
)
);
});
const COOKIE_NAME_ID = "__waiting_room_id";
const COOKIE_NAME_TIME = "__waiting_room_last_update_time";
const init = {
headers: {
Authorization: "Bearer " + UPSTASH_REDIS_REST_TOKEN,
},
};
async function handleRequest(request) {
const { pathname } = new URL(request.url);
if (!pathname.startsWith("/favicon")) {
const cookie = parse(request.headers.get("Cookie") || "");
let userId;
if (cookie[COOKIE_NAME_ID] != null) {
userId = cookie[COOKIE_NAME_ID];
} else {
userId = makeid(8);
}
const size = await redis.dbsize();
console.log("current capacity:" + size);
// there is enough capacity
if (size < TOTAL_ACTIVE_USERS) {
return getDefaultResponse(request, cookie, userId);
} else {
// site capacity is full
const user = await redis.get(userId);
if (user === "1") {
// the user has already active session
return getDefaultResponse(request, cookie, userId);
} else {
// capacity is full so the user is forwarded to waiting room
return getWaitingRoomResponse(userId);
}
}
} else {
return fetch(request);
}
}
async function getDefaultResponse(request, cookie, userId) {
// uncomment below to test the function with a static html content
// const newResponse = new Response(default_html)
// newResponse.headers.append('content-type', 'text/html;charset=UTF-8')
const response = await fetch(request);
const newResponse = new Response(response.body, response);
const now = Date.now();
let lastUpdate = cookie[COOKIE_NAME_TIME];
if (!lastUpdate) lastUpdate = 0;
const diff = now - lastUpdate;
const updateInterval = (SESSION_DURATION_SECONDS * 1000) / 2;
if (diff > updateInterval) {
await redis.setex(userId, SESSION_DURATION_SECONDS, 1);
newResponse.headers.append(
"Set-Cookie",
`${COOKIE_NAME_TIME}=${now}; path=/`
);
}
newResponse.headers.append(
"Set-Cookie",
`${COOKIE_NAME_ID}=${userId}; path=/`
);
return newResponse;
}
async function getWaitingRoomResponse(userId) {
const newResponse = new Response(waiting_room_html);
newResponse.headers.set("content-type", "text/html;charset=UTF-8");
return newResponse;
}
function makeid(length) {
let result = "";
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
const waiting_room_html = `
<title>Waiting Room</title>
<meta http-equiv='refresh' content='30' />
<style>*{box-sizing:border-box;margin:0;padding:0}body{line-height:1.4;font-size:1rem;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif;padding:2rem;display:grid;place-items:center;min-height:100vh}.container{width:100%;max-width:800px}p{margin-top:.5rem}</style>
<div class='container'>
<h1>
<div>You are now in line.</div>
<div>Thanks for your patience.</div>
</h1>
<p>We are experiencing a high volume of traffic. Please sit tight and we will let you in soon. </p>
<p><b>This page will automatically refresh, please do not close your browser.</b></p>
</div>
`;
const default_html = `
<title>Waiting Room Demo</title>
<style>*{box-sizing:border-box;margin:0;padding:0}body{line-height:1.4;font-size:1rem;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif;padding:2rem;display:grid;place-items:center;min-height:100vh}.container{width:100%;max-width:800px}p{margin-top:.5rem}</style>
<div class="container">
<h1>
<div>Waiting Room Demo</div>
</h1>
<p>
Visit this site from a different browser, you will be forwarded to the waiting room when the capacity is full.
</p>
<p> Check <a href={"https://github.com/upstash/waiting-room"} style={{"color": "blue"}}>this project </a> to set up a waiting room for your website.</p>
</div>
`;
上記のコードでは、waiting_room_html変数を編集できます。これは待合室ページの静的HTMLです。日本語のメッセージに書き換えても問題ありません。
handleRequestメソッドは、サイトの空き状況に応じてgetDefaultResponseまたはgetWaitingRoomResponseのいずれかを返します。
4. ローカルでの実行
待合室をローカルでテストするには、キャパシティを1、セッション継続時間を30秒に設定しておくと分かりやすいでしょう。そのうえで、以下のコマンドを実行します。
wrangler dev
Chromeで https://127.0.0.1:8787/ を開くと、次のように表示されます。

続いて、同じURLをSafari(またはChromeのシークレットモード)で開くと、次のように表示されます。

30秒以上待つと、待合室のページが自動更新され、サイトへ入場できるはずです。
ローカル環境では実際のWebサイトへの転送が行われないため、Cloudflareの404ページが表示されますが、これは正常な動作です。
5. 公開(デプロイ)
以下のコマンドでCloudflare Workersの関数をデプロイします。
wrangler publish
すると、https://waiting-room.upsdev.workers.dev/ のようなURLが発行されます。
次に、このWorkerを自分のWebサイトにルーティングしましょう。まず、ドメインのネームサーバーがCloudflareを指している必要があります。詳細はこちらを確認してください。その後、CF Workersダッシュボードでドメインをルートとして追加し、Workers関数を選択します。

まとめ
Cloudflare WorkersとUpstash Redisのおかげで、アプリケーションのコードには一切手を加えることなく、待合室を構築できました。エッジ関数とUpstashを組み合わせることで、ここまで強力なソリューションが実現できる好例だと言えるでしょう。
今後改善できる点もあります。
- 推定待ち時間の表示: 平均待ち時間を計算して表示できるようにする
- 公平で順序のあるキュー: 現状では空きが出た際に待機中の訪問者がランダムに入場します。キューを保持し、順番通りに入場できるように改善できる
ただし、これらの改善はいずれもより多くの状態管理と追加のリモート呼び出しを必要とするため、今回は省略しました。もしユースケース上どうしても必要な場合は、Cloudflareチームがエンタープライズ向けソリューションで行った取り組みを参考にするとよいでしょう。
ソースコードもぜひチェックしてみてください。
-
Cloudflare WorkersとUpstash Redisで作る、Webサイト独自の待合室(Waiting Room)
この記事では、Cloudflare WorkersとUpstash Redisを組み合わせて、Webサイト向けの待合室(Waiting Room)ページを実装する方法を解説します。 なぜ待合室が必要なのか? Webサイトへの訪問者数が増えるのは本来喜ばしいことですが、必ずしもそうとは限りません。突発的な大量トラフィックはアプリケーションを容易に圧倒し、最悪の場合、サービス全体が停止してしまうこともあります。待合室は、トラフィック急増時にアクセスを制御し、リソースを保護するための有効なソリューションです。 Cloudflareには公式の「Waiting Room」機能がありますが、残念ながらBu
-
Cloudflare WorkersとRedisで実現するエッジコンピューティング活用術
エッジコンピューティングは、近年もっとも注目されている技術のひとつです。CDNがファイルをユーザーの近くに配置できるようにしたのと同じように、エッジコンピューティングはアプリケーションそのものをユーザーの近くで実行できるようにします。これにより、開発者はグローバルに分散され、高いパフォーマンスを発揮するアプリケーションを構築できるようになります。 Cloudflare Workersとステートレス性の課題 現在この分野をリードしている製品がCloudflare Workersです。コールドスタートのないサーバーレス実行環境を提供し、Cloudflareのグローバルネットワークを活かすことで、ア