skip to content

Bodies and Connection Reuse

You must close resp.Body, and unless it is read to the end the connection is closed instead of returned to the pool, so a busy client quietly burns sockets. The classic Go client trap.

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

questions

4

Why must you call resp.Body.Close() on an http.Response, and where does that defer belong?

level: juniorimportance: must knowfreq 82%

answer

  1. who owns the connection after the call returns
  2. the headers arrived; the payload has not
  3. no finalizer will do it for you
  4. one line too early and resp is nil
  5. defer fires at function return, not per iteration

basics

~20 s

An http.Response body is an open stream on a live connection; closing it releases that connection and its socket. Put defer resp.Body.Close() straight after the error check, because a failed request returns a nil response.

solid answer

~40 s

`http.Client.Do` returns once the response headers arrive, so `resp.Body` is an `io.ReadCloser` sitting on the still-open connection to the server. Nothing in `net/http` knows when you have finished with it, so closing is the caller's job: until you do, the socket and its file descriptor stay allocated and the connection can never go back to the Transport's idle pool. The garbage collector will not do it for you. The defer goes *after* the `if err != nil` check, because when `Do` returns an error the response is nil and `defer resp.Body.Close()` dereferences nil. When the error is nil the body is guaranteed non-nil even for a 204 or a HEAD, so you always close it, including on a 500 response you are about to turn into an error.

code

go · 10 lines
go
resp, err := http.Get(url)
if err != nil {
	return err // resp is nil here - there is nothing to close
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
	return fmt.Errorf("unexpected status %s", resp.Status)
}
return json.NewDecoder(resp.Body).Decode(&out)

go deeper

for a junior

Recall the three-line shape and its order: call, error check, then defer resp.Body.Close(). Be ready to say what the body actually is - an open stream on a live connection, not a string already in memory.

for a middle

Explain why the language cannot close it for you: Do returns at the headers, the connection stays open, and there is no finalizer. Be ready to explain what defer inside a loop does and why req.Body is not yours to close.

for a senior

Show the operational shape of the bug: sockets and descriptors climbing in a long-running process until unrelated work starts failing. Be ready to say that closing is necessary for the descriptor but not sufficient for connection reuse.

for a principal

Frame it as a convention the codebase enforces rather than a fact people remember: a review checklist item, a vet or lint rule for unclosed bodies, and a wrapper helper so no caller writes the raw sequence by hand.

### The body is a live stream, not a buffer `http.Client.Do` — and the wrappers `http.Get`, `http.Post`, `http.PostForm` that call it — return as soon as the response *status line and headers* have been read. The payload has usually not been transferred yet. `resp.Body` is an `io.ReadCloser` layered on top of the still-open TCP (or TLS) connection to the server: reading it pulls bytes off the wire, and closing it tells the `net/http` Transport that you are finished with that stream. That is why closing is the caller's responsibility and not the library's. `net/http` cannot know when you have stopped caring about a response, so until `Close` is called the Transport must assume the stream is still in use. Nothing in the runtime rescues you: there is no finalizer that closes response bodies, so "the GC will clean it up" is simply wrong. ### What an unclosed body costs * **A socket and its file descriptor.** Every live connection holds one. A daemon that leaks one per request climbs steadily until the process hits its descriptor ceiling, at which point *everything* that needs a new descriptor starts failing — not just HTTP. * **A connection that can never be reused.** The Transport only returns a keep-alive connection to its idle pool after the body has been read to EOF *and* closed. An unclosed body pins that connection out of circulation, so the next request pays for a new DNS lookup, TCP handshake and TLS handshake. * **Reader goroutines and buffers.** Each in-flight HTTP/1 connection has bookkeeping behind it; leak enough of them and the leak shows up as memory growth as well as descriptor growth. ### Where the defer goes The canonical shape is three lines and the order of them matters: ```go resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() ``` When `Do` returns a non-nil error, `resp` is nil. Putting the defer first therefore panics with a nil pointer dereference on the way out of the function — a very common beginner bug, and one that turns a recoverable network error into a crash. The mirror-image mistake is skipping the close on a non-2xx response. A 404 or a 500 is a perfectly successful HTTP exchange as far as the client is concerned: `err` is nil, the body is non-nil, and the connection is real. Check the status code *after* arranging the close, never instead of it. `http.Response`'s documentation guarantees that `Body` is non-nil whenever the error is nil, even for a 204 No Content or a response to a HEAD request. In those cases the first `Read` simply returns `io.EOF`. So there is never a case where you must nil-check the body before closing it. ### Deferring inside a loop ```go for _, url := range urls { resp, err := http.Get(url) // wrong: one open body per iteration if err != nil { return err } defer resp.Body.Close() // ... } ``` `defer` runs at *function* return, not at the end of the loop iteration, so this holds every body open until the whole loop finishes. With a thousand URLs that is a thousand open sockets. The fix is to move the request into its own function (or a closure invoked per iteration) so the defer fires each time round, or to close explicitly at the end of the iteration on every path. ### The request body is not your problem A frequent confusion: you do **not** close `req.Body`. When you hand a body to `http.NewRequest` or `http.NewRequestWithContext`, the `http.Client` takes ownership and closes it, on success and on failure alike. Closing it yourself can turn into a use-after-close on a retry. Only the *response* body is yours. ### Closing is necessary but not sufficient Closing releases the descriptor, but it does not by itself guarantee the connection is *reused*. For a keep-alive connection to go back into the Transport's idle pool the body normally has to be read to EOF first; closing an HTTP/1 body with a large unread remainder still costs you the connection. That is a separate discipline — draining — layered on top of the rule here. The rule here is unconditional: if `err` is nil, the body gets closed. ### What the error from Close means `Close` returns an error, and on a response body it is almost always ignored — there is nothing meaningful to do about a failure to tear down a read side, and linters that demand it are usually satisfied with `defer func() { _ = resp.Body.Close() }()`. That is unlike a *file you wrote to*, where the close error can be the first sign that your data never landed.

  • Do you also have to close the request body you passed to http.NewRequest?
    No. The `http.Client` takes ownership of `req.Body` and closes it for you, on success and on failure. Closing it yourself risks a use-after-close if the client replays or retries the request. Only the response body is the caller's to close.
  • The server replied 204 No Content. Is resp.Body nil, so you can skip the close?
    It is never nil when the error is nil — `http.Response` documents that the Client and Transport always supply a non-nil `Body`, even for responses with no payload. The first `Read` returns `io.EOF` immediately. Close it exactly as you would any other response.
  • What goes wrong when defer resp.Body.Close() sits inside a loop over a thousand URLs?
    `defer` runs when the enclosing *function* returns, so all thousand bodies stay open at once — a thousand sockets and descriptors held simultaneously. Move the request into its own function so each close fires per iteration, or close explicitly at the end of the loop body on every path.

saying these in an interview costs you the question

  • Says the garbage collector eventually closes response bodies
  • Writes defer resp.Body.Close() above the if err != nil check
  • Skips the close when the status code is not 2xx
  • Thinks only large responses need closing
  • Closes req.Body as well as resp.Body
  • Defers the close inside a loop and calls it done
open as a page

Why drain an http.Response body with io.Copy(io.Discard, resp.Body) before closing it?

level: middleimportance: should knowfreq 58%

basics

~20 s

Go's net/http Transport pools a keep-alive connection only after the response body is read to EOF and closed. Closing with unread bytes on the wire makes it drop the connection instead, so the next request re-dials.

open as a page

In a Go daemon whose open socket count climbs every push cycle, how do you tell an unclosed resp.Body from an undrained one?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Look at the shape, not the count. A body never closed pins one live connection per request, so sockets grow without bound. A body closed before it was read releases each socket but reuses none, so you see constant re-dialling.

open as a page

In Go's net/http, what breaks when you set Accept-Encoding: gzip on the request yourself?

level: middleimportance: nice to knowfreq 34%

basics

~20 s

Go's http.Transport requests gzip itself and silently decompresses the reply, but only while the caller sets no Accept-Encoding header. Set it yourself and the Transport steps back: resp.Body hands you raw gzip bytes to wrap in gzip.NewReader.

open as a page