DEV Community

ZèD
ZèD

Posted on • Edited on

HTTP Server Management in Go: Graceful Shutdowns and Error Handling

HTTP Server Management in Go: Graceful Shutdowns and Error Handling

A production HTTP server is not only about serving requests. It also needs predictable startup, proper runtime error handling, and graceful shutdown behavior.

This guide shows a clean lifecycle pattern for Go HTTP services.

Why It Matters

  • Prevents dropped requests during deployments.
  • Handles OS signals cleanly in containers and VMs.
  • Improves operational reliability and observability.
  • Avoids hanging processes during shutdown.

Core Concepts

1. Preconfigured Server Construction

Create *http.Server with explicit timeouts and handlers.

srv := webapi.NewServer()
Enter fullscreen mode Exit fullscreen mode

2. Runtime Error Channel

Use buffered error channel to capture fatal server errors without goroutine leaks.

serverErrCh := make(chan error, 1)
Enter fullscreen mode Exit fullscreen mode

3. Non-Blocking Server Start

Run ListenAndServe in goroutine so main routine can monitor shutdown signals.

4. Signal-Aware Context

Use signal.NotifyContext to simplify signal handling.

5. Graceful Shutdown with Timeout

Allow in-flight requests to finish within a bounded timeout window.

6. Fallback Forced Close

If graceful shutdown fails, force close server to ensure process exits.

Practical Example

package main

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

func main() {
    srv := webapi.NewServer()
    serverErrCh := make(chan error, 1)

    go func() {
        log.Printf("starting server on http://%s", srv.Addr)
        if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            serverErrCh <- err
        }
    }()

    sigCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    select {
    case err := <-serverErrCh:
        log.Fatalf("server error: %v", err)
    case <-sigCtx.Done():
        log.Println("shutdown signal received")
    }

    shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := srv.Shutdown(shutdownCtx); err != nil {
        log.Printf("graceful shutdown failed: %v", err)

        if closeErr := srv.Close(); closeErr != nil {
            log.Fatalf("forced close failed: %v", closeErr)
        }
    }

    log.Println("server stopped")
}
Enter fullscreen mode Exit fullscreen mode

This pattern keeps behavior deterministic during Ctrl+C, orchestration stop signals, and runtime failures. Your pager will appreciate that.

Common Mistakes

  • Running ListenAndServe on main goroutine and blocking lifecycle control.
  • Ignoring http.ErrServerClosed and treating normal shutdown as failure.
  • No shutdown timeout context for in-flight requests.
  • Not handling SIGTERM in containerized environments.
  • Forgetting forced close fallback when graceful shutdown hangs.

Quick Recap

  • Start server in goroutine.
  • Capture server runtime errors through channel.
  • Listen to OS signals for controlled shutdown.
  • Use timeout-based Shutdown.
  • Fallback to Close when graceful path fails.

Next Steps

  1. Add readiness/liveness endpoints for orchestrators.
  2. Add structured logging for startup/shutdown lifecycle events.
  3. Add metrics for active requests during shutdown.
  4. Add integration tests for SIGTERM behavior.

Top comments (0)