Edgio

Waiting Room

You may only deploy this edge function using CDN-as-Code due to Node.js dependencies. It will not work when deployed through the Edgio Console.
A waiting room can be used to restrict access to a website by means of queueing requests during high traffic periods. This can be useful for preventing a website from becoming overloaded during a traffic spike, or for restricting access to a website during a limited-time event.
The example is derived from an Upstash blog post using an edge database to store the number of active sessions, and to determine whether the current user is active. If the number of active sessions is less than the maximum allowed, or if the current user is already active, the request will be allowed to proceed. Otherwise, the request will be queued and the user will be shown a waiting room page.

Router Configuration

In the Edgio router, you can use the edge_function feature to specify the path to the edge function that will handle the waiting room.
JavaScriptroutes.js
1import {Router, edgioRoutes} from '@edgio/core';
2
3export default new Router()
4 .use(edgioRoutes)
5
6 .match('/:path*', {
7 edge_function: './edge-functions/main.js',
8 });

Edge Function

The edge function will be responsible for determining whether the request should be allowed to proceed, or whether it should be queued for later processing. In this example, we will use Upstash to store the number of active sessions, and to determine whether the current user is active.
The following sample code contains import declarations that are not documented in this guide. View the full source code for these functions from within the edgio-examples repository.
JavaScriptedge-functions/main.js
1import createFetchForOrigin from '../../../utils/createFetchForOrigin';
2import {
3 getCookiesFromRequest,
4 setCookieToResponse,
5} from '../../../utils/cookies';
6import {setEnvFromContext} from '../../../utils/polyfills/process.env';
7import waitingPage from './waitingPage';
8
9// Constants
10const COOKIE_NAME_ID = '__sessiong_id';
11const COOKIE_NAME_TIME = '__session_last_update_time';
12const TOTAL_ACTIVE_USERS = 2;
13const SESSION_DURATION_SECONDS = 15;
14
15// Setup fetch function for Upstash
16const fetch = createFetchForOrigin('upstash');
17
18/**
19 * Main handler for the edge request.
20 */
21export async function handleHttpRequest(request, context) {
22 let resp;
23
24 // Set context environment variables to process.env
25 setEnvFromContext(context);
26
27 const cookies = getCookiesFromRequest(request);
28
29 // Get user ID from cookie or generate a new one
30 const userId = cookies[COOKIE_NAME_ID] ?? generateId();
31
32 // Get the current number of active sessions and the active user
33 const size = await getRecordCount();
34 const isActiveUser = (await getRecord(userId)) === '1';
35
36 console.log('Current number of active sessions: ', size);
37
38 // Check capacity
39 if (size < TOTAL_ACTIVE_USERS || isActiveUser) {
40 // User is able to access the website
41 resp = await getDefaultResponse(request, userId);
42 } else {
43 // User is not able to access the website, hold them in the waiting room
44 resp = await getWaitingRoomResponse();
45 }
46
47 return resp;
48}
49
50/**
51 * Generate a random ID
52 */
53function generateId(len = 10) {
54 return Array.from({length: len}, () =>
55 ((Math.random() * 36) | 0).toString(36)
56 ).join('');
57}
58
59/**
60 * Handle the default response.
61 */
62async function getDefaultResponse(request, userId) {
63 const cookiesToSet = [[COOKIE_NAME_ID, userId]];
64
65 // Read the session cookie and update the expiry time
66 const cookies = getCookiesFromRequest(request);
67 const now = Date.now();
68 const lastUpdate = cookies[COOKIE_NAME_TIME];
69 let lastUpdateTime = 0;
70
71 if (lastUpdate) {
72 lastUpdateTime = parseInt(lastUpdate);
73 }
74
75 const diff = now - lastUpdateTime;
76 const updateInterval = (SESSION_DURATION_SECONDS * 1000) / 2;
77 if (diff > updateInterval) {
78 await setExpiryRecord(userId, '1', SESSION_DURATION_SECONDS);
79 cookiesToSet.push([COOKIE_NAME_TIME, now.toString()]);
80 }
81
82 // Fetch the response from the origin
83 const response = await fetch(request);
84
85 // Set the cookies
86 setCookieToResponse(response, cookiesToSet);
87
88 return response;
89}
90
91/**
92 * Send a REST request to Upstash.
93 */
94async function sendUpstashRequest(cmd) {
95 cmd = Array.isArray(cmd) ? cmd.join('/') : cmd;
96
97 return (
98 await fetch(`${process.env.UPSTASH_REDIS_REST_URL}`, {
99 method: 'POST',
100 body: JSON.stringify(cmd.split('/')),
101 headers: {
102 Authorization: `Bearer ${process.env.UPSTASH_REDIS_REST_TOKEN}`,
103 },
104 })
105 ).json();
106}
107
108/**
109 * Get the current number of records.
110 */
111async function getRecordCount() {
112 const data = await sendUpstashRequest('DBSIZE');
113 return data.result;
114}
115
116/**
117 * Fetch a record from Upstash by key.
118 */
119async function getRecord(key) {
120 const data = await sendUpstashRequest(['GET', key]);
121 return data.result;
122}
123
124/**
125 * Set a record with an expiry time in Upstash.
126 */
127async function setExpiryRecord(key, value, seconds) {
128 return sendUpstashRequest(['SET', key, value, 'EX', seconds]);
129}
130
131/**
132 * Response for the waiting room.
133 */
134async function getWaitingRoomResponse() {
135 const response = new Response(waitingPage);
136 response.headers.set('content-type', 'text/html;charset=UTF-8');
137 return response;
138}
JavaScriptedge-functions/waitingPage.js
1export default `
2<!DOCTYPE html>
3<html lang="en">
4
5<head>
6 <meta charset="UTF-8">
7 <meta http-equiv='refresh' content='5'>
8 <title>Waiting Room</title>
9 <style>
10 * {
11 box-sizing: border-box;
12 margin: 0;
13 padding: 0;
14 }
15
16 body {
17 line-height: 1.4;
18 font-size: 1rem;
19 font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
20 padding: 2rem;
21 display: grid;
22 place-items: center;
23 min-height: 100vh;
24 background-color: #f3f4f6;
25 color: #333;
26 }
27
28 .container {
29 width: 100%;
30 max-width: 800px;
31 text-align: center;
32 background-color: #fff;
33 padding: 2rem;
34 border-radius: 10px;
35 box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
36 }
37
38 p {
39 margin-top: .5rem;
40 }
41
42 .loader {
43 border: 6px solid #f3f3f3;
44 border-top: 6px solid #3498db;
45 border-radius: 50%;
46 width: 50px;
47 height: 50px;
48 animation: spin 1s linear infinite;
49 }
50
51 @keyframes spin {
52 0% {
53 transform: rotate(0deg);
54 }
55 100% {
56 transform: rotate(360deg);
57 }
58 }
59
60 h1 {
61 margin-top: 20px;
62 margin-bottom: 20px;
63 }
64
65 </style>
66</head>
67
68<body>
69 <div class='container'>
70 <div class="loader"></div>
71 <h1>Almost There!</h1>
72 <p>Our site is currently at full capacity. Thanks for your patience.</p>
73 <p>You'll be redirected shortly. Please do not close your browser.</p>
74 </div>
75</body>
76
77</html>
78`;