Loading documentation...
Loading documentation...
Loading documentation...
This guide covers performance optimization techniques for Helix applications.
Helix is designed for high performance out of the box:
Helix uses sync.Pool for frequently created objects:
The router uses a radix tree (compressed trie) for O(k) route matching where k is the path length:
Root
āāā users (static)
ā āāā / (GET handler)
ā āāā {id} (param)
ā āāā / (GET handler)
āāā posts (static)
āāā {id} (param)Call Build() after registering all routes to pre-compile the middleware chain:
s := helix.New(nil)
// Register routes and middleware
s.Use(middleware.RequestID())
s.Use(middleware.Logger(middleware.LogFormatJSON))
s.Use(middleware.Recover())
s.GET("/users", helix.HandleCtx(listUsers))
s.POST("/users", helix.HandleCtx(createUser))
// Pre-compile for production
s.Build()
s.Start(":8080")HandleCtx is optimized for common use cases with minimal overhead:
s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
id := c.Param("id")
return c.OK(user)
}))// ā
Better - uses typed accessor
id, err := c.ParamInt("id")
// ā Worse - parses twice
idStr := c.Param("id")
id, _ := strconv.Atoi(idStr)// QuerySlice returns existing slice when possible
tags := c.QuerySlice("tags")Struct reflection information is cached automatically:
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
// First call: reflects and caches struct info
// Subsequent calls: uses cached info
req, err := helix.Bind[CreateUserRequest](r)Use Skip functions to bypass middleware for specific routes:
s.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: middleware.LogFormatJSON,
Skip: func(r *http.Request) bool {
// Don't log health checks
return r.URL.Path == "/health"
},
}))Configure appropriate timeouts for your use case:
s := helix.New(&helix.Options{
ReadTimeout: 30 * time.Second, // Max time to read request
WriteTimeout: 30 * time.Second, // Max time to write response
IdleTimeout: 120 * time.Second, // Keep-alive timeout
GracePeriod: 30 * time.Second, // Shutdown grace period
})For high-traffic scenarios, consider connection limits at the load balancer level rather than in the application.
Compression reduces bandwidth but increases CPU usage:
// Enable compression for large responses
s.Use(middleware.CompressWithConfig(middleware.CompressConfig{
Level: gzip.DefaultCompression,
MinSize: 1024, // Only compress responses > 1KB
}))Token bucket rate limiting is efficient but adds overhead:
// Only enable where needed
api := s.Group("/api")
api.Use(middleware.RateLimit(100, 10)) // 100 req/sec, burst 10func BenchmarkListUsers(b *testing.B) {
s := helix.New(nil)
s.GET("/users", helix.HandleCtx(listUsers))
req := httptest.NewRequest(http.MethodGet, "/users", nil)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
rec := httptest.NewRecorder()
s.ServeHTTP(rec, req)
}
}Run benchmarks with:
go test -bench=. -benchmem -benchtime=5sBenchmarkListUsers-8 500000 2450 ns/op 256 B/op 4 allocs/op
# Before changes
go test -bench=. -benchmem > before.txt
# After changes
go test -bench=. -benchmem > after.txt
# Compare
benchstat before.txt after.txtUse the profiling middleware (requires build tag):
//go:build profiling
s.Use(middleware.Profiling("/debug/pprof"))Or use net/http/pprof directly:
import _ "net/http/pprof"
// Profiles available at /debug/pprof/# Collect heap profile
curl http://localhost:8080/debug/pprof/heap > heap.prof
# Analyze
go tool pprof heap.prof# Collect trace
curl http://localhost:8080/debug/pprof/trace?seconds=5 > trace.out
# View trace
go tool trace trace.outCause: Unbounded request body reading
Solution: Limit body size
s.Use(middleware.BodyLimit(1024 * 1024)) // 1MB limitCause: Too many routes with overlapping patterns
Solution: Organize routes with groups
// Better organization
api := s.Group("/api/v1")
api.Mount("/users", &UserModule{})
api.Mount("/posts", &PostModule{})Cause: Not closing response bodies in HTTP clients
Solution: Always close response bodies
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()Cause: Context not respected in background operations
Solution: Always check context cancellation
select {
case <-ctx.Done():
return ctx.Err()
case result := <-resultChan:
return result, nil
}s.Build() before starting the serverThis documentation should be updated by package maintainers to reflect the actual architecture and design patterns used.
This guide covers performance optimization techniques for Helix applications.
Helix is designed for high performance out of the box:
Helix uses sync.Pool for frequently created objects:
The router uses a radix tree (compressed trie) for O(k) route matching where k is the path length:
Root
āāā users (static)
ā āāā / (GET handler)
ā āāā {id} (param)
ā āāā / (GET handler)
āāā posts (static)
āāā {id} (param)Call Build() after registering all routes to pre-compile the middleware chain:
s := helix.New(nil)
// Register routes and middleware
s.Use(middleware.RequestID())
s.Use(middleware.Logger(middleware.LogFormatJSON))
s.Use(middleware.Recover())
s.GET("/users", helix.HandleCtx(listUsers))
s.POST("/users", helix.HandleCtx(createUser))
// Pre-compile for production
s.Build()
s.Start(":8080")HandleCtx is optimized for common use cases with minimal overhead:
s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
id := c.Param("id")
return c.OK(user)
}))// ā
Better - uses typed accessor
id, err := c.ParamInt("id")
// ā Worse - parses twice
idStr := c.Param("id")
id, _ := strconv.Atoi(idStr)// QuerySlice returns existing slice when possible
tags := c.QuerySlice("tags")Struct reflection information is cached automatically:
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
// First call: reflects and caches struct info
// Subsequent calls: uses cached info
req, err := helix.Bind[CreateUserRequest](r)Use Skip functions to bypass middleware for specific routes:
s.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: middleware.LogFormatJSON,
Skip: func(r *http.Request) bool {
// Don't log health checks
return r.URL.Path == "/health"
},
}))Configure appropriate timeouts for your use case:
s := helix.New(&helix.Options{
ReadTimeout: 30 * time.Second, // Max time to read request
WriteTimeout: 30 * time.Second, // Max time to write response
IdleTimeout: 120 * time.Second, // Keep-alive timeout
GracePeriod: 30 * time.Second, // Shutdown grace period
})For high-traffic scenarios, consider connection limits at the load balancer level rather than in the application.
Compression reduces bandwidth but increases CPU usage:
// Enable compression for large responses
s.Use(middleware.CompressWithConfig(middleware.CompressConfig{
Level: gzip.DefaultCompression,
MinSize: 1024, // Only compress responses > 1KB
}))Token bucket rate limiting is efficient but adds overhead:
// Only enable where needed
api := s.Group("/api")
api.Use(middleware.RateLimit(100, 10)) // 100 req/sec, burst 10func BenchmarkListUsers(b *testing.B) {
s := helix.New(nil)
s.GET("/users", helix.HandleCtx(listUsers))
req := httptest.NewRequest(http.MethodGet, "/users", nil)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
rec := httptest.NewRecorder()
s.ServeHTTP(rec, req)
}
}Run benchmarks with:
go test -bench=. -benchmem -benchtime=5sBenchmarkListUsers-8 500000 2450 ns/op 256 B/op 4 allocs/op
# Before changes
go test -bench=. -benchmem > before.txt
# After changes
go test -bench=. -benchmem > after.txt
# Compare
benchstat before.txt after.txtUse the profiling middleware (requires build tag):
//go:build profiling
s.Use(middleware.Profiling("/debug/pprof"))Or use net/http/pprof directly:
import _ "net/http/pprof"
// Profiles available at /debug/pprof/# Collect heap profile
curl http://localhost:8080/debug/pprof/heap > heap.prof
# Analyze
go tool pprof heap.prof# Collect trace
curl http://localhost:8080/debug/pprof/trace?seconds=5 > trace.out
# View trace
go tool trace trace.outCause: Unbounded request body reading
Solution: Limit body size
s.Use(middleware.BodyLimit(1024 * 1024)) // 1MB limitCause: Too many routes with overlapping patterns
Solution: Organize routes with groups
// Better organization
api := s.Group("/api/v1")
api.Mount("/users", &UserModule{})
api.Mount("/posts", &PostModule{})Cause: Not closing response bodies in HTTP clients
Solution: Always close response bodies
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()Cause: Context not respected in background operations
Solution: Always check context cancellation
select {
case <-ctx.Done():
return ctx.Err()
case result := <-resultChan:
return result, nil
}s.Build() before starting the serverThis documentation should be updated by package maintainers to reflect the actual architecture and design patterns used.
Root
āāā users (static)
ā āāā / (GET handler)
ā āāā {id} (param)
ā āāā / (GET handler)
āāā posts (static)
āāā {id} (param)s := helix.New(nil)
// Register routes and middleware
s.Use(middleware.RequestID())
s.Use(middleware.Logger(middleware.LogFormatJSON))
s.Use(middleware.Recover())
s.GET("/users", helix.HandleCtx(listUsers))
s.POST("/users", helix.HandleCtx(createUser))
// Pre-compile for production
s.Build()
s.Start(":8080")s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
id := c.Param("id")
return c.OK(user)
}))// ā
Better - uses typed accessor
id, err := c.ParamInt("id")
// ā Worse - parses twice
idStr := c.Param("id")
id, _ := strconv.Atoi(idStr)// QuerySlice returns existing slice when possible
tags := c.QuerySlice("tags")type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
// First call: reflects and caches struct info
// Subsequent calls: uses cached info
req, err := helix.Bind[CreateUserRequest](r)s.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: middleware.LogFormatJSON,
Skip: func(r *http.Request) bool {
// Don't log health checks
return r.URL.Path == "/health"
},
}))s := helix.New(&helix.Options{
ReadTimeout: 30 * time.Second, // Max time to read request
WriteTimeout: 30 * time.Second, // Max time to write response
IdleTimeout: 120 * time.Second, // Keep-alive timeout
GracePeriod: 30 * time.Second, // Shutdown grace period
})// Enable compression for large responses
s.Use(middleware.CompressWithConfig(middleware.CompressConfig{
Level: gzip.DefaultCompression,
MinSize: 1024, // Only compress responses > 1KB
}))// Only enable where needed
api := s.Group("/api")
api.Use(middleware.RateLimit(100, 10)) // 100 req/sec, burst 10func BenchmarkListUsers(b *testing.B) {
s := helix.New(nil)
s.GET("/users", helix.HandleCtx(listUsers))
req := httptest.NewRequest(http.MethodGet, "/users", nil)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
rec := httptest.NewRecorder()
s.ServeHTTP(rec, req)
}
}go test -bench=. -benchmem -benchtime=5s# Before changes
go test -bench=. -benchmem > before.txt
# After changes
go test -bench=. -benchmem > after.txt
# Compare
benchstat before.txt after.txt//go:build profiling
s.Use(middleware.Profiling("/debug/pprof"))import _ "net/http/pprof"
// Profiles available at /debug/pprof/# Collect heap profile
curl http://localhost:8080/debug/pprof/heap > heap.prof
# Analyze
go tool pprof heap.prof# Collect trace
curl http://localhost:8080/debug/pprof/trace?seconds=5 > trace.out
# View trace
go tool trace trace.outs.Use(middleware.BodyLimit(1024 * 1024)) // 1MB limit// Better organization
api := s.Group("/api/v1")
api.Mount("/users", &UserModule{})
api.Mount("/posts", &PostModule{})resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()select {
case <-ctx.Done():
return ctx.Err()
case result := <-resultChan:
return result, nil
}Root
āāā users (static)
ā āāā / (GET handler)
ā āāā {id} (param)
ā āāā / (GET handler)
āāā posts (static)
āāā {id} (param)s := helix.New(nil)
// Register routes and middleware
s.Use(middleware.RequestID())
s.Use(middleware.Logger(middleware.LogFormatJSON))
s.Use(middleware.Recover())
s.GET("/users", helix.HandleCtx(listUsers))
s.POST("/users", helix.HandleCtx(createUser))
// Pre-compile for production
s.Build()
s.Start(":8080")s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
id := c.Param("id")
return c.OK(user)
}))// ā
Better - uses typed accessor
id, err := c.ParamInt("id")
// ā Worse - parses twice
idStr := c.Param("id")
id, _ := strconv.Atoi(idStr)// QuerySlice returns existing slice when possible
tags := c.QuerySlice("tags")type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
// First call: reflects and caches struct info
// Subsequent calls: uses cached info
req, err := helix.Bind[CreateUserRequest](r)s.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: middleware.LogFormatJSON,
Skip: func(r *http.Request) bool {
// Don't log health checks
return r.URL.Path == "/health"
},
}))s := helix.New(&helix.Options{
ReadTimeout: 30 * time.Second, // Max time to read request
WriteTimeout: 30 * time.Second, // Max time to write response
IdleTimeout: 120 * time.Second, // Keep-alive timeout
GracePeriod: 30 * time.Second, // Shutdown grace period
})// Enable compression for large responses
s.Use(middleware.CompressWithConfig(middleware.CompressConfig{
Level: gzip.DefaultCompression,
MinSize: 1024, // Only compress responses > 1KB
}))// Only enable where needed
api := s.Group("/api")
api.Use(middleware.RateLimit(100, 10)) // 100 req/sec, burst 10func BenchmarkListUsers(b *testing.B) {
s := helix.New(nil)
s.GET("/users", helix.HandleCtx(listUsers))
req := httptest.NewRequest(http.MethodGet, "/users", nil)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
rec := httptest.NewRecorder()
s.ServeHTTP(rec, req)
}
}go test -bench=. -benchmem -benchtime=5s# Before changes
go test -bench=. -benchmem > before.txt
# After changes
go test -bench=. -benchmem > after.txt
# Compare
benchstat before.txt after.txt//go:build profiling
s.Use(middleware.Profiling("/debug/pprof"))import _ "net/http/pprof"
// Profiles available at /debug/pprof/# Collect heap profile
curl http://localhost:8080/debug/pprof/heap > heap.prof
# Analyze
go tool pprof heap.prof# Collect trace
curl http://localhost:8080/debug/pprof/trace?seconds=5 > trace.out
# View trace
go tool trace trace.outs.Use(middleware.BodyLimit(1024 * 1024)) // 1MB limit// Better organization
api := s.Group("/api/v1")
api.Mount("/users", &UserModule{})
api.Mount("/posts", &PostModule{})resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()select {
case <-ctx.Done():
return ctx.Err()
case result := <-resultChan:
return result, nil
}