Understanding HTTP/1.1 Persistent Connections in Go

This article walks through the HTTP/1.1 keep‑alive model, shows how a simple Go program creates separate TCP connections for sequential requests, uses netstat and tcpdump to observe the behavior, and demonstrates how adjusting http.Client's transport settings enables true persistent connections even under concurrency.

Code DAO
Code DAO
Code DAO
Understanding HTTP/1.1 Persistent Connections in Go

Background

Originally HTTP used a single request‑response cycle per TCP connection, which meant each request required a costly TCP handshake and teardown. HTTP/1.1 introduced the keep‑alive (persistent) model so that a TCP connection can stay open across multiple requests, reducing latency.

Sequential request demo

A minimal Go program starts an HTTP server on port 8080 and then issues ten consecutive http.Get calls from a goroutine. After the program runs, netstat -n | grep 8080 shows ten distinct TCP connections, confirming that the default client does not reuse the connection.

The TCP state diagram explains the TIME‑WAIT period that follows a graceful close, ensuring the peer receives the final ACK and preventing packet mix‑up between successive connections.

Using tcpdump

Running sudo tcpdump -i any -n host localhost captures every packet. With the original loop (10 requests) the capture contains two SYN packets, indicating two separate connections because the loop was reduced to two requests for brevity.

Fixing the client

The Go documentation states that if the response body is not fully read and closed, the underlying Transport cannot reuse the TCP connection. Adding io.Copy(ioutil.Discard, resp.Body) followed by resp.Body.Close() resolves this.

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "net/http"
    "time"
)

func startHTTPServer() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(50 * time.Microsecond)
        fmt.Fprintf(w, "Hello world")
    })
    go func() { http.ListenAndServe(":8080", nil) }()
}

func startHTTPRequest() {
    for i := 0; i < 10; i++ {
        resp, err := http.Get("http://localhost:8080/")
        if err != nil { panic(err) }
        io.Copy(ioutil.Discard, resp.Body) // read the body
        resp.Body.Close()                  // close the body
        log.Printf("HTTP request #%d", i)
        time.Sleep(1 * time.Second)
    }
}

func main() {
    startHTTPServer()
    startHTTPRequest()
}

After this change, netstat shows only one TCP connection for the ten requests, confirming a persistent connection.

Concurrent requests

The next version launches ten goroutines, each issuing ten requests. Because HTTP/1.1 creates a separate TCP connection per goroutine, the number of connections far exceeds ten, showing that the default MaxIdleConnsPerHost (value 2) limits the connection pool.

Source inspection of net/http reveals the Client struct and the default Transport configuration, where MaxIdleConns defaults to 100 and MaxIdleConnsPerHost defaults to 2.

type Client struct {
    Transport RoundTripper
    CheckRedirect func(req *Request, via []*Request) error
    Jar CookieJar
    Timeout time.Duration
}

var DefaultTransport RoundTripper = &Transport{
    Proxy: ProxyFromEnvironment,
    DialContext: (&net.Dialer{Timeout: 30*time.Second, KeepAlive: 30*time.Second}).DialContext,
    ForceAttemptHTTP2: true,
    MaxIdleConns: 100,
    IdleConnTimeout: 90*time.Second,
    TLSHandshakeTimeout: 10*time.Second,
    ExpectContinueTimeout: 1*time.Second,
}

const DefaultMaxIdleConnsPerHost = 2

To allow ten idle connections per host, a custom client is created in init() with

Transport: &http.Transport{MaxIdleConnsPerHost: 10, MaxIdleConns: 100}

. The same concurrent test now shows only ten TCP connections, confirming that the pool size controls reuse.

Summary

The article demonstrates how HTTP/1.1 keep‑alive works in Go, how to observe connection behavior with netstat and tcpdump, why reading and closing the response body is required for reuse, and how adjusting MaxIdleConnsPerHost enables true persistent connections both in sequential and concurrent scenarios.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

GoHTTPhttp.Clienttcpdumpnetstatpersistent connections
Code DAO
Written by

Code DAO

We deliver AI algorithm tutorials and the latest news, curated by a team of researchers from Peking University, Shanghai Jiao Tong University, Central South University, and leading AI companies such as Huawei, Kuaishou, and SenseTime. Join us in the AI alchemy—making life better!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.