# Link Header

> Learn how the Link header provides resource hints and enables preloading of CSS, fonts, and scripts to improve page load performance and user experience.

Source: https://howhttpworks.com/headers/link
Last reviewed: 2026-10-05

> **TL;DR:** `Link` is the HTTP-header version of HTML's `<link>` element. Send `Link: </app.css>; rel=preload; as=style` to start a stylesheet download early (you still need the stylesheet link in your HTML to apply it), or `rel="next"` to point API clients at the next page.

## What is Link?

[RFC 8288](https://www.rfc-editor.org/rfc/rfc8288#section-3) defines how to write typed links as HTTP fields. Each link is a target URL in angle brackets followed by parameters such as `rel`. What a browser does with it depends on the relation type; a URL by itself does nothing.

For example, `rel="next"` tells an API client that the target is the next page of the collection. Note the brackets: `Link: /items?page=2` without them isn't valid syntax.

## How Link Works

Headers and URLs on this page are examples.

```http
Link: </app.css>; rel=preload; as=style
```

The browser can start fetching `/app.css` before it finds the reference in the document. The HTML still has to use it, for example with `<link rel="stylesheet" href="/app.css">`. [MDN explains preload consumption](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload).

## Syntax

```http
Link: </app.css>; rel=preload; as=style, </items?page=2>; rel="next"
```

Angle brackets wrap each target and commas separate links. Relative targets resolve against the response URL. Use a proper parser rather than splitting on commas, since quoted parameter values can contain commas too.

## Resource Hints

`preload` fetches a resource this document needs; `preconnect` opens a connection to an origin ahead of time. Both are hints. The browser decides whether to act and at what priority, and you'll only know it helped by measuring.

Not every HTML `rel` works in a header. [MDN's Link reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Link) lists `preload` and `preconnect` as the relations browsers reliably support there. Test anything else in the browsers you care about.

A connection hint targets an origin rather than a particular CSS file:

```http
Link: <https://static.example.com>; rel=preconnect
```

Point it at an origin the page really contacts; a warm connection nobody uses is wasted work. `as` belongs on preloads, which name a specific resource, not on connection hints.

## Resource Types (as parameter)

Use `as=style`, `as=script`, `as=image`, or `as=font` to match the consuming request's destination. For a font, match the CORS mode too:

```http
Link: </fonts/site.woff2>; rel=preload; as=font; type="font/woff2"; crossorigin
```

Font preloads need `crossorigin` even when the font is same-origin, as [MDN's preload guide](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload#cors-enabled_fetches) explains.

## Pagination

An API can advertise another page:

```http
Link: </items?page=2>; rel="next", </items?page=10>; rel="last"
```

The API client decides whether to follow these. A browser won't change pages because of them.

Build the links from the data you actually have. This route goes in an existing Express app:

```javascript
const items = ['alpha', 'beta', 'gamma']
app.get('/items', (req, res) => {
  const page = Number(req.query.page ?? 1)
  if (!Number.isInteger(page) || page < 1 || page > items.length) {
    return res.status(400).json({ error: 'Invalid page' })
  }
  const links = []
  if (page > 1) links.push(`</items?page=${page - 1}>; rel="prev"`)
  if (page < items.length) links.push(`</items?page=${page + 1}>; rel="next"`)
  if (links.length) res.set('Link', links.join(', '))
  res.json({ items: [items[page - 1]] })
})
```

The fixture serves one item per page, so page three has no `next` link. Clients should follow whatever links the server sends instead of assuming every collection has a fixed last page.

## Multiple Links

Send several `Link` fields or one comma-separated list; they mean the same thing. Check the final response, because the app server and the reverse proxy may each be adding values.

In Express, use `append` if another handler may already have set links:

```javascript
res.append('Link', '</app.css>; rel=preload; as=style')
res.append('Link', '</fonts/site.woff2>; rel=preload; as=font; crossorigin')
```

A later `res.set('Link', ...)` replaces those values. [Express documents that distinction](https://expressjs.com/en/5x/api/response/#res.append); check middleware order when only one of your hints reaches the client.

## Real-World Scenarios

HTML responses preload stylesheets. API responses carry pagination. Non-HTML documents such as PDFs can name their canonical URL with `rel="canonical"`; [Google documents canonical HTTP links](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls#rel-canonical-header).

For a PDF that duplicates an HTML publication, the response can advertise the preferred URL:

```http
Link: <https://example.com/publications/report>; rel="canonical"
```

Point it at the URL of the equivalent publication, not a generic landing page. Canonical is a signal for search engines; the person reading the PDF never gets redirected.

## Server Implementation

Add this inside the nginx location that serves the HTML needing `/app.css`:

```nginx
add_header Link '</app.css>; rel=preload; as=style';
```

[`add_header`](https://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header) adds it only to common success and redirect statuses (200, 201, 204, 206, 301–304, 307, 308); add `always` to include other status codes. Keep the hint scoped to documents that actually use the resource.

With Apache `mod_headers` loaded, scope a preload to HTML documents that need it:

```apache
<FilesMatch "\.html$">
    Header add Link "</app.css>; rel=preload; as=style"
</FilesMatch>
```

[`Header add`](https://httpd.apache.org/docs/2.4/mod/mod_headers.html#header) adds a field rather than replacing an existing one. If only some pages use this stylesheet, narrow the match.

For Caddy serving a static site where the root page needs that CSS:

```caddyfile
example.com {
    root * /srv/www
    header / +Link "</app.css>; rel=preload; as=style"
    file_server
}
```

Caddy's [`+` prefix](https://caddyserver.com/docs/caddyfile/directives/header) appends a header value. The `/` matcher limits the hint to the root request, so asset responses don't get it.

## HTML Equivalent

```html
<link rel="preload" href="/app.css" as="style" />
<link rel="stylesheet" href="/app.css" />
```

They do different jobs: preload fetches, stylesheet applies. A preload header can replace the first line, but you always need the second.

## Getting Link right

Match the preload's URL, `as` destination, and credentials mode to the request that will use it. Get any of them wrong and you get an unused preload, a second fetch, or both. Preload only what the current page needs, not every route's assets.

A common slip: preloading `/app.css` while the page links `/app.css?v=2`. Those are different URLs, so the preload goes to waste. Use exactly the same URL, version query included, and recheck whenever asset versioning changes while a server-level header still points at the old file.

## Performance Impact

Measure the waterfall before and after adding a hint. A `103 Early Hints` response can deliver `Link` before the final response headers are ready; see [MDN's 103 reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/103). How much time you save depends entirely on your page.

In DevTools, compare when the hinted request starts against the document request and the element that later uses it. Keep the cache state the same between runs. A preload that starts earlier but goes unused is a cost, not a win. Also check whether it's stealing bandwidth from other resources the page needs before you keep it.

## Common Issues

If a hinted resource is fetched twice, compare URL and CORS mode with the consuming request. If a stylesheet is fetched but never applied, check that a stylesheet link still exists in the HTML.

Use a GET to inspect the field actually sent for a document:

```bash
curl -sS -D - -o /dev/null https://example.com/
```

If a font is requested twice, compare the preload's `crossorigin` setting with the font request. If the header is missing, check the response status and where the server directive applies. For a cross-origin API, JavaScript can read `Link` only if the response exposes it, for example with `Access-Control-Expose-Headers: Link`.

## Browser Support

A browser that supports the `Link` header still might not support every relation in it. In **Network → document → Response Headers**, first confirm the server sent the hint, then find the request for its target and check the initiator. Compatibility tables for HTML `<link>` don't tell you how the header behaves, so test the exact header in the browsers you support.

## Related Headers

- [Content-Type](https://howhttpworks.com/headers/content-type)
- [Cache-Control](https://howhttpworks.com/headers/cache-control)
