Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan Now×
Skip to content
backend

How to Build a Go net/http Server (with Safe Timeouts, Limits, Shutdown, and Tests)

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The smallest useful Go HTTP server has three parts: a handler that writes a response, a ServeMux that chooses a handler for each request, and a server that accepts connections. Start with an explicit mux and the standard library, then add request limits, workload-appropriate timeouts, graceful shutdown, and boundary tests before exposing the service to real traffic.

The mental model: handler, mux, and server

A handler implements ServeHTTP(http.ResponseWriter, *http.Request). It reads the request and writes status, headers, and a response body. A function with the same inputs can be adapted with http.HandlerFunc, which is why small endpoints are often declared as functions.

A mux (multiplexer) matches the incoming method and path to a handler. http.NewServeMux() creates an explicit router. Passing that mux to a server keeps route registration visible instead of relying on package-global state. The server accepts connections, applies its read/write policies, and invokes the mux.

A minimal, runnable server

This example targets Go 1.22 or newer and listens on a local development port.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
package main

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

func home(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hello from Go")
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", home)

    log.Println("listening on http://localhost:8080")
    if err := http.ListenAndServe(":8080", mux); err != nil {
        log.Fatal(err)
    }
}

Run it with go run ., then request http://localhost:8080/. ListenAndServe blocks while the server is running. It returns a non-nil error when serving stops; in this simple program, logging an unexpected return is appropriate.

Default mux versus an explicit mux

If you pass nil as the handler, ListenAndServe uses the package-level http.DefaultServeMux. Code can register routes with http.HandleFunc, but global registration makes dependencies harder to see and can cause conflicts in tests or larger programs. An explicit mux, as above, gives each server its own route table.

Plain HTTP versus HTTPS

For local development, plain HTTP is convenient. For an externally exposed service, configure TLS with ListenAndServeTLS or the corresponding http.Server method and provide certificate and private-key material. The standard library does not automatically obtain certificates for you.

Use an http.Server when you need control

The convenience function hides important operational settings. A configured server lets you select timeout and header policies and gives you a lifecycle object for shutdown.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
srv := &http.Server{
    Addr:              ":8080",
    Handler:           mux,
    ReadHeaderTimeout: 5 * time.Second,
    ReadTimeout:       30 * time.Second,
    WriteTimeout:      30 * time.Second,
    IdleTimeout:       120 * time.Second,
    MaxHeaderBytes:    1 << 20, // 1 MiB
}

if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
    log.Fatal(err)
}

Add errors and time to the imports. The 10-second read/write and 1 MiB header values shown in Go’s package documentation are illustrative settings, not universal defaults. Choose values from your request sizes, client behavior, proxy limits, and latency objectives.

What each timeout covers

  • ReadHeaderTimeout: maximum time to read request headers. It is useful against clients that connect and then send headers very slowly.
  • ReadTimeout: maximum time to read the entire request, including its body. A short value can reject legitimate slow uploads.
  • WriteTimeout: maximum time spent writing a response. Streaming endpoints may need a different design or policy.
  • IdleTimeout: time to wait for the next request on an existing keep-alive connection.

For timeout fields, zero or a negative value has documented no-timeout semantics. “More aggressive” is not automatically safer: a value that protects one API can break uploads, long polls, or slow but valid clients. Measure or reason about each route and set policies accordingly.

Header bytes are not body bytes

MaxHeaderBytes limits request headers and the request line. It does not limit a JSON document, multipart upload, or other request body. Body limits must be enforced while the handler reads the body.

Limit request bodies per route

Use http.MaxBytesReader before decoding or copying an incoming body. The handler chooses the limit because an avatar upload and a small JSON command have different requirements.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
func createUser(w http.ResponseWriter, r *http.Request) {
    const maxBody = 1 << 20 // 1 MiB
    r.Body = http.MaxBytesReader(w, r.Body, maxBody)
    defer r.Body.Close()

    var input struct {
        Name string `json:"name"`
    }
    dec := json.NewDecoder(r.Body)
    if err := dec.Decode(&input); err != nil {
        var tooLarge *http.MaxBytesError
        if errors.As(err, &tooLarge) {
            http.Error(w, "request body too large", http.StatusRequestEntityTooLarge)
            return
        }
        http.Error(w, "invalid JSON", http.StatusBadRequest)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"name":%q}`+"n", input.Name)
}

Import encoding/json along with errors and net/http. When the limit is exceeded, reads return an *http.MaxBytesError. Handle that case separately so clients receive a useful 413 response rather than an opaque decoding error. Consider rejecting trailing JSON values, validating fields, and applying stricter limits to especially expensive operations.

Routing and the Go 1.22 change

ServeMux pattern syntax and matching changed significantly in Go 1.22. Method-qualified patterns and wildcard segments behave according to the version of the standard library you compile with. For example, patterns that use newer method or wildcard syntax should be checked against the Go 1.22 package documentation before deployment.

Escaped path segments and invalid patterns also have compatibility implications. If you are migrating an older application and need the pre-1.22 behavior, the documented compatibility switch is GODEBUG=httpmuxgo121=1, read at process startup. Treat that as a migration aid, not a reason to mix old and new assumptions indefinitely. State your target Go version in examples, CI, and deployment images.

Graceful shutdown that actually waits

Closing a process immediately can truncate responses and interrupt in-flight work. Handle an interrupt or termination signal, call Shutdown with a bounded context, and wait for it to return.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
package main

import (
    "context"
    "errors"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func run() error {
    mux := http.NewServeMux()
    mux.HandleFunc("/", home)

    srv := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    serverErr := make(chan error, 1)
    go func() {
        serverErr <- srv.ListenAndServe()
    }()

    stop := make(chan os.Signal, 1)
    signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
    defer signal.Stop(stop)

    select {
    case err := <-serverErr:
        if !errors.Is(err, http.ErrServerClosed) {
            return err
        }
    case <-stop:
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        if err := srv.Shutdown(ctx); err != nil {
            return err
        }
        // Shutdown has completed; the serving goroutine can now finish.
        if err := <-serverErr; !errors.Is(err, http.ErrServerClosed) {
            return err
        }
    }
    return nil
}

In a complete program, call log.Fatal(run()) from main. Shutdown closes listeners and idle connections, then waits for active connections to become idle until the context deadline. Once shutdown begins, ListenAndServe returns http.ErrServerClosed; that is the expected path, not a startup failure.

Upgraded and hijacked connections

Shutdown does not close or wait for hijacked connections, such as WebSockets. Track those connections separately and signal their handlers to stop. A shutdown deadline should be long enough for normal requests but finite enough for the process supervisor to recover a stuck instance.

Test at the HTTP boundary

The net/http/httptest package lets you test handlers and complete request/response behavior without binding your production port.

func TestHome(t *testing.T) {
    mux := http.NewServeMux()
    mux.HandleFunc("/", home)

    req := httptest.NewRequest(http.MethodGet, "http://example.test/", nil)
    rec := httptest.NewRecorder()
    mux.ServeHTTP(rec, req)

    if rec.Code != http.StatusOK {
        t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
    }
    if got := rec.Body.String(); got != "hello from Gon" {
        t.Fatalf("body = %q", got)
    }
}

For more realistic tests, httptest.NewServer(mux) starts an in-process HTTP server and returns a client-facing URL; close it with defer ts.Close(). Assert status, headers, body, redirects, and error responses. Configure test-server behavior before first use, because changing its configuration after requests begin is unsafe or ineffective.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Production checklist

  • Use an explicit mux and document the Go version whose routing rules you rely on.
  • Set header, read, write, and idle policies based on actual endpoint behavior.
  • Apply MaxBytesReader to every route that accepts an untrusted body.
  • Validate content type, decoded fields, authentication, and authorization separately from size checks.
  • Return http.ErrServerClosed as an expected shutdown result and wait for Shutdown.
  • Coordinate WebSocket or other hijacked connections outside Shutdown.
  • Test handlers with httptest, including oversized bodies and slow-client timeout behavior.
  • Use TLS and a certificate strategy appropriate for the environment rather than assuming the standard library provisions one.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Common failures and fixes

“Address already in use”

Another process owns the port. Stop it, choose another port such as :8081, or configure the listener explicitly. This is a bind-time error, not a handler problem.

The server exits immediately with no useful response

Check the error returned by ListenAndServe. A startup error should be logged. During intentional shutdown, compare it with http.ErrServerClosed and do not report that expected value as a crash.

Large uploads fail unexpectedly

Inspect both policies: MaxHeaderBytes does not limit the body, while MaxBytesReader does. Also check ReadTimeout; it includes body reading and can be too short for slow clients.

Legitimate clients see timeouts

Identify which phase timed out. Increase only the relevant setting, or give upload/streaming endpoints a different architecture and policy. Do not copy documentation example values without considering payload size and latency.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A route changes behavior after upgrading Go

Review ServeMux 1.22 pattern and escaping rules, run route tests under the target toolchain, and use GODEBUG=httpmuxgo121=1 only as a deliberate compatibility step during migration.

Or skip the browser setup

If your Go service also needs website screenshots for previews, reports, or automated checks, ScreenshotNeo provides a single HTTP request instead of maintaining browser setup. It accepts cookie and consent banners like a visitor, removes more than 60 known consent platforms plus newsletter popups and chat widgets before capture, and lets you turn those steps off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether the shot was billed. Its MCP server gives Claude, Cursor, and other MCP clients take_screenshot, get_page_info, and capture_pdf tools.

Use the API from Go or any HTTP client. The complete endpoint and options are documented at ScreenshotNeo’s API documentation.

package main

import (
    "os"
    "io"
    "log"
    "net/http"
    "net/url"
)

func main() {
    q := url.Values{}
    q.Set("access_key", os.Getenv("SCREENSHOTNEO_API_KEY"))
    q.Set("url", "https://stripe.com")

    res, err := http.Get("https://api.screenshotneo.com/v1/shot?" + q.Encode())
    if err != nil {
        log.Fatal(err)
    }
    defer res.Body.Close()

    if res.StatusCode < 200 || res.StatusCode >= 300 {
        log.Fatalf("ScreenshotNeo returned %s", res.Status)
    }
    f, err := os.Create("shot.webp")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
    if _, err := io.Copy(f, res.Body); err != nil {
        log.Fatal(err)
    }
}

ScreenshotNeo supports full-page captures with lazy images loaded, CSS-selector element captures, dark mode, 12 device presets or custom viewports, retina scale, PNG/JPEG/WebP, PDF paper and page options, custom CSS and JavaScript, pre-capture clicks, hidden selectors, selector/delay/network-idle waits, ad/tracker/request blocking, custom headers/cookies/user agents and Authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed public image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, an OpenAPI specification, and compatible parameter names used by other screenshot APIs.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; Growth is $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing provides two months free, and every feature is on every plan. Sign up free for 1,000 screenshots a month with no card.

Frequently Asked Questions

Should a small Go service use ListenAndServe or http.Server?

Use ListenAndServe for a genuinely local or disposable program. Use http.Server when you need explicit timeouts, header limits, TLS configuration, or graceful shutdown.

Does Shutdown stop WebSocket connections?

No. Hijacked connections are outside Shutdown’s close-and-wait behavior and require their own coordination.

What Go version do these routing examples require?

The article’s examples target Go 1.22 or newer, whose ServeMux patterns and matching differ significantly from earlier releases.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read next

Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.