There is a good chance you've seen HTTP headers hundreds of times without paying much attention to them.

I know I have.

In my day to day work, I deal with HTTP requests and responses all the time. I inspect response objects, check status codes and look at response bodies when something goes wrong or when I'm building something new.

Yet somehow, I never spent much time looking at the headers themselves. They were always there, but they felt like background noise rather than something worth understanding.

So what changed? Why did I decide to look at them now?

I'm not sure there was a specific reason. It just occurred to me that I'd spent years walking past this part of HTTP without ever stopping to see what was there. It's a bit like living in the same city for years and suddenly deciding to visit a museum you've walked past hundreds of times. It was always there. I just never made the time.

Then one day, it simply felt like it was time.

If HTTP requests and responses are such a fundamental part of web development, why had I never stopped to understand this piece of the conversation?

One thing I've noticed about myself is that I understand new concepts much more easily when I can actually see them. I don't know whether it's the frontend side of me talking, but I've always found it easier to learn when I have something tangible to look at: a log, a network request, a metric or some real output. It helps me visualize what's happening.

HTTP headers turned out to be perfect for that because you don't have to imagine anything about them. You can inspect HTTP headers yourself.

Open your browser's Developer Tools, switch to the Network tab and click on any request, or use a CLI tool like curl.

In this article, we'll use curl because it gives us a clear view of the HTTP conversation.

Enough talking, let's look at a real request.

We'll make a simple GET request to example.com, a domain reserved specifically for documentation and examples.

If you're curious about everything that happens before the browser sends this request, I've written about the journey from typing a URL to sending an HTTP request.

All we need is:

curl -v https://example.com

The -v (verbose) flag tells curl to show the full HTTP conversation, including both the request headers sent by the client and the response headers returned by the server.

The output contains much more than just the headers. It also includes information about the network connection and the TLS handshake. To keep things simple, I've included only the header portion below.

> GET / HTTP/2
> Host: example.com
> User-Agent: curl/8.7.1
> Accept: */*

< HTTP/2 200
< Content-Type: text/html
< Content-Length: 1248
< Cache-Control: max-age=3600

The > lines are the request headers sent by the client, while the < lines are the response headers returned by the server.

Even if you've never looked at request and response headers before, one thing probably stands out immediately.

Most of the names are plain English.

Host, User-Agent, Content-Type and Cache-Control are all fairly descriptive once you stop and read them. You don't need to memorize every HTTP header. After you understand a handful of the common ones, you'll start recognizing the patterns.

Every HTTP header follows the same structure. It's simply a name followed by a value, separated by a colon.

Host: example.com
Content-Type: text/html

The name tells you what information is being sent. The value contains that information.

As developers, we've all worked with key-value pairs in one form or another, whether it's Javascript objects, dictionaries or maps. HTTP headers follow exactly the same idea. They're simply a collection of key-value pairs describing the request or the response.

Now that we've seen what HTTP headers look like, let's take a step back. Before we look at what each header does, it's worth asking a more fundamental question:

Why do HTTP headers exist?

I started wondering about headers by trying to remove them from the equation to better understand their role.

Do we actually need HTTP headers?

After all, the browser wants a page and the server sends one back. Why isn't the conversation simply a request followed by a response?

Let's ignore headers for a moment.

Imagine your browser sends a request, the server replies with a stream of bytes and that's the end of the conversation.

The browser immediately has a problem.

What are those bytes? Are they HTML, JSON or an image? Should they be displayed in the browser or downloaded as a file? Can they be cached? Did the server create a session cookie?

My first thought was that perhaps the browser could simply inspect the data and figure it out. For some file formats, that's actually possible. Images, PDFs and other file types often have distinctive signatures that make them easier to identify.

The more I thought about it, the less I liked the idea of relying on guesses. Not every format is easy to identify, different browsers could make different decisions and, as we'll see in a future article, guessing content has caused real security problems in the past. Then how was this solved?

HTTP takes a much simpler approach.

Instead of asking the browser to guess, the sender provides information about what it's sending before the actual data arrives.

That kind of information has a name: metadata.

You've probably heard the phrase "data about data" before. That's exactly what metadata is. It isn't the content itself; it's information that describes the content.

And that's exactly what request and response headers are.

They don't contain the page itself. They contain metadata about the request or the response, helping the other side understand what it's about to receive and how it should handle it.

Requests have headers. Responses do too.

One thing that surprised me while learning about HTTP headers was realizing that they aren't just something the server sends back.

Every HTTP conversation contains two sets of headers. The client sends request headers before the request, and the server sends response headers before the response. Together, they describe the two halves of the HTTP conversation.

This article focuses on request headers. In the next one, we'll switch sides and explore the response headers sent back by the server. Stay tuned!

Common request headers

Every HTTP request includes a collection of HTTP request headers. Some identify the client, others describe what it expects from the server, and some carry information that allows the server to recognize or authorize the request.

The browser introduces itself

The first few headers tell the server who is making the request and which website the request is intended for.

Host

The Host header tells the server which website the browser is trying to access. You'll typically see it like this:

Host: example.com

If it feels confusing don't worry. At first this confused me too.

Didn't the browser already connect to example.com? Why does it need to send the domain name again?

Here's what actually happens:

  • When you type a URL such as example.com, one of the first things that happens is that DNS resolves the domain name to an IP address.
  • The browser then sends the request to that IP address.
  • When the request reaches the server, the server already knows which IP address the request arrived on. What it doesn't know yet is which website hosted on that server should handle it.
  • That's where the Host header comes in. It tells the server exactly which website the client is trying to access.

This becomes much easier to understand when multiple websites share the same server. Imagine two different domains both point to the same IP address:

example.com -> 203.0.113.10
example.net -> 203.0.113.10

Both requests arrive at exactly the same server. Without the Host header, the server wouldn't know whether you wanted example.com or example.net.

If you ever visit the IP address directly instead of the domain, you'll often see a completely different website or the server's default page. That's because the server uses the Host header to decide which website should handle the request.

User-Agent

This one is a little easier to understand.

User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/138.0

The User-Agent header answers a simple question:

What software is making this request?

Notice that it doesn't identify you. It doesn't contain your username or prove your identity. Instead, it describes the client sending the request. For example, it could tell the server that the request came from:

  • Google Chrome
  • Mozilla Firefox
  • a search engine crawler such as Googlebot
  • a CLI tool like curl

So why is this useful?

Historically, websites often returned slightly different content depending on the browser because browsers supported different features. Today, a much more common use is analytics, allowing website owners to see which browsers and devices are visiting their site. Search engine crawlers also identify themselves using the User-Agent header, making it easy for servers to recognize that the request is coming from a crawler.

One important thing to remember is that the User-Agent header should never be trusted for authentication or authorization. It's just information provided by the client, and the client can change it to anything.

For example, this command makes curl identify itself as Googlebot:

curl -v -A "Googlebot" https://example.com

The server receives:

User-Agent: Googlebot

even though the request was sent by curl.

In other words, the User-Agent is useful as a hint, but it should never be treated as proof of identity.

User-Agent ≠ Authentication

The browser says what it wants

After introducing itself, the browser also explains what it's expecting in return. These headers describe the formats, languages and compression methods the client is willing to accept.

Accept

The Accept header tells the server what kind of response the client is willing to receive.

For example:

Accept: text/html

In this case, the client is saying that it's expecting an HTML document. Other common values include:

Accept: application/json
Accept: image/webp
Accept: */*

The client is expressing a preference, not giving the server an order. The server decides whether it can satisfy that preference. If it can't, it may respond with 406 Not Acceptable, although in practice many servers simply ignore the header and return their default format.

You'll also come across other Accept-* headers that follow the same idea.

For example, Accept-Language tells the server which languages the client prefers:

Accept-Language: fr,en;q=0.8

The q value indicates preference. In this example, the client prefers French but is also willing to accept English if French isn't available.

Accept-Encoding

Another common one is Accept-Encoding, which tells the server which compression formats the client understands.

Accept-Encoding: gzip, br

In other words, the browser is saying:

"If you want to compress the response before sending it, I know how to decompress gzip and Brotli (br)."

If the server decides to compress the response, it lets the browser know by including a Content-Encoding response header. The browser then automatically decompresses the response before displaying the page. The entire process is completely invisible to the user.

💡 Did you know?
Many HTML, CSS and Javascript files become 70–90% smaller when compressed with modern algorithms like Gzip or Brotli. That's one of the reasons websites can load much faster, even though the browser still receives exactly the same content after decompressing it.

Origin

One more request header worth knowing is Origin.

Origin: https://example.com

Unlike the headers we've looked at so far, the Origin header tells the server which website initiated the request. You'll most often encounter it when building or consuming APIs that are called directly from a browser.

For example, imagine your frontend is running at https://app.example.com and it sends a request to an API at https://api.example.com. The browser automatically includes the Origin header, allowing the server to decide whether the cross-origin request should be allowed.

The browser proves it has access

So far, the browser has introduced itself and described what kind of response it expects. Sometimes, that's still not enough. Some requests also include information that allows the server to recognize the client or verify that it has permission to access a protected resource.

The Cookie header is how the browser sends information it has previously stored back to the server. Most commonly, this includes a session identifier that allows the server to recognize a returning user.

Imagine you log in to a website. The server doesn't remember you simply because you're using the same browser. Instead, it asks the browser to store a small piece of information called a cookie. On every future request, the browser automatically sends that cookie back using the Cookie header. This is one of the most common ways websites keep you logged in as you move from page to page.

Authorization

Unlike Cookie, the Authorization header is used when the client wants to explicitly prove it has permission to access a protected resource. Instead of relying on information stored by the browser, the client includes credentials such as an API key or an access token directly in the request.

You'll encounter this header frequently when working with APIs. If you're wondering how a website can verify your identity without ever storing your password, I wrote about how password hashing works and why websites can't tell you your password.

For example, after logging into an application, the client might receive an access token and include it in every subsequent request. The server validates that token before deciding whether to return the requested resource or reject the request.

Although both headers are commonly involved in authentication, they serve different purposes. Cookies are usually managed automatically by the browser, while the Authorization header is typically added explicitly by the client or application.

Key takeaways

  • Every HTTP request contains request headers that describe the client and the request itself.
  • Request headers are metadata sent before the request body.
  • The Host header tells the server which website should handle the request.
  • The User-Agent header identifies the software making the request, not the user.
  • The Accept family of headers tells the server what kinds of responses the client can handle.
  • Cookie and Authorization are commonly used to recognize or authenticate a client, but they serve different purposes.
  • Understanding request headers makes browser DevTools and network debugging much easier.

The next time you open your browser's Network tab, don't skip over the headers. They're no longer just a list of key-value pairs, they're the browser and the server explaining exactly what they're about to do.