
go’s http server does not buffer the entire request body by default—your handler runs immediately, and `r.body` is a streaming reader; however, uncontrolled slow clients (e.g., slowloris) can exhaust resources if you don’t enforce timeouts or limit read behavior.
In Go’s net/http package, the http.Request.Body is not pre-buffered into memory. Instead, it’s an io.ReadCloser backed by the underlying network connection—essentially a lazy, on-demand stream. When your ServeHTTP method is called, the HTTP server has already parsed the request headers and started reading the body, but it does not wait for the full body to arrive before invoking your handler. This means:
- For a GET request: r.Body is typically empty (nil or an empty io.NopCloser), so no issue.
- For a POST/PUT with payload: r.Body remains open and readable as data arrives, and your code must explicitly read from it (e.g., via io.ReadAll, json.NewDecoder(r.Body).Decode(...), or streaming logic).
⚠️ Critical implication for security & resource usage:
If a malicious client sends a large Content-Length (e.g., 9 MB) but transmits bytes extremely slowly (one byte every 10 seconds), Go will keep the connection—and the associated goroutine and OS file descriptor—alive until the body is fully read or the connection closes. Since each active request consumes memory (stack, buffers, TLS state) and goroutines are cheap but not free, 10,000 such stalled connections can indeed exhaust memory, file descriptors, or CPU scheduler capacity, enabling Slowloris-style denial-of-service.
✅ The correct defense is timeout configuration — not buffering logic in your handler:
server := &http.Server{
Addr: ":8080",
Handler: &MyHandler{},
ReadTimeout: 5 * time.Second, // terminates slow header/body reads
WriteTimeout: 10 * time.Second, // protects response writes
IdleTimeout: 30 * time.Second, // enforces keep-alive idle limits
}
log.Fatal(server.ListenAndServe())- ReadTimeout starts ticking as soon as the connection is accepted, covering both header parsing and body reading. If the client fails to send data fast enough (including pauses between chunks), the connection is closed automatically.
- Note: ReadTimeout applies per-connection, not per-request body read — so even io.Copy or ioutil.ReadAll inside your handler will be interrupted if the total read time exceeds the timeout.
? Additional hardening tips:
- Use http.MaxBytesReader to cap total bytes read from r.Body:
limitedBody := http.MaxBytesReader(w, r.Body, 10<<20) // max 10 MB data, err := io.ReadAll(limitedBody) if err == http.ErrBodyReadAfterClose { // client closed early — handle gracefully } - Avoid r.ParseForm() or r.FormValue() on untrusted, high-volume endpoints without prior size limiting — they internally call ParseMultipartForm, which may allocate large in-memory buffers for multipart data.
- For file uploads, prefer streaming parsers (e.g., mime/multipart.Reader) and write to disk or external storage immediately, rather than accumulating in memory.
In summary: Go gives you streaming control — not automatic buffering — making your handler responsible for safe, bounded consumption of request bodies. Combine ReadTimeout, MaxBytesReader, and defensive reading patterns to build resilient, production-ready HTTP handlers.










