# 103 Early Hints

> Let browsers start loading stylesheets, fonts and the main image while your origin builds the page, with links the edge learns from your HTML.

With Early Hints on, the edge answers an HTML navigation with a `103 Early Hints` response **before** it asks your origin for the page. The `103` carries `Link` headers (`rel=preload`, `preconnect`, `modulepreload`) for the page's critical resources, so the browser downloads them while the origin is still generating the HTML. It improves LCP on pages that are slow at the origin and doesn't change the final response.

Early Hints are independent of the cache: you can use them with the cache on or off. They are **off by default**.

## When the edge sends a 103

Only when the site has Early Hints on and the request:

- is a `GET` navigation (`Accept` contains `text/html`), without `Range`, over HTTP/1.1 or HTTP/2;
- **goes to the origin**: a cache `MISS` or `EXPIRED`, a `BYPASS` or `DYNAMIC` request, or a site without the cache;
- is for a page the edge node has already learned links for.

Never on a cache `HIT` or `UPDATING` (the answer is already immediate), on a challenge, the access gate, a WAF block, an edge redirect or a `HEAD` request.

## How links are learned

Each edge node learns links per host and path (without the query) from `200` `text/html` responses of your origin. The first visit to a page teaches the node; later visits get the hints. Learned links are kept in memory for an hour, renewed on every `200`.

| `mode` | Dashboard | Source |
|---|---|---|
| `origin_links` | Origin Link headers | The origin's `Link` headers with `rel=preload`, `preconnect` or `modulepreload`. |
| `html_head` | HTML head | The first 32 KB of the HTML: `<link rel=preload \| modulepreload \| preconnect \| stylesheet>` (a stylesheet is announced as `preload; as=style`) and `<img fetchpriority=high>` (as `preload; as=image`, with `imagesrcset`/`imagesizes`). |
| `both` (default) | Both | Both sources together. |

Only `https` links from the page's own origin or one of the **extra origins** are announced, without credentials in the URL; at most 10 links per page. Stylesheets and LCP images come first, then fonts and `preconnect`, then scripts.

> **Note: Privacy**
>
> The edge never learns from a request with `Authorization`. A request with a `Cookie` only teaches if the response is shareable: no `Set-Cookie`, no `Cache-Control: private` or `no-store`, no `Vary: Cookie` or `Vary: *`. So a `103` never announces resources from another user's session.


## Turn it on

**Dashboard**

In **Caching › Early Hints (103)**, turn on **Send Early Hints**, choose **Where to learn the links** and, if your assets come from a CDN or a font host, add them under **Extra origins**. **Save changes**.

**CLI**

```sh
openwaf-cloud ctl zones set example.com early_hints.enabled:=true --reason "103 Early Hints"
openwaf-cloud ctl zones set example.com early_hints.mode=html_head 'early_hints.extra_origins:=["https://cdn.example.com"]' --reason "CDN"
# Off for one name:
openwaf-cloud ctl zones set example.com --host api.example.com early_hints.enabled:=false --reason "API without HTML"
```

**API**

```json
{"early_hints": {"enabled": true, "mode": "both", "extra_origins": ["https://cdn.example.com"]}}
```

Sent with `PATCH /v1/orgs/{org}/zones/{zone}/edge`. The patch merges field by field (`{"early_hints": {"mode": "html_head"}}` changes only the mode); `extra_origins` is replaced as a whole and `"early_hints": null` restores the defaults.

**MCP**

`update_settings` with `{"zone": "example.com", "settings": {"early_hints": {"enabled": true}}, "reason": "103 Early Hints"}`; add `host` for one name.


## Settings

| Key | Values | Default |
|---|---|---|
| `early_hints.enabled` | `true`, `false` | `false` |
| `early_hints.mode` | `origin_links`, `html_head`, `both` | `both` |
| `early_hints.extra_origins` | Up to 10 origins `https://host` or `https://host:port`, no path | `[]` |

Extra origins are normalized (lower case, no trailing `/`, `:443` removed); a non-https origin, one with a path, a wildcard or a duplicate is rejected with `422`.

**Per name**, `hosts.<name>.early_hints` replaces the zone's setting **as a whole**. Fields you leave out are copied from the zone when the override is created, so `{"enabled": false}` only switches that name off; `"early_hints": null` inside the name inherits the zone again.

## Check it

Request a page twice like a browser; the second time the node has learned it:

```sh
curl -sv --http2 -o /dev/null https://www.example.com/ -H 'Accept: text/html' 2>&1 | grep -E '^< (HTTP|link)'
# < HTTP/2 103
# < link: </style.css>; rel=preload; as=style
# < HTTP/2 200
```

No `103`? The page came from the cache (`x-cache-status: HIT`; try a path that isn't cached), no shareable `200` HTML was seen yet, or the request reached a different edge node that hasn't learned the page. Learned links stay on each node and aren't shown in the dashboard.
