This episode optimizes and fixes the Echo application: reducing allocations with reuse, connection pooling, Redis caching, HTTP server tuning, and troubleshooting route conflicts, binding errors, deadlocks, goroutine leaks, and the v4 to v5 migration.

Performance isn't about chasing benchmark numbers, it's about eliminating waste. This episode combines two sides: measurable optimization to speed up the application, and troubleshooting to find the root cause when something runs slowly or goes wrong.
Episode 19 covers reducing allocations and reuse, connection pooling, Redis caching, HTTP server tuning, and troubleshooting route conflicts, binding errors, deadlocks, goroutine leaks, and the v4 to v5 migration.
Don't optimize based on guesses. Use the benchmarks from episode 17 to find the points of waste:
import _ "net/http/pprof"Then take a CPU profile under load:
go test -bench=. -benchmem -cpuprofile=cpu.out ./internal/handler/
go tool pprof -top cpu.outgo tool pprof -top cpu.out shows the functions consuming the most CPU. Optimization starts from the top rows of this list, not from speculation.
A common waste pattern: creating a new slice or buffer for every request. Use sync.Pool for objects that are frequently used and expensive to create:
var bufPool = sync.Pool{
New: func() any { return bytes.NewBuffer(make([]byte, 0, 4096)) },
}
func handler(c echo.Context) error {
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)
buf.Reset()
buf.WriteString("hasil proses yang membutuhkan buffer besar")
return c.Blob(http.StatusOK, echo.MIMETextPlain, buf.Bytes())
}sync.Pool stores objects that can be reused between requests, significantly reducing allocation and GC pressure.
Pooling isn't just for databases. Outbound HTTP clients must also be pooled so TCP connections are reused:
var httpClient = &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
IdleConnTimeout: 90 * time.Second,
},
}A single pooled global client is much faster than creating a new client per request.
Redis cuts latency for data that rarely changes. Use Redis as a cache in front of queries:
import "github.com/redis/go-redis/v9"
func getProduct(ctx context.Context, id int) (*model.Product, error) {
key := "product:" + strconv.Itoa(id)
if cached, err := rdb.Get(ctx, key).Result(); err == nil {
var p model.Product
if json.Unmarshal([]byte(cached), &p) == nil {
return &p, nil
}
}
p, err := repo.FindByID(ctx, id)
if err != nil {
return nil, err
}
data, _ := json.Marshal(p)
rdb.Set(ctx, key, data, 10*time.Minute)
return p, nil
}The cache-aside pattern: read the cache first, hit the database on a miss, then store the result with a TTL.
Episode 10 introduced a custom http.Server. Some extra tuning values for production load:
s := &http.Server{
Addr: ":" + cfg.Port,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 1 << 20,
}ReadHeaderTimeout protects against slow-loris, MaxHeaderBytes bounds the header size. Each value is chosen based on client behavior, not random numbers.
Errors when registering a duplicate route: use e.Routes() and e.Debug = true (episode 4) to see the already-registered routes before adding new ones.
A mismatched payload produces a binding error. Check the struct tags, Content-Type, and the body size limited by BodyLimit. Validation errors usually carry a field-specific message — print it for debugging.
Deadlocks often come from channels that are never sent to or consumed. Goroutine leaks appear when a context is never canceled — for example a goroutine that never receives a cancel signal. Detect them with pprof:
import _ "net/http/pprof"curl http://localhost:6060/debug/pprof/goroutine?debug=1/debug/pprof/goroutine shows all active goroutines with their stacks. Goroutines piling up in the same function are a hint of a leak.
The migration follows the official API_CHANGES_V5.md document. The main changes are explained in episode 20; the practical steps:
go get github.com/labstack/echo/v5@latest
go mod tidy
go build ./...Start by updating the imports, then fix each compile error as it appears.
Episode 19 combines optimization and fixes: benchmarks with pprof guide decisions, sync.Pool reduces allocations, pooling and Redis cut latency, server tuning adapts capacity, and troubleshooting techniques handle route conflicts, binding errors, deadlocks, goroutine leaks, and the v4 to v5 migration.
Key takeaways:
sync.Pool reuses objects and reduces allocations.API_CHANGES_V5.md step by step.In episode 20 next, we'll discuss the latest stable Echo v5 features — *echo.Context as a pointer receiver, RequestLogger with slog, a safer concurrent router, breaking API changes, and the CVE-2026-55677 fix and the v4 support policy.