# Waiting rooms for ticket drops and flash sales

> Put a fair, live line in front of a sale: visitors see their place move, come in at the rate you set, and your site checks a signed pass. Any host, Laravel and Filament included.

Source: https://hibira.io/use-cases/waiting-room

A ticket release, a limited drop or a pre-order opening brings a whole day's traffic in the first minute. Autoscaling can't react that fast, so checkout slows down for everyone and the fastest connection wins. A waiting room puts a calm, fair line in front of the busy pages: visitors take a number, watch the line move live, and come in at the rate your site can serve. You sign up and set one up yourself, with no sales call and no change of host or CDN.

## What your visitors get

- A queue number and "4,812 people ahead of you", updated live while they wait.
- A place that a refresh or a second tab doesn't lose or improve.
- A hosted waiting page in English, Traditional and Simplified Chinese, Japanese, Korean, Thai and Vietnamese.
- A clear countdown before the sale starts, and a clear "sold out" when it ends.

![The hosted waiting page: the visitor's place in line, people ahead, the estimated wait and your notice, in the visitor's language.](https://hibira.io/portal/articles/waitroom-page-en-light.webp)

## What you get

- A line that moves at the rate you set, for example 300 visitors a minute.
- Buttons for the moments that matter: pause the line, let 500 in now, close it when stock runs out.
- A signed pass for each admitted visitor that your site checks before serving the protected page, so nobody skips the line by guessing a URL.
- One check-out per queue number if you want it, using the pass's ticket ID.
- Visitors waiting in line don't use your app's connections, so the realtime features on the rest of your site keep their room during the rush.
- A notice for everyone in line, such as "Zone A is sold out", shown on the waiting page at once.

![Waiting rooms in the portal: live counts, the rate, when the line clears, and buttons to pause, let people in, mark sold out or post a notice.](https://hibira.io/portal/articles/waitroom-console-en-light.webp)

Waiting rooms are an add-on to any Hibira plan. One room and 5,000 queued visitors a month are free; paid plans start at $29 a month, and a one-off event pass is $49 for 72 hours. Past your plan, extra visitors come from credits and nobody is turned away. See [pricing](https://hibira.io/pricing#waiting-room).

## Built with

- **Waiting rooms** in your Hibira app, managed from the portal or with signed HTTP API calls.
- The **hosted waiting page**, `ws.hibira.io/wait/APP_KEY/ROOM`, or your own page with [@hibira-io/waiting-room](https://www.npmjs.com/package/@hibira-io/waiting-room).
- A **cache channel** per room (`cache-waitroom-ROOM`): one message tells every waiting visitor how far the line has moved, and each browser works out its own position.
- **Passes** signed with your app secret (HMAC-SHA256), checked by your site with no call to Hibira.

```text
flow: Visitors without a pass wait in line; when their number comes up they return with a signed pass
Visitor|opens /drop --no pass: redirect--> *Hibira|hosted waiting page --their turn: `?hibira_pass=…`--> Your site|checks the pass, sets a cookie
== Moving the line
Your team|portal, Filament or API --open, pause, admit, close--> *Hibira|cache-waitroom-presale --one update--> Every waiting visitor|works out its own position
```

## Choose how to connect

Pick the one that matches where your site runs. The higher in the list, the harder it is to get around.

| Where your site runs | How it connects | Check happens |
| --- | --- | --- |
| Cloudflare Workers, Next.js on Vercel, Deno, Bun | `protect()` from [@hibira-io/waiting-room](https://www.npmjs.com/package/@hibira-io/waiting-room) | before your page is served |
| Your own server: Express, Laravel and others | a middleware that checks the pass | before your route runs |
| Fully custom waiting page | `joinWaitingRoom()` in the browser, plus one of the checks above | before your route runs |
| iOS, Android, Flutter or Unity app | the public waiting room endpoints, then the pass in an API header | in your API |
| No backend of your own (hosted shop) | the hosted page, plus a pass check at checkout | at checkout |

## Step 1: create a room

Create the room in the portal, or with a signed request to the HTTP API, signed the same way as publishing an event:

```http
POST /apps/APP_ID/waiting_rooms/presale
{
  "admit_per_minute": 300,
  "state": "paused",
  "starts_at": "2026-12-04T12:00:00+09:00",
  "title": "Winter tour presale",
  "return_urls": ["https://shop.example.com/"]
}
```

`open` lets people in at the rate, `paused` holds the line, and `closed` stops handing out numbers when you're sold out. Visitors who arrive before `starts_at` get a number and a countdown; the line starts moving at that time.

## Step 2: protect the page at the edge

On Cloudflare Workers, Next.js middleware, Deno or Bun, one call does the whole job:

```js
// middleware.js (Next.js)
import { NextResponse } from 'next/server';
import { protect } from '@hibira-io/waiting-room/server';

export async function middleware(request) {
  const blocked = await protect(request, {
    appKey: process.env.HIBIRA_KEY,
    secret: process.env.HIBIRA_SECRET,
    room: 'presale',
  });
  return blocked ?? NextResponse.next();
}

export const config = { matcher: ['/drop/:path*'] };
```

A visitor without a pass is sent to the waiting page. When it's their turn they come back with a pass, which `protect()` checks and keeps in an HttpOnly cookie. On Express, use `expressWaitingRoom()` from the same package.

## Step 3: Laravel

With [hibira-io/laravel](https://hibira.io/guides/laravel) 0.4 or later, which you may already use for broadcasting, create the room from the command line and protect your routes with one middleware:

```shell
composer require hibira-io/laravel:^0.4
php artisan hibira:waiting-room presale create --rate=300 --title="Winter presale"
```

```php
Route::middleware('hibira.waiting:presale')->group(function () {
    Route::get('/drop', [DropController::class, 'show']);
    Route::post('/drop/checkout', [DropController::class, 'checkout']);
});
```

A visitor without a pass is sent to the waiting page, in your app's language. The pass they bring back is checked with your app secret, with no API call, and kept in an HttpOnly cookie until it expires. A `POST` without a pass gets a `403`. In your controller, the pass is on the request:

```php
$pass = $request->attributes->get('hibira_pass');
$pass->number;   // queue number
$pass->ticketId; // record it at checkout: one order per number
```

Run the room from code, a queued job or the command line:

```php
use Hibira\Laravel\Facades\Hibira;

Hibira::waitingRoom('presale')->pause();
Hibira::waitingRoom('presale')->admit(500);
Hibira::waitingRoom('presale')->notice('Zone A is sold out');
Hibira::waitingRoom('presale')->close();
```

Set `HIBIRA_WAITING_ROOMS=false` in local development to skip the line.

## Step 4: run the line from Filament

Your team doesn't need another dashboard on sale day. With [hibira-io/filament](https://hibira.io/use-cases/filament) 0.5 or later, one line adds a Waiting rooms page to your panel:

```php
HibiraPlugin::make()->waitingRooms(
    authorize: fn ($user) => $user->is_admin,
)
```

The page shows each room's waiting count, how far the line has got and the rate, updated live. From it your team can pause and open the line, let people in now, post a notice, close the room when stock runs out, and create or edit rooms. Leave out `authorize` to let every panel user in.

## Step 5: your own waiting page

The hosted page fits most sites. To match your brand exactly, build the page yourself:

```js
import { joinWaitingRoom } from '@hibira-io/waiting-room';

joinWaitingRoom({
  key: 'APP_KEY',
  room: 'presale',
  onUpdate: ({ ahead, etaMinutes }) => render(ahead, etaMinutes),
  onTurn: (turn) => turn.redirect('https://shop.example.com/drop'),
  onClosed: () => showSoldOut(),
});
```

The client keeps the visitor's ticket in the browser, so a refresh keeps their place. If it can't open a connection, it checks the status every few seconds instead, and nobody is locked out.

## Step 6: in your app

A mobile or game client uses the same public endpoints as the hosted page: it joins, follows the room's cache channel, asks for a pass when its number comes up, and sends the pass with its API calls. Your API checks the pass with your app secret, with no call to Hibira; on Laravel, `WaitingRoom::verifyPass($pass, 'presale')` does it.

```http
POST /waitroom/APP_KEY/presale/join         -> {"ticket": "…", "number": 5121}
GET  /waitroom/APP_KEY/presale              -> {"admitted": 4980, "state": "open", …}
POST /waitroom/APP_KEY/presale/pass         {"ticket": "…"}  -> {"pass": "…"}
```

## Compared with Queue-it and Cloudflare Waiting Room

|  | Hibira | Queue-it | Cloudflare Waiting Room |
| --- | --- | --- | --- |
| How you start | Sign up and create a room yourself | Talk to sales; one published per-event offer | Business or Enterprise plan |
| Where your site runs | Any host, CDN or framework | Any host, with their connectors | Traffic proxied through Cloudflare |
| Scheduled start with a countdown | Yes | Yes | Enterprise |
| More than one room | Yes | Yes | Enterprise, with the Advanced add-on |
| Your own waiting page | Yes, with a small browser client | Yes | Enterprise custom templates |
| Run the line from your own admin | Yes, for example a Filament page | Their dashboard | Cloudflare dashboard or API |

Based on their public pricing and plan pages in November 2026. Plans change, so check with each vendor before you decide.

Full comparisons, with prices and when each one fits better: [Queue-it](https://hibira.io/compare/queue-it), [Cloudflare Waiting Room](https://hibira.io/compare/cloudflare-waiting-room), [CrowdHandler](https://hibira.io/compare/crowdhandler) and [Queue-Fair](https://hibira.io/compare/queue-fair).

## Tips

- Protect the pages that actually run out: the product page, the cart and checkout. Leave the home page and your images outside the room.
- Check the pass on the request that matters, not only in the browser. A visitor can edit any script on their own page; they can't forge a signature.
- If each queue number may buy once, record the pass's ticket ID at checkout and refuse a second order with it.
- Open the room 15 to 30 minutes before the sale and set `starts_at`. Early visitors wait on the countdown instead of hitting refresh on your site.
- Start with a rate your checkout can comfortably serve, then raise it while you watch your error rates. Pausing is always one click away.
