# HTTP HEAD Method

> Learn how HTTP HEAD requests retrieve resource metadata (headers) without downloading the body. Useful for checking existence, size, and modification dates.

Source: https://howhttpworks.com/methods/head
Last reviewed: 2026-10-05

> **TL;DR:** HEAD is GET without the body. The server returns the same status and headers it would for GET, so you can check a file's size, type or ETag, or whether a URL exists, without downloading it. Servers may leave out headers they only know after generating the body, and HEAD often runs the same server code as GET, so it saves bandwidth more than server time. In curl, use `curl -I`, not `-X HEAD`.

## What is HEAD?

HEAD gives you the outcome of a GET without the content. Everything else about the request works the same way: authorization and content negotiation still apply, and an endpoint's access checks cover HEAD just as they cover GET.

[RFC 9110 §9.3.2](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.2) says the server should send the same headers it would for GET. It may leave out ones it only learns while generating the content, such as `Content-Length` or `Vary`.

## Key Characteristics

HEAD is safe and idempotent. Logging it is fine, and the headers change when the resource does. A HEAD response never has a body, even when it's an error. If it includes `Content-Length`, that's the size the matching GET body would be, not zero.

## How HEAD Works

Here's an exchange for a file whose GET body is the five bytes `hello` (examples on this page are trimmed):

```http
HEAD /hello.txt HTTP/1.1
Host: example.com

```

```http
HTTP/1.1 200 OK
Date: Mon, 05 Oct 2026 12:00:00 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 5
ETag: "hello-v1"

```

The response ends after the blank line. Some tools print a placeholder like `[No body content]`, but that's the tool talking; nothing like it appears on the wire.

## Real-World Examples

Use curl's HEAD mode:

```bash
curl --head 'https://example.com/'
```

Skip `curl -X HEAD`. It changes the method name but leaves curl expecting a body, so it hangs waiting for bytes that never come. `--head` (or `-I`) tells curl there's no body. See the [curl manual](https://curl.se/docs/manpage.html#-I).

To compare HEAD with GET, request the same representation both ways and save GET's headers separately from its content:

```bash
curl --head -H 'Accept-Encoding: identity' 'https://example.com/report.pdf'
curl -D ./get-headers.txt -o ./report.pdf \
  -H 'Accept-Encoding: identity' 'https://example.com/report.pdf'
wc -c ./report.pdf
```

When GET's headers include Content-Length, it should match the downloaded byte count. Send the same `Accept-Encoding`, `Accept` and authorization on both requests; a gzip HEAD and an uncompressed GET will report different sizes, and that's expected.

## When to Use HEAD

Reach for HEAD to check a download's size and type, or to test whether a link works. The answer is a snapshot, and the file may change before your GET. A missing `Content-Length` means the size is unknown, not zero. And if you're going to fetch the content right away anyway, a HEAD first just adds a round trip.

## HEAD vs GET

HEAD saves sending the body. On the server it often runs the same handler, database query or template as GET, so measure before you count on lower latency. For health checks, a 200 from HEAD only tells you the status; if you need to confirm the body is valid JSON with the expected result, use GET.

## Common Use Cases

A conditional HEAD returns 304 when the ETag still matches:

```bash
curl --head 'https://api.example.com/catalog' \
  -H 'If-None-Match: "catalog-v7"'
```

Use the validator from an earlier response. For cross-origin checks from the browser, CORS has to allow the request, and the server must send `Access-Control-Expose-Headers: ETag` for your JavaScript to read `ETag`.

### Checking download metadata in browser code

This checks a size limit before starting a download, treating a missing length as unknown:

```javascript
async function inspectDownload(url, maxBytes) {
  const response = await fetch(url, { method: 'HEAD' })
  if (!response.ok) throw new Error(`HEAD failed: ${response.status}`)
  const rawLength = response.headers.get('Content-Length')
  const bytes = rawLength === null ? null : Number(rawLength)
  if (bytes !== null && (!Number.isSafeInteger(bytes) || bytes < 0)) {
    throw new Error('Invalid Content-Length')
  }
  if (bytes !== null && bytes > maxBytes) throw new Error('Download exceeds limit')
  return { bytes, etag: response.headers.get('ETag') }
}
```

Use it for UI feedback; enforce real limits while downloading. The file may change before the GET, and with compression the bytes on the wire differ from the decoded size. To make sure you download the version you inspected, send the ETag in `If-Match` on the GET (if the server supports preconditions). On a 412, refresh the metadata instead of downloading a version you didn't check.

For a link checker, look at the status and the final URL. Fetch follows redirects by default, so a redirect to a login page comes back as a 200. Use curl without `-L` to see the first response, then `curl --head -L URL` to see the whole chain. A HEAD-only checker can't check links inside the page either, since it never gets the HTML.

## Common Response Codes

[200](https://howhttpworks.com/status-codes/200) means the GET would succeed, and here are its headers. [304](https://howhttpworks.com/status-codes/304) answers a conditional check. [405](https://howhttpworks.com/status-codes/405) means the resource doesn't allow HEAD, and it must include an `Allow` header. A 404 means the resource isn't there, or the server won't say.

## Important Headers in HEAD Responses

`Content-Type` describes what GET would return. `Content-Encoding` tells you whether the size is compressed. `ETag` and `Last-Modified` let you validate a cached copy. Expect some endpoints to send only a few of these, and compare ETags only between responses for the same negotiated representation.

## Getting HEAD right

Express runs your GET route for HEAD requests unless a HEAD route was registered before it. In this example Express computes the headers from the JSON it would send, then drops the body for HEAD:

```javascript
const express = require('express')
const app = express()
app.get('/metadata', (_req, res) => {
  res.json({ name: 'catalog' })
})
app.listen(3000)
```

See [Express routing](https://expressjs.com/en/5x/api/application/#app.get). If you set headers yourself, compute the length with `Buffer.byteLength(body, 'utf8')`, since a JavaScript string's `length` counts UTF-16 code units, not bytes. And make sure the headers describe the body you actually send: setting a stored file's size as `Content-Length` and then returning JSON about the file produces a broken response.

### Go file serving

Go's `http.ServeContent` handles HEAD, byte ranges and conditional requests for any seekable file. Let it build the headers from the file it serves:

```go
package main

import (
    "log"
    "net/http"
    "os"
)

func main() {
    http.HandleFunc("GET /report.pdf", func(w http.ResponseWriter, r *http.Request) {
        file, err := os.Open("./files/report.pdf")
        if err != nil {
            http.NotFound(w, r)
            return
        }
        defer file.Close()
        info, err := file.Stat()
        if err != nil {
            http.Error(w, "Cannot stat report", http.StatusInternalServerError)
            return
        }
        http.ServeContent(w, r, info.Name(), info.ModTime(), file)
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}
```

A `GET` ServeMux pattern matches HEAD too. [Go's documentation](https://pkg.go.dev/net/http#ServeContent) explains how ServeContent works out the size and handles validators. It won't make up an ETag; set one yourself only if your application can identify that exact representation.

## Try It Yourself

Compare HEAD and GET in the [playground](https://howhttpworks.com/tools/playground). On your own server, run both through curl and note which headers only appear once the body has been generated.

Try an error path too: `curl --head 'https://example.com/missing-report.pdf'` should get a 404 with no error-page body. Its Content-Type, if present, describes the error page GET would return.

## Related Methods

- [GET](https://howhttpworks.com/methods/get)
- [OPTIONS](https://howhttpworks.com/methods/options)

## Related Concepts

- [ETag](https://howhttpworks.com/headers/etag)
- [CORS](https://howhttpworks.com/guides/cors)
