Learn Chi - Performance & Troubleshooting
Series/Learn Chi/Episode 19
Episode 19 of 23

Learn Chi - Performance & Troubleshooting

This episode polishes performance: efficient route patterns, http.Server tuning, connection pooling, and caching with Redis. You will also learn to solve common problems such as route pattern conflicts, panic handlers, context deadlocks, goroutine leaks, and strategies for upgrading between chi versions.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

A correct application isn't necessarily a fast one. Episode 19 closes the technical side: how to squeeze out performance without sacrificing correctness, then how to solve the classic problems that haunt every production Go project.

The first part covers optimization: route patterns, server tuning, pooling, and caching. The second part covers troubleshooting: symptoms, root causes, and solutions for the five most common problems in the chi ecosystem.

Efficient Route Patterns

Structure That Helps the Router

chi's radix tree works best with regular patterns:

Efficient route patterns
r.Get("/users", listUsers)
r.Get("/users/{id:[0-9]+}", getUser)
r.Post("/users", createUser)

{id:[0-9]+} is more efficient than a free param for numeric identifiers — the regex is more specific, so fewer candidates are tried. Avoid the {path:*} wildcard on frequently called routes unless it's really needed.

Avoid Ambiguity

Two routes that could both match the same request slow down matching and cause confusion. Make patterns as specific as possible and use subrouters to group paths that share a prefix — the radix tree shares nodes automatically.

Tuning http.Server

Timeouts and Header Limits

Server configuration provides load protection:

Tune http.Server
srv := &http.Server{
    Addr:         ":8080",
    Handler:      r,
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
    IdleTimeout:  120 * time.Second,
    MaxHeaderBytes: 1 << 20,
}

ReadTimeout limits the time to read a request; WriteTimeout limits response writing. MaxHeaderBytes prevents giant headers. These values reduce the odds of slow attacks and hanging connections.

Why Timeouts Matter

Without timeouts, a slow connection can hold a goroutine forever — each connection consumes a goroutine, and hanging goroutines drain memory. The values above are a reasonable starting point for a JSON API.

Connection Pooling

Tuning the Database Pool

The pool you've used since episode 9 needs to be set for real load:

Pool configuration
pool.Config().MaxConns = 20
pool.Config().MinConns = 5
pool.Config().MaxConnLifetime = time.Hour
pool.Config().MaxConnIdleTime = 5 * time.Minute

MaxConns caps the maximum parallel connections to the database; MinConns keeps ready-to-use connections. MaxConnLifetime forces connection rotation so stale connections aren't reused. The right size depends on the database spec.

Caching with Redis

Caching Hot Data

Frequently requested responses shouldn't be recomputed:

Install Redis client
go get github.com/redis/go-redis/v9
Cache with Redis
client := redis.NewClient(&redis.Options{
    Addr: "localhost:6379",
})
 
func cacheOrFetch(w http.ResponseWriter, req *http.Request) {
    ctx := req.Context()
 
    val, err := client.Get(ctx, "popular").Result()
    if err == nil {
        writeJSON(w, http.StatusOK, val)
        return
    }
 
    data := fetchSlowData(ctx)
    client.Set(ctx, "popular", data, time.Minute)
    writeJSON(w, http.StatusOK, data)
}

client.Get(ctx, "popular") reads the cache first; if it's empty, the data is computed and stored with client.Set(ctx, "popular", data, time.Minute). The one-minute TTL keeps the cache from going stale forever.

When Caching Is Wrong

Don't cache sensitive per-user data under a global key, and don't cache rapidly changing data without invalidation. The best caches are for data that rarely changes and is expensive to compute.

Common Troubleshooting

Route Pattern Conflicts

Symptom: a route is registered but never matches, or a panic at startup. The fix: chi's radix tree forbids two ambiguous patterns at the same position. Check the order — literals must win over params, and don't register two wildcards in the same segment.

Uncaught Panics

Symptom: the server dies suddenly without logs. The fix: put middleware.Recoverer at the top of your r.Use calls:

Recoverer as a safety net
r.Use(middleware.RequestID)
r.Use(middleware.Recoverer)

middleware.Recoverer prints a stack trace to the log — the primary clue for finding the panicking line.

Context Deadlocks

Symptom: a handler hangs, timeouts never fire. Cause: a context isn't forwarded, or a channel waits without a select on ctx.Done(). The fix: always select with <-ctx.Done() (episode 12) and forward req.Context() to every operation.

Goroutine Leaks

Symptom: memory keeps rising even under stable load. Cause: goroutines without an exit path, for example a go func() writing to an unbuffered channel with no reader. The fix:

Prevent goroutine leaks
go func() {
    select {
    case ch <- result:
    case <-ctx.Done():
    }
}()

select { case ch <- result: case <-ctx.Done(): } gives the goroutine an exit path when the request is cancelled. Monitor with pprof go tool pprof to find hanging goroutines.

Upgrading Between chi Versions

Symptom: old code breaks after go get of a new version. The fix: read the release notes, run tests first, and upgrade gradually:

Upgrade chi
go get github.com/go-chi/chi/v5@latest
go mod tidy
go test ./...

go get github.com/go-chi/chi/v5@latest followed by go test ./... ensures the upgrade doesn't break behavior. We cover per-version feature details in episode 20.

Conclusion

Key takeaways:

  • Specific regexes and subrouters help the radix tree match fast.
  • ReadTimeout and WriteTimeout protect against slow requests.
  • Tune the pool: MaxConns, MinConns, and MaxConnLifetime.
  • Redis caching cuts latency for data that rarely changes.
  • Route conflicts are prevented with unambiguous patterns.
  • Recoverer, ctx.Done, and pprof solve panics, deadlocks, and leaks.

In the next episode 20 we look at the latest: the newest stable features in chi v5.2 and v5.3 — support for the last four Go versions, middleware.Discard and the RedirectSlashes fix in v5.2, and inline middleware on subrouters, XML support in the default compressible types, the io/ioutil replacement, and Go 1.26 CI in v5.3.

Learn Chi - Performance & Troubleshooting | Learn Chi