How HTTP Works

Response header · non-standard

< Refresh: 5; url=/next

Refresh Header

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

Reviewed 6 min readbeginner2 sourcesTry itMarkdown
Direction
Response
Category
Redirection
Spec
HTML Standard (meta refresh)
Status
Non-standard: Not defined in the HTTP RFCs
On this page

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 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/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. 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

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:

<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:

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 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:

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:

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. 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
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 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, like any other 200. The header neither blocks caching nor makes the response fresh, so set Cache-Control for what the response really needs.

MechanismWhere navigation is controlled
303 plus LocationHTTP redirect handling; retrieves another resource
Refresh: 5; url=/nextDocument loading and declarative refresh processing
Meta refreshThe 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:

<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. 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:

<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() 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

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.

Frequently asked questions

What is the Refresh header?

Refresh tells browsers to reload the page or redirect after a delay. Equivalent to meta http-equiv refresh. Example: Refresh: 5; url=https://example.com

Should I use Refresh for redirects?

For a moved URL, use an HTTP redirect status with Location. Refresh is defined through HTML document loading and schedules a navigation after a delay.

What are the problems with Refresh?

Timed navigation can remove content before a user finishes reading. W3C documents timed meta redirects as an accessibility failure. Refresh is HTML loading behavior rather than an HTTP redirect.

When might Refresh be acceptable?

A status document can request a reload, but users may need control over the timing. Use a manual refresh or application-controlled update when a full timed navigation is unnecessary.

Sources

  1. MDN Web Docs: Refreshdeveloper.mozilla.org
  2. HTML Standard: Refreshhtml.spec.whatwg.org

Keep going

Browse /search