Engineering Article

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()

2. Runtime Error Channel

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

serverErrCh := make(chan error, 1)

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")
}

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.