Go HTTP Server Timeouts

Erman İmerErman İmer
4 min read

A few days ago, I was looking into an idle timeout error reported by a client using one of our APIs. To better understand HTTP server timeouts, I wrote some server and client code for testing, which I'd like to share with you. This might help others gain a clearer understanding. I believe the code is mostly self-explanatory, so I'll keep the explanations brief.

Write Timeout

WriteTimeout is the maximum duration before timing out writes of the response.

To trigger a write timeout, we simulate a delay in the server's root handler. The client gets an io.EOF error when it tries to receive the response from the server.

server.go

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    // create a new HTTP request multiplexer (router)
    h := http.NewServeMux()

    // register the root handler
    h.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
        // simulate a delay of 2 seconds to trigger a write timeout
        time.Sleep(2 * time.Second)

        // write the status
        w.WriteHeader(http.StatusOK)
    })

    // create the server with a write timeout of 1 second
    s := &http.Server{
        Addr:         ":8080",
        Handler:      h,
        WriteTimeout: 1 * time.Second,
    }

    // start the server
    fmt.Println("server: listening...")
    err := s.ListenAndServe()
    if err != nil && err != http.ErrServerClosed {
        fmt.Printf("server: %s\n", err.Error())
    }
}

client.go

package main

import (
    "fmt"
    "net/http"
    "os"
)

func main() {
    // try to send 
    resp, err := http.Get("http://localhost:8080/")
    if err != nil {
        fmt.Printf("client: %s\n", err)
        os.Exit(1)
    }
    defer resp.Body.Close()
    fmt.Printf("client: %s\n", resp.Status)
}

Client Output:

client: Get "http://localhost:8080/": EOF
exit status 1

Idle Timeout

IdleTimeout is the maximum amount of time to wait for the next request when keep-alives are enabled.

To trigger an idle timeout, we simulate a delay in the client after sending the first request and reading the status of the first response from the server. The client gets an io.EOF error when it tries to read the status of the second response.

server.go

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    // create a new HTTP request multiplexer (router)
    h := http.NewServeMux()

    // register the root handler
    h.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
        // write the status
        w.WriteHeader(http.StatusOK)
    })

    // create the server with an idle timeout of 1 second
    s := &http.Server{
        Addr:        ":8080",
        Handler:     h,
        IdleTimeout: 1 * time.Second,
    }

    // start the server
    fmt.Println("server: listening...")
    err := s.ListenAndServe()
    if err != nil && err != http.ErrServerClosed {
        fmt.Printf("server: %s\n", err.Error())
    }
}

client.go

package main

import (
    "bufio"
    "fmt"
    "net"
    "os"
    "time"
)

func main() {
    // connect to the server
    conn, err := net.Dial("tcp", "localhost:8080")
    if err != nil {
        fmt.Printf("client: %s\n", err.Error())
        os.Exit(1)
    }

    // write the first request
    fmt.Fprintf(conn, "GET / HTTP/1.1\r\n")
    fmt.Fprintf(conn, "Host: localhost:8080\r\n")
    fmt.Fprintf(conn, "Connection: keep-alive\r\n")
    fmt.Fprintf(conn, "\r\n")

    // read the status of the first response
    reader := bufio.NewReader(conn)
    status, err := reader.ReadString('\n')
    if err != nil {
        fmt.Printf("client: %s\n", err.Error())
        os.Exit(1)
    }
    fmt.Printf("client: %s", status)

    // discard the rest of the first response
    for {
        line, err := reader.ReadString('\n')
        if err != nil {
            fmt.Println("client: ", err.Error())
            os.Exit(1)
        }
        if line == "\r\n" {
            break
        }
    }

    // simulate a delay of 2 seconds to trigger an idle timeout
    time.Sleep(2 * time.Second)

    // write the second request
    fmt.Fprintf(conn, "GET / HTTP/1.1\r\n")
    fmt.Fprintf(conn, "Host: localhost:8080\r\n")
    fmt.Fprintf(conn, "Connection: keep-alive\r\n")
    fmt.Fprintf(conn, "\r\n")

    // try to read the status of the second response
    status, err = reader.ReadString('\n')
    if err != nil {
        fmt.Printf("client: %s\n", err.Error())
        os.Exit(1)
    }
    fmt.Printf("client: %s", status)
}

Client Output

client: HTTP/1.1 200 OK
client: EOF
exit status 1

Read Timeout

ReadTimeout is the maximum duration for reading the entire request, including the body.

To trigger a read timeout, we simulate a delay in the client after it connects to the server. The client gets an io.EOF error when it tries to read the server's response status.

server.go

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    // create a new HTTP request multiplexer (router)
    h := http.NewServeMux()

    // register the root handler
    h.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
        // write the status
        w.WriteHeader(http.StatusOK)
    })

    // create the server with a read timeout of 1 second
    s := &http.Server{
        Addr:        ":8080",
        Handler:     h,
        ReadTimeout: 1 * time.Second,
    }

    // start the server
    fmt.Println("server: listening...")
    err := s.ListenAndServe()
    if err != nil && err != http.ErrServerClosed {
        fmt.Printf("server: %s\n", err.Error())
    }
}
package main

import (
    "bufio"
    "fmt"
    "net"
    "os"
    "time"
)

func main() {
    // connect to the server
    conn, err := net.Dial("tcp", "localhost:8080")
    if err != nil {
        fmt.Printf("client: %s\n", err.Error())
        os.Exit(1)
    }

    // simulate a delay of 2 seconds to trigger a read timeout
    time.Sleep(2 * time.Second)

    // write the first request
    fmt.Fprintf(conn, "GET / HTTP/1.1\r\n")
    fmt.Fprintf(conn, "Host: localhost:8080\r\n")
    fmt.Fprintf(conn, "Connection: keep-alive\r\n")
    fmt.Fprintf(conn, "\r\n")

    // read the status of the first response
    reader := bufio.NewReader(conn)
    status, err := reader.ReadString('\n')
    if err != nil {
        fmt.Printf("client: %s\n", err.Error())
        os.Exit(1)
    }
    fmt.Printf("client: %s", status)
}

Client Output

client: EOF
exit status 1
0
Subscribe to my newsletter

Read articles from Erman İmer directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Erman İmer
Erman İmer