# Accept-Ranges Header

> Learn how the Accept-Ranges header tells clients whether your server supports partial content requests (byte ranges) for efficient downloads and streaming.

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

> **TL;DR:** `Accept-Ranges: bytes` tells clients they can request byte ranges, which is what makes resumable downloads and video seeking possible. It's an advertisement: check for `206` on the actual response, feel free to send Range when the header is absent, and remember that adding the header doesn't implement range handling.

## What is Accept-Ranges?

Accept-Ranges lists the range units a server supports for this resource. In practice the only unit is `bytes`. [RFC 9110 §14.3](https://www.rfc-editor.org/rfc/rfc9110#section-14.3) lets clients send Range without ever seeing Accept-Ranges, and points out that seeing it once is no promise about later responses.

## Syntax

Headers and outputs on this page are examples.

```http
Accept-Ranges: bytes
Accept-Ranges: none
```

`bytes` advertises byte ranges. `none` asks clients not to try range requests. Leaving the header out is not a refusal, and sending `none` doesn't change how the server actually handles Range.

## How Accept-Ranges Works

A downloader can use the advertisement to decide whether to show a resume button. It still has to check the response to the actual [Range](https://howhttpworks.com/headers/range) request: `206` is partial content, `200` usually means Range was ignored, and `416` means the requested interval doesn't exist.

Support varies by resource. A static file and a generated report on the same host are often served by different handlers with different range support. Treat each response's Accept-Ranges as describing that resource, not the whole host.

## Common Examples

A ten-byte static file might come back with:

```http
Accept-Ranges: bytes
Content-Length: 10
ETag: "sample-v1"
```

To resume it, save the ETag and send it back in [If-Range](https://howhttpworks.com/headers/if-range). Accept-Ranges says nothing about which version you have, so the validator is what protects you from stitching together two different files.

## Server Implementation

For nginx static files, range handling is controlled by [`max_ranges`](https://nginx.org/en/docs/http/ngx_http_core_module.html#max_ranges). Its default has no range-count limit. To disable byte-range support for a location:

```nginx
location /downloads/ {
    max_ranges 0;
}
```

Put it inside your existing server block. Adding an `Accept-Ranges: none` header instead would leave the range filter running. The reverse holds too: adding `Accept-Ranges: bytes` to a dynamic response won't slice its body or produce `206` and Content-Range.

Express supports byte ranges through its file-serving methods. With the deterministic fixture from the [Range page](https://howhttpworks.com/headers/range#server-implementation), add a download route to an existing app:

```javascript
app.get('/download', (req, res, next) => {
  res.download('sample.txt', 'sample.txt', {
    root: '/tmp/http-range-demo',
    acceptRanges: true,
  }, (error) => {
    if (error) next(error);
  });
});
```

[`res.download()`](https://expressjs.com/en/5x/api/response/#res.download) uses file-transfer handling and offers `acceptRanges`, which defaults to true. Set it to false to turn off range support for that transfer. [`res.attachment()`](https://expressjs.com/en/5x/api/response/#res.attachment) is a different thing: it only sets Content-Disposition and, with a filename, Content-Type. Call it before `res.send()` and you get a download prompt, but no range handling. A browser download prompt and a resumable response are separate features.

Django's [`FileResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#fileresponse-objects) is useful for streaming a binary file and setting download metadata:

```python
from django.http import FileResponse

def download(request):
    return FileResponse(open('/tmp/http-range-demo/sample.txt', 'rb'),
                        as_attachment=True, filename='sample.txt')
```

Django closes the file for you, so open it without a `with` block; one that ends before the response is sent would close the file too early. FileResponse sets length, type, and disposition when it can work them out, but its [implementation](https://github.com/django/django/blob/stable/5.2.x/django/http/response.py) doesn't parse Range. Adding `Accept-Ranges: bytes` to this view would promise something it can't do. If you need resume support, serve the file through a range-capable server or write the range handling yourself.

## Getting Accept-Ranges right

A valid single-part partial response needs the right bytes and a matching [Content-Range](https://howhttpworks.com/headers/content-range), so test the body length, not just the headers. A wrong advertisement may lead a downloader to try resuming, which is why downloaders have to handle a full `200` response safely anyway.

Compression and ranges can coexist: ranges count bytes of the encoded representation. What breaks is mixing the two, such as resuming against a gzip response using offsets from the decompressed file on disk. That corrupts the result.

For generated exports, ask whether the handler can produce the exact same bytes again and seek within them. A one-pass stream can't serve a later request starting at an arbitrary offset, whatever the header says.

## Testing Accept-Ranges

Use the [nginx fixture](https://howhttpworks.com/headers/range#server-implementation) to test a real GET:

```bash
curl -sS -D - -o /dev/null http://localhost:8080/sample.txt
curl -sS -D - --range 0-4 http://localhost:8080/sample.txt
```

Compare the advertisement with the actual status, Content-Range, and five-byte body. Use GET for this, since servers must ignore Range on HEAD.

Cross-origin JavaScript can read Accept-Ranges only if the response exposes it through CORS. On a response that already has CORS enabled, add:

```http
Access-Control-Expose-Headers: Accept-Ranges, Content-Range, ETag
```

With the Express route above in place, test both the download metadata and the byte transfer:

```bash
curl -sS -D - --range 0-4 http://localhost:3000/download
```

Check Content-Disposition along with the 206, Content-Range, and the `01234` body. Then set `acceptRanges: false`, repeat the request, and confirm you now get the full response. Test the exact URL a downloader receives, redirects included: the headers on a redirect describe the redirect, not the file handler behind it.

This browser probe checks the response itself rather than the advertisement:

```javascript
const response = await fetch('/download', {
  headers: { Range: 'bytes=0-4' },
});
const bytes = new Uint8Array(await response.arrayBuffer());
console.table({
  advertised: response.headers.get('accept-ranges'),
  status: response.status,
  interval: response.headers.get('content-range'),
  receivedBytes: bytes.length,
});
```

Run it on the Express origin that serves the route. Five bytes back means success only if the status is 206 and the interval matches. A server can ignore Range even after advertising support, and a file that happens to be five bytes long would also return five bytes. Check all three together before you let a download UI append bytes.

## Related Headers

- [Range](https://howhttpworks.com/headers/range)
- [Content-Range](https://howhttpworks.com/headers/content-range)
- [If-Range](https://howhttpworks.com/headers/if-range)
