# Handling peaks

Prepare an environment for a burst of requests, such as a ticket sale or a vote: replicas, PHP threads, the database, open connections.


A peak is a burst of requests that comes at a known moment: a ticket sale that opens at 10:00, a vote during a broadcast, a mailing. An environment that sleeps does not catch one cold. Prepare it before the moment, and give it back afterwards.

## How much an environment can answer

Every replica of the web container runs FrankenPHP with two PHP threads per vCPU, and each thread answers one request at a time. Roughly:

```text
requests a second ≈ replicas × threads per replica ÷ seconds per request
```

20 replicas of 1 vCPU have 40 threads. At 100 ms a request that is about 400 requests a second; at 1 s a request, 40. A request that waits on the database holds its thread while it waits, so the time a request takes is what decides it, more than the processor.

What a test environment did with a vote endpoint that stores each vote in a Serverless SQL Database before it answers (November 2026):

| Set-up | Offered | Answered | Median |
| --- | --- | --- | --- |
| 20 replicas of 1 vCPU, a new database connection per request | 500/s | 184/s | 6.3 s |
| The same, connections kept open | 500/s | 335/s | 67 ms |
| 20 replicas of 2 vCPU, connections kept open | 500/s | 492/s | 99.9% accepted |
| From zero replicas, nothing prepared | 1,000/s | 7% in the first 10 s | |

The database itself stayed around 1 vCPU: the threads were the limit, not PostgreSQL.

## Before the peak

1. **Start replicas in advance.** A replica that starts during the peak makes the first requests wait for it. Raise `web.min` in `brewless.yml` and deploy, or set it in the console or with `brewless scale`, some minutes before the moment; see [Scaling](/running/scaling). Raise `web.max` too when the formula above says you need more.
2. **Give the replicas more processor.** `web.cpu: 2000` gives each replica 2 vCPU and so four threads instead of two; see [brewless.yml](/reference/brewless-yml).
3. **Boost the database.** A Serverless SQL Database scales up as the load grows, from a minimum that is low unless you raised it. Plan a boost with time to warm up, so that a restart for a large raise happens before the peak and not during it: `brewless db:boost production --min=4 --max=8 --for=2h --at=09:30`. See [Database capacity](/running/database-capacity).
4. **Keep database connections open.** Without it, every request opens a new TLS connection to the database first. See below.

Afterwards, put `web.min` back and let the boost end, or the replicas and the database keep costing what they cost during the peak.

## Connections that stay open

With `brewless/laravel` 1.5 or later, the plain variable `BREWLESS_DB_PERSISTENT` keeps the PostgreSQL and MySQL connections open between requests:

```yaml brewless.yml
environments:
  production:
    variables:
      BREWLESS_DB_PERSISTENT: 'true'
```

A variable reaches the containers with the next deploy. Each PHP thread then keeps one connection, also while the container is quiet, so the number of connections is replicas × threads: 20 replicas of 2 vCPU hold 80. Keep that under what the database accepts. Scaleway gives a Serverless SQL Database more connections as it gets more vCPU, up to 1,000; an open connection does not keep it awake.

What the package takes care of:

- A connection that an earlier request left in the middle of a transaction is rolled back before the next request uses it.
- After the database restarted, for a boost, a failover or maintenance, the first query on an old connection fails as a lost connection and is tried again on a new one, for up to `BREWLESS_DB_RETRY_SECONDS`.
- A setting of the session (`SET …`) stays with the connection and reaches the next request on that thread. Do not rely on one; on Serverless SQL sessions are shared anyway. See [Databases](/running/databases).

## A queue in between

Answering at once and storing the vote from a queued job feels faster, but the jobs still have to be stored by the same database, by fewer workers. In the test above it only paid off with more workers (`worker.max`) or with jobs that store many votes in one insert. Without that the queue grows during the peak and empties long after it.
