skip to content

Size Limits and Caps

json.Unmarshal reads the whole body into memory before it parses anything, so an unbounded request body is a memory bug rather than a parsing one. The cap belongs in front of the decoder.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

In a Go HTTP handler, what does http.MaxBytesReader do when a client sends a body larger than the limit?

level: juniorimportance: must knowfreq 58%

answer

  1. wrap the body before anything reads it
  2. the limit is in bytes, not fields
  3. a typed error, not a silent stop
  4. the handler still writes the status code
  5. errors.As gives you the limit that was hit

basics

~20 s

http.MaxBytesReader wraps a request body so any read past the given byte limit fails with a *http.MaxBytesError instead of continuing. It caps what one request can allocate, but the handler must still write the 413 response itself.

solid answer

~50 s

`http.MaxBytesReader(w, r.Body, n)` returns an `io.ReadCloser` that you assign back to `r.Body` before anything reads it. Reads work normally up to `n` bytes; the read that would go past `n` fails with an error of type `*http.MaxBytesError`, which carries the `Limit` field, so you can match it with `errors.As` and reply `http.StatusRequestEntityTooLarge`. It also tells the server that the request was too large, so the server stops trying to drain the rest of the body and reuse the connection. Two things it does not do: it does not write any response for you, and it does not limit anything but bytes — a 1 MB body is still 1 MB of untrusted structure to decode. It must wrap the body before `json.NewDecoder(r.Body).Decode` or any other read, because after the bytes are read the allocation has already happened.

code

go · 12 lines
go
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB

var payload Event
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
	var tooLarge *http.MaxBytesError
	if errors.As(err, &tooLarge) {
		http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
		return
	}
	http.Error(w, "malformed body", http.StatusBadRequest)
	return
}

go deeper

for a junior

Be ready to write the two lines from memory: reassign r.Body to http.MaxBytesReader(w, r.Body, n) at the top of the handler, then return 413 when the read fails. Know that the limit counts bytes.

for a middle

Explain why the wrapper needs the ResponseWriter, how errors.As pulls *http.MaxBytesError out of a decoder's wrapped error, and why the cap is useless once the body has been read.

for a senior

Show where the cap belongs so no route can forget it — middleware or a handler wrapper — and be able to turn a chosen limit into a worst-case memory number for the number of requests you expect in flight.

for a principal

Own the default. Decide what limit every public endpoint inherits, how a team with a legitimately larger payload gets an exception, and why the server keeps its own cap even when an edge proxy has one.

## The problem it solves An exported HTTP endpoint reads whatever the client sends. Nothing in `net/http` bounds the size of a request body by default: the server reads request *headers* up to `Server.MaxHeaderBytes` (1 MB by default, `http.DefaultMaxHeaderBytes`), but the body is whatever the handler chooses to read. If a handler does `json.NewDecoder(r.Body).Decode(&v)` or reads the body into a `[]byte`, an anonymous poster sending a multi-gigabyte body decides how much memory the process allocates. On a public webhook receiver, that is a one-request denial of service that needs no credentials. ## What the wrapper does `http.MaxBytesReader(w http.ResponseWriter, r io.ReadCloser, n int64) io.ReadCloser` returns a reader that passes through the first `n` bytes and then fails. Concretely: - Reads that stay within `n` behave exactly like the underlying body. - The read that would cross `n` returns an error of concrete type `*http.MaxBytesError`. That type has a `Limit int64` field holding the limit that was exceeded, so an error message can name the number the caller has to respect. Match it with `errors.As(err, &tooLarge)` where `tooLarge` is a `*http.MaxBytesError` — never by string comparison. - It takes the `ResponseWriter` as its first argument precisely so it can tell the server the request was over-sized. The server then stops trying to consume the remainder of the body before writing the reply and does not keep the connection alive for reuse — otherwise the server would sit there draining the very flood you just rejected. - The returned value is an `io.ReadCloser`, so it is a drop-in replacement for `r.Body`. ## How it is used The idiom is to reassign `r.Body` at the very top of the handler (or in middleware, or with `http.MaxBytesHandler`, which wraps a whole handler with a cap), then decode: ```go r.Body = http.MaxBytesReader(w, r.Body, 1<<20) ``` Order matters. The cap constrains *reads that happen after it*. Wrapping the body after something has already read it into memory accomplishes nothing at all — the allocation is already on the heap. ## What it does not do **It does not send a response.** Many handlers get this wrong and return 400 for an over-sized body, or return 500 because the error fell through to a generic branch. The handler must inspect the error and write `http.Error(w, ..., http.StatusRequestEntityTooLarge)` — status 413 — so the caller learns that the body, not its content, was the problem. **It does not limit structure.** A byte cap says nothing about how many objects that byte count expands into once decoded. A megabyte of deeply nested arrays decoded into an `any` becomes a very large number of small heap objects; a byte cap bounds the input, not the decoded graph. **It is not `Server.MaxHeaderBytes`.** That field bounds the request line and headers the server will accept before the handler ever runs; it has nothing to do with the body. **It is not `io.LimitReader`.** `io.LimitReader` reports a plain `io.EOF` at its limit, which a decoder cannot distinguish from a body that genuinely ended — the request comes back as a syntax error, not as "too large". `http.MaxBytesReader` exists to give an HTTP server a distinguishable, typed failure. ## Choosing the number The limit is a capacity decision, not a style choice: the worst-case memory a body cap allows is roughly the cap multiplied by the number of requests in flight, so 1 MB across 500 concurrent requests is half a gigabyte of body bytes before decoding costs are counted. Pick a number from the observed size distribution of real payloads with headroom, apply it per route where a route legitimately needs more, and document it so a 413 is diagnosable by the caller rather than mysterious.

  • Does http.MaxBytesReader send the 413 response for you?
    No. It only makes the read fail. The handler must detect the error and write the status itself, typically `http.Error(w, "body too large", http.StatusRequestEntityTooLarge)`. If you let the error fall into a generic branch, the caller sees 400 or 500 and has no idea the body size was the problem.
  • How do you tell an over-sized body apart from any other read error?
    Declare `var tooLarge *http.MaxBytesError` and call `errors.As(err, &tooLarge)`. Because the decoder wraps the read error, `errors.As` unwraps to find it. The `Limit` field then lets you put the actual byte limit in the response so the caller knows what to send instead.
  • Where in the handler must the wrap happen?
    Before any read of the body. Assign `r.Body = http.MaxBytesReader(w, r.Body, n)` as the first statement, or push it into middleware or `http.MaxBytesHandler` so no route can forget. Wrapping after the body has been read is a no-op: the bytes are already in memory.
  • Does capping the body also protect the server's header parsing?
    No. Request headers are bounded separately by `Server.MaxHeaderBytes`, which defaults to 1 MB and is enforced before your handler runs. A body cap and a header cap cover different parts of the request, and neither substitutes for the other.

It is a turnstile with a counter, not a bouncer: it stops letting bytes through and raises an alarm, but somebody still has to tell the visitor why they were turned away.

saying these in an interview costs you the question

  • Thinks http.MaxBytesReader replies with 413 automatically
  • Wraps r.Body after the body has already been read
  • Believes it limits the number of decoded fields, not bytes
  • Confuses it with Server.MaxHeaderBytes, which covers headers only
  • Expects a silent truncation at the limit like io.LimitReader
  • Matches the over-limit error by comparing message strings
open as a page

Why is checking r.ContentLength not a safe size cap on an untrusted HTTP request body in Go?

level: middleimportance: should knowfreq 45%

basics

~20 s

r.ContentLength only reports what the client declared. A chunked request declares nothing and arrives as -1, so a length check silently passes exactly the requests whose size is unknown. Only a cap on bytes actually read bounds the allocation.

open as a page

A webhook receiver caps bodies at 1 MB, yet its heap profile spikes on deeply nested JSON. Why?

level: seniorimportance: should knowfreq 36%

basics

~20 s

A byte cap bounds input, not the decoded graph. Nested arrays decoded into an any allocate a fresh slice and interface value per level, so a megabyte of brackets becomes many megabytes of small heap objects — multiplied by every request in flight.

open as a page

Your platform caps every request body at 1 MB and a team's legitimate 5 MB payload now returns 413. How do you decide?

level: principalimportance: should knowfreq 28%

basics

~20 s

Do not raise the global default. Price the request — cap times decoded expansion times requests in flight — then grant a per-route exception with its own cap and concurrency bound, or change the payload shape so the large case never travels in a request body.

open as a page

Why does io.LimitReader in front of a JSON decoder turn an over-sized request into a parse error?

level: middleimportance: nice to knowfreq 38%

basics

~20 s

io.LimitReader reports io.EOF once its byte budget is spent, which looks exactly like the stream ending. The decoder sees a value cut off mid-way and reports truncated input, so the handler cannot tell an over-sized body from malformed JSON.

open as a page