# Refresh Header

> Learn how the Refresh header instructs browsers to reload or redirect after a delay. Understand its use cases, limitations, and better alternatives.

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

> **TL;DR:** `Refresh: 5; url=/next` tells the browser to go to `/next` five seconds after the page loads, and `Refresh: 5` reloads the current page. It works like `<meta http-equiv="refresh">`, not like a real HTTP redirect. If a URL has moved, send a `301`/`308` (or another `3xx`) with `Location` instead.

## What is Refresh?

The `Refresh` response header uses the same declarative refresh machinery as `<meta http-equiv="refresh">`. The [HTML Standard](https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-http-equiv-refresh) defines how it behaves. RFC 9110 doesn't list it among HTTP's redirect mechanisms, because it isn't one.

## How Refresh Works

Here's a response that sends it (example headers, HTML body left out):

```http
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Refresh: 5; url=/next
```

The countdown is tied to the document loading, not to the moment the headers arrive. [MDN says the delay starts after the page has fully loaded](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Refresh). Even `Refresh: 0` goes through that document refresh processing, so it isn't the same as an HTTP redirect that happens before anything renders.

## Syntax

```http
Refresh: 30
Refresh: 5; url=/next
Refresh: 0; url=https://example.com/new-page
```

The first line reloads the page, the second goes to a relative URL, and the third to an absolute one. Give the delay in whole seconds; browsers ignore any fractional part. How long to wait is up to you.

## Common Examples

The HTML-only version goes in the document head:

```html
<meta http-equiv="refresh" content="5; url=/next" />
```

It's the same timed navigation, just declared in the markup instead of a header. Everything on this page about timing and accessibility applies to both.

## Real-World Scenarios

A status page that should keep reloading has to send the directive on every response. Each `Refresh` only schedules one load. If the next response leaves the header out, the reloading stops.

So a status page can show a generated timestamp and ask for another load. Keep each GET read-only, so reloading only fetches the status and never starts or repeats the job being monitored. If a POST submits the job, keep it on a separate URL from the status page.

Say `/status` returns `Refresh: 30`. Each new load can repeat the header while the job runs and drop it once the job is done, which ends the cycle. Base that on the job's real state: thirty seconds passing doesn't mean the job finished.

## Server Implementation

This nginx route goes inside a `server` block:

```nginx
location = /waiting {
    default_type text/html;
    add_header Refresh '5; url=/result';
    return 200 '<!doctype html><html lang="en"><head><title>Waiting</title></head><body><p>Opening the result page in five seconds.</p><a href="/result">Open result now</a></body></html>';
}
```

[`add_header`](https://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header) puts the field on this `200` response. `/result` is your application's job, and nothing here checks whether a background task is finished.

In an existing Express app, this route serves a timestamped status page:

```javascript
app.get('/status', (req, res) => {
  res.set('Refresh', '30')
  res.set('Cache-Control', 'no-store')
  res.type('html').send(`<!doctype html>
    <html lang="en"><head><title>Status</title></head><body>
    <p>Generated at ${new Date().toISOString()}</p>
    <a href="/status">Refresh now</a></body></html>`)
})
```

`Cache-Control: no-store` is what keeps this response out of caches; `Refresh` has no effect on caching. The example always schedules another load. In a real app, check the job's state and leave the header off once it's complete.

A Django view sets the field on the response it returns:

```python
from django.http import HttpResponse

def waiting(request):
    response = HttpResponse(
        '<p>Opening the result page in five seconds.</p>'
        '<a href="/result">Open result now</a>',
        content_type='text/html; charset=utf-8',
    )
    response['Refresh'] = '5; url=/result'
    response['Cache-Control'] = 'no-store'
    return response
```

See [Django's docs on setting header fields](https://docs.djangoproject.com/en/6.0/ref/request-response/#setting-header-fields). You'll need a `/result` route of your own. Like the nginx version, this view just schedules a navigation; it doesn't wait for any background result.

In PHP, send headers before any output:

```php
<?php
header('Refresh: 30');
header('Cache-Control: no-store');
header('Content-Type: text/html; charset=utf-8');
echo '<!doctype html><html lang="en"><head><title>Status</title></head>';
echo '<body><p>Status checked.</p><a href="/status">Refresh now</a></body></html>';
```

[PHP's `header()` documentation](https://www.php.net/manual/en/function.header.php) requires headers to go out before any output. Even stray whitespace before the opening `<?php` tag can stop these calls from working, unless output buffering happens to mask it.

## Getting Refresh right

For a URL that has moved, send the appropriate `301`, `302`, `303`, `307` or `308` with `Location`. For a long-running operation, a timer is the wrong signal. Show the real status and let people move on once the result exists.

## Refresh vs Other Redirect Methods

A redirect with `Location` happens at the HTTP level. `Refresh` and meta refresh happen while the document loads. JavaScript navigation is a third mechanism, with a timer your script controls.

A `200` with `Refresh` is cacheable under the normal [HTTP caching rules](https://www.rfc-editor.org/rfc/rfc9111#section-3), like any other `200`. The header neither blocks caching nor makes the response fresh, so set `Cache-Control` for what the response really needs.

| Mechanism | Where navigation is controlled |
| --- | --- |
| `303` plus `Location` | HTTP redirect handling; retrieves another resource |
| `Refresh: 5; url=/next` | Document loading and declarative refresh processing |
| Meta refresh | The document's declarative refresh processing |
| `setTimeout(() => location.assign('/next'), 5000)` | A JavaScript timer owned by the page |

Only the last row gives your code a timer handle it can pass to `clearTimeout()`. Once document processing has scheduled a refresh, removing the meta element or clearing some other timer won't reliably cancel it.

## Common Use Cases

Before adding auto-reload to a monitoring page, ask whether you need a full navigation at all. A reload replaces the whole document to update what might be one number. If people need control over the timing, give them a manual refresh.

A manual reload needs nothing more than an ordinary link:

```html
<p>The export is being prepared.</p>
<a href="/status">Check export status</a>
```

Now the reader decides when the page gets replaced. For a dashboard that has to keep filters, focus or unsaved text, fetch the new values and update just those DOM elements, so the rest of the document stays put.

## Accessibility Considerations

Five seconds isn't necessarily enough time to read a page, and [W3C lists timed meta redirects as a WCAG failure](https://www.w3.org/WAI/WCAG21/Techniques/failures/F40.html). A "go now" link lets people leave early, but it doesn't let them stay longer. And a JavaScript `clearTimeout()` call has no power over a refresh the header scheduled.

If you need a delayed navigation that people can cancel, run the timer in JavaScript and drop both the header and the meta tag:

```html
<p id="notice">Opening the result in five seconds.</p>
<button id="stay" type="button">Stay on this page</button>
<a href="/result">Open result now</a>
<script>
  const timer = setTimeout(() => location.assign('/result'), 5000)
  document.querySelector('#stay').addEventListener('click', () => {
    clearTimeout(timer)
    document.querySelector('#notice').textContent = 'Automatic navigation stopped.'
  })
</script>
```

[`clearTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/clearTimeout) cancels this script's own timer. The button makes this one behavior controllable; check the rest of the page against accessibility requirements separately.

## Testing Refresh Header

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

curl shows you the header but never acts on it. `curl -L` follows HTTP redirects, and this isn't one. To see the actual behavior, load the route in a browser with **Network → Preserve log** turned on.

Fetching `/waiting` from JavaScript just retrieves the response; the current page doesn't navigate. So finding `Refresh` in `response.headers` doesn't tell you the page will redirect when loaded as a document.

In the Network log, you're looking for the first `200`, then a second document request once the page has loaded and the delay has passed. Check both the response headers and the document's `<head>`, since middleware might add the header while a template separately includes a meta refresh. A redirect chain can also land on a page that schedules a refresh, so keep the log from the very first URL instead of looking only at the last request.

## Related Headers

- [Location](https://howhttpworks.com/headers/location)
- [Retry-After](https://howhttpworks.com/headers/retry-after)
- [Content-Type](https://howhttpworks.com/headers/content-type)
- [Cache-Control](https://howhttpworks.com/headers/cache-control)
