# Edge cache

Pages that are the same for every visitor are kept at bunny.net, so the web container can scale to zero between visits.


The web container of an environment sleeps when nobody needs it: Scaleway scales it to zero after 15 minutes without a request, and every request that reaches it starts those 15 minutes again. Behind an edge that keeps nothing, every visitor and every crawler reaches the container, and a public site hardly ever sleeps. So bunny.net keeps the pages that are the same for everybody, and answers the next request for them itself.

## What decides

The application decides, with `Cache-Control`. A pull zone Brewless manages follows what the application says and keeps nothing it was not allowed to keep. It keeps a page per hostname: one pull zone answers for your domain, every subdomain and the test address, and the page of `example.com/` is never what `shop.example.com/` gets. Every deploy checks that, and turning it on for a pull zone made earlier empties that pull zone. It also has Origin Shield: bunny.net has a cache in every point of presence, and without one cache in front of the origin, a crawler that comes in through another point of presence reaches the container again. With it, a page reaches the container once per cache time. Origin Shield costs nothing extra at bunny.net. `brewless/laravel` (from version 0.17) gives an answer `Cache-Control: public, max-age=0, s-maxage=3600` (from 0.18; 300 before) when it cannot depend on who asked:

- the request is a GET or HEAD without cookies and without `Authorization`, and not for `/_ops/*`;
- the answer is a 200, 301, 308, 404 or 410, sets no cookie and started no session;
- the application did not set `Cache-Control` itself;
- it varies on nothing but `Accept-Encoding`;
- the page did not ask who is signed in (`@auth`, `auth()->user()`): the edge would give a guest's page to a signed-in visitor too (from 0.18.1).

`s-maxage` is for the edge, `max-age=0` keeps the page out of the browser. Every release empties the edge, so after a deploy nobody gets a page of the release before, and between deploys a page is at most `s-maxage` seconds old.

## How long

`edge.cache` in `brewless.yml` says how long, per environment: `0`, a number of seconds, or a time such as `30m`, `1h` or `1d`, at most a week. A deploy gives it to the application as `BREWLESS_EDGE_CACHE`, in seconds, together with the plain variables of the file. Without it, the application keeps what it has: the default of its package, or a `BREWLESS_EDGE_CACHE` you set yourself. Pages that only change with a release can stay a day, which lets the container sleep far more often than a few minutes would:

```yaml brewless.yml
environments:
  production:
    edge:
      cache: 1d
```

`edge.cache: 0` keeps nothing at the edge. Next to `edge.cache`, the file may not name `BREWLESS_EDGE_CACHE` under `variables`. Like a variable of the file, the value is changed in the file, not on the Variables page; leaving `edge.cache` out later leaves the value as it was. The variables of the environment have to be kept through Brewless (`brewless env:push` once).

## Pages without a session

A route in Laravel's `web` group starts a session, and the session sets a cookie on every answer: such a page is never kept.

From `brewless/laravel` 0.18, on Laravel 13, that is taken care of: a guest without any cookie gets no session cookie and no `XSRF-TOKEN`, and nothing is stored, when the HTML page put nothing in the session and does not show the CSRF token. A page with a form, a flash message or anything else in the session keeps it, and so does every visitor who sent a cookie. `BREWLESS_STATELESS_GUESTS=false` turns it off, for instance for an app shell whose script relies on the `XSRF-TOKEN` cookie; `true` turns it on on Laravel 12, whose CSRF check needs that token.

To say it yourself, or on Laravel 12, take the session off public routes:

```php routes/web.php
Route::withoutSession(function () {
    Route::view('/', 'home');
    Route::get('/blog/{post}', [BlogController::class, 'show']);
});
```

`->withoutSession()` does the same for one route. Before 0.18, take `StartSession`, `ShareErrorsFromSession` and the CSRF middleware (`PreventRequestForgery`, or `ValidateCsrfToken` on Laravel 12) off with `withoutMiddleware()`. A page with a form of its own keeps the session. A form a script sends with `fetch()` passes Laravel 13's CSRF check on `Sec-Fetch-Site: same-origin` without a token.

> [!WARNING]
> The edge keeps one answer per hostname and address and does not look at `Vary`. A page that answers differently on something else than its address (the visitor's IP address, a header of your own, `Accept`, a cookie it reads without a session) must set `Cache-Control` itself: `->middleware('cache.headers:private')`, or `no-store`. Give another version of a page, such as its Markdown, an address of its own.

## Markdown for agents

An agent that asks for a page with `Accept: text/markdown` would get the HTML the edge keeps for browsers: the edge keeps one answer per address, and bunny.net cannot skip a page it already keeps. So every pull zone Brewless manages answers a GET or HEAD whose `Accept` names `text/markdown` with a `302` to the same address with `brewless-markdown=1` added to the query string. Nothing under that address is ever kept at the edge (the rule `brewless: markdown address never kept`), so the request reaches your application, with its `Accept` header, and the application answers as it likes. A browser that follows such a link gets your HTML, and that is not kept either, so it never reaches the next agent. A browser never asks for `text/markdown`, so nothing changes for it. A new pull zone gets both rules when it is made, and every deploy adds them to a pull zone made earlier, or puts one back when it was changed or switched off.

The agent has to follow the redirect; `curl` does with `-L`:

```bash
curl -sL -H 'Accept: text/markdown' https://example.com/pricing
```

The redirect leaves the query string the request had as it was, and your application can ignore `brewless-markdown`. An address of its own for the Markdown, such as `?format=markdown`, needs no redirect.

## When the edge cannot keep the homepage

Right after a deploy, once the edge was emptied, Brewless asks the homepage of the environment's own domain through the edge, the way a first visitor does: without a cookie. It follows one redirect to another page of the same domain. When the edge cannot keep that page, the deploy says so after the release is live, and why:

- the page sets a cookie, mostly the session: every visit then reaches the container;
- its `Cache-Control` does not let a shared cache keep it;
- asked again, up to three times in all, bunny.net still did not answer it from its cache.

It is said, never a reason to call the release failed. `brewless plan` asks the homepage once and shows the same as a line `edge.homepage`, without the last check. Nothing is asked without a domain of your own, without bunny.net in front, or with `edge.cache: 0`, and never on a schedule: every request is a visit that wakes the container. The requests carry the user agent `Brewless edge check`.

## Warmed after every release

Every release empties the edge, and bunny.net has a cache in every point of presence with Origin Shield in front of your container. Without more, each page reaches the container once more after a deploy before the shield has it, and crawlers that walk your site page by page keep the container awake for hours. So after a deploy and a rollback, once the release is live and the edge was emptied, Brewless asks every page once through the edge: the container is awake anyway at that moment.

The pages are those of the sitemaps of every own domain of the environment that is not a wildcard: the `Sitemap:` lines of its `robots.txt`, or else its `/sitemap.xml`, following one redirect on the same domain and a sitemap index one level down. A sitemap or page on a subdomain that a wildcard domain answers for counts as your own too, so a `robots.txt` that names the sitemap of your docs on `docs.` warms them as well. A domain without a sitemap gives only its `/`. Only `https` addresses on the environment's own domains are asked, each once, without a cookie, eight at a time, at most 500, within about two and a half minutes. Without a sitemap only `/` is asked.

The edge empties the files of your pages too, and your web server serves those without your application: the first visitor after a deploy, whose page the edge already has, would still wake the container for its stylesheet. So the files the warmed pages name on your own domains are asked as well: stylesheets, scripts, fonts, icons, images and videos with their `poster`, the image of a share preview (`og:image`) that crawlers such as Googlebot-Image fetch on their own, and the fonts and images those stylesheets name, at most 500, within the same time. Only pages and stylesheets are read; an image or a video passes through the edge without being kept in memory. The hashed build output the pull zone serves from your storage zone never reaches the container and is left alone. The step says how many: `Edge warmed  216 pages and 31 files`, with what failed or was left out. It is never a reason to call the release failed. The requests carry the user agent `Brewless edge warm-up`.

`edge.warm: false` turns it off for an environment:

```yaml brewless.yml
environments:
  production:
    edge:
      warm: false
```

Nothing is warmed without a domain of your own, without bunny.net in front, or with `edge.cache: 0`.

## Checking it

Ask for a page twice. The second answer comes from the edge:

```bash
curl -sI https://example.com/pricing | grep -i -E 'cache-control|cdn-cache'
curl -sI https://example.com/pricing | grep -i -E 'cache-control|cdn-cache'
```

`cdn-cache: HIT` on the second. Still `MISS`, with `cache-control: no-cache, private`: the page set a cookie or a session, or the request carried a cookie. How long the container ran and slept is on the environment's page, under serverless usage; see [Scaling](/running/scaling).

## What the edge did

The environment's overview shows, under **At the edge**, what bunny.net answered in the time range you chose above the traffic chart:

- **Requests**: every request bunny.net answered, and how many of them it passed on to your container.
- **From the cache**: the share it answered itself, and how long your container took on average for the rest.
- **Data sent**: what bunny.net sent to visitors, and what it fetched from your container for that.
- **Errors**: how many answers were client errors (4xx) and server errors (5xx).

Below the figures, a chart puts the requests at the edge per hour next to the ones passed on to your container: the gap between the two lines is what the cache saved your container. Under it, a map with a dot for every place bunny.net served data from, as large as its share; point at a dot, or move to it with the keyboard, for its figures. The map opens around your visitors and can show the whole world, and the places are ranked beside it. It is drawn in the console itself, so no map service sees who looks at it.

The figures are read live from bunny.net when you open the page and kept for under a minute; Brewless stores none of them. bunny.net counts per hour, so the hour the range starts in counts in full, and **Last hour** shows the figures without a chart: choose 24 hours or 7 days to see the hours. The places cover the whole days bunny.net reports on.
