Introduction
When raw performance is the priority, Hyper (the HTTP library that powers Deno and AWS Lambda) combined with Tokio offers unmatched throughput. While frameworks like Gin provide excellent performance, Hyper takes you to the metalโgiving you control over every byte of HTTP processing.
This guide explores how to build ultra-fast HTTP servers using Hyper and Tokio, the foundation of modern high-performance Go services.
What Are Hyper and Tokio?
The Basic Concept
Hyper is a low-level HTTP implementation written in Rust, providing HTTP/1.1 and HTTP/2 support with excellent performance. Tokio is an async runtime for Rust that powers Hyper, providing the event loop and I/O operations.
In the Go ecosystem, while not directly using Rust, the concepts and libraries inspired by this architecture provide similar performance benefits.
Key Terms
- HTTP/1.1: The standard HTTP protocol
- HTTP/2: Multiplexed HTTP with header compression
- Async Runtime: Event loop for handling concurrent operations
- Connection: Client-server network connection
- Request/Response: HTTP messages
- Middleware: Processing layer around handlers
Why This Matters in 2025-2026
| Server | Requests/sec | Latency (p99) |
|---|---|---|
| Go net/http | 50,000 | 5ms |
| Gin | 120,000 | 2ms |
| Hyper + Tokio | 180,000+ | 0.5ms |
Architecture
How Hyper Works
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Application โ
โ (Your Handler) โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโ
โ Hyper Server โ
โ - Protocol negotiation โ
โ - Request parsing โ
โ - Response formatting โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโ
โ Tokio Runtime โ
โ - Async I/O (epoll/kqueue/IOCP) โ
โ - Task scheduling โ
โ - Timer management โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโ
โ Operating System โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Using Hyper in Go
With Go’s net/http (Hyper-inspired patterns)
While Hyper itself is Rust-based, we can achieve similar performance using Go’s standard library patterns:
package main
import (
"context"
"log"
"net/http"
"time"
)
// Custom server with optimized settings
func main() {
addr := ":8080"
// Optimized HTTP server
server := &http.Server{
Addr: addr,
Handler: router(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
// Keep-alive settings
MaxHeaderBytes: 1 << 20, // 1MB
// Disable HTTP/2 for specific use cases
// NextProtos: []string{"http/1.1"},
}
log.Printf("Server starting on %s", addr)
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
func router() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/health", healthCheck)
mux.HandleFunc("/api/users", handleUsers)
return mux
}
func healthCheck(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
}
func handleUsers(w http.ResponseWriter, r *http.Request) {
// Process request
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`[{"id":1,"name":"John"}]`))
}
Using fasthttp (Similar to Hyper performance)
package main
import (
"fmt"
"github.com/valyala/fasthttp"
)
func main() {
// fasthttp provides similar performance to Hyper
handler := func(ctx *fasthttp.RequestCtx) {
switch string(ctx.Path()) {
case "/health":
ctx.SetContentType("application/json")
ctx.WriteString(`{"status":"ok"}`)
case "/api/users":
ctx.SetContentType("application/json")
ctx.WriteString(`[{"id":1,"name":"John"}]`)
default:
ctx.Error("Not Found", fasthttp.StatusNotFound)
}
}
// High-performance server
fmt.Println("Server starting on :8080")
fasthttp.ListenAndServe(":8080", handler)
}
Building with Tokio-style Patterns in Go
Using the golang.org/x/net/http2
package main
import (
"crypto/tls"
"log"
"net/http"
"golang.org/x/net/http2"
)
func main() {
// Configure HTTP/2
server := &http.Server{
Addr: ":8443",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message":"HTTP/2 response"}`))
}),
}
// Configure HTTP/2
http2.ConfigureServer(server, &http2.Server{
MaxConcurrentStreams: 250,
MaxReadFrameSize: 1048576,
})
// TLS config
server.TLSConfig = &tls.Config{
NextProtos: []string{"h2", "http/1.1"},
}
log.Println("HTTP/2 server starting on :8443")
log.Fatal(server.ListenAndServeTLS("server.crt", "server.key"))
}
Async-style with Go Channels
package main
import (
"context"
"log"
"net/http"
"time"
)
type AsyncHandler struct {
workerChan chan func()
workers int
}
func NewAsyncHandler(workers int) *AsyncHandler {
handler := &AsyncHandler{
workerChan: make(chan func(), workers*10),
workers: workers,
}
// Start worker pool
for i := 0; i < workers; i++ {
go func() {
for work := range handler.workerChan {
work()
}
}()
}
return handler
}
func (h *AsyncHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Process in worker pool
done := make(chan bool, 1)
h.workerChan <- func() {
// Process request here
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"async":"processed"}`))
done <- true
}
// Wait with timeout
select {
case <-done:
case <-time.After(5 * time.Second):
http.Error(w, "timeout", http.StatusGatewayTimeout)
}
}
func main() {
handler := NewAsyncHandler(10)
log.Println("Async server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", handler))
}
High-Performance Patterns
Connection Pooling
type PooledTransport struct {
transport *http.Transport
idleConns chan *http.Client
maxConns int
}
func NewPooledTransport(maxConns int) *PooledTransport {
transport := &http.Transport{
MaxIdleConns: maxConns,
MaxIdleConnsPerHost: maxConns,
IdleConnTimeout: 30 * time.Second,
}
pool := &PooledTransport{
transport: transport,
idleConns: make(chan *http.Client, maxConns),
maxConns: maxConns,
}
// Pre-populate pool
for i := 0; i < maxConns; i++ {
pool.idleConns <- &http.Client{Transport: transport}
}
return pool
}
func (p *PooledTransport) GetClient() *http.Client {
select {
case client := <-p.idleConns:
return client
default:
return &http.Client{Transport: p.transport}
}
}
func (p *PooledTransport) ReturnClient(client *http.Client) {
select {
case p.idleConns <- client:
default:
// Pool full, let GC handle it
}
}
Request Pooling
// Reusable request objects
var reqPool = sync.Pool{
New: func() interface{} {
return &http.Request{
Header: make(http.Header),
}
},
}
func GetWithPooling(url string) (*http.Response, error) {
req := reqPool.Get().(*http.Request)
defer reqPool.Put(req)
// Reset request
*req = http.Request{
Method: "GET",
URL: &url.URL{},
Header: make(http.Header),
}
// Make request
client := &http.Client{}
return client.Do(req)
}
Zero-Allocation JSON
import (
"encoding/json"
"github.com/json-iterator/go"
)
var json = jsoniter.ConfigCompatibleWithStandardLibrary
// Instead of marshal, use encoder
func WriteJSON(w http.ResponseWriter, v interface{}) {
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.Encode(v)
}
// Use struct tags for zero-allocation
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// Pre-allocate for known sizes
func WriteUsers(w http.ResponseWriter, users []User) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`[`))
for i, user := range users {
if i > 0 {
w.Write([]byte(`,`))
}
data, _ := json.Marshal(user)
w.Write(data)
}
w.Write([]byte(`]`))
}
Best Practices
1. Optimize for p99 Latency
// Use buffered channels for handlers
handler := &http.Server{
Handler: mux,
// Increase read buffers
ReadHeaderTimeout: time.Second,
// Proper timeouts
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
2. Use ResponsePool
var respBufferPool = sync.Pool{
New: func() interface{} {
return make([]byte, 0, 1024) // Pre-allocate 1KB
},
}
func efficientResponse(w http.ResponseWriter) {
buf := respBufferPool.Get().(*[]byte)
defer respBufferPool.Put(buf)
*buf = (*buf)[:0]
*buf = append(*buf, `{"data":`...)
// ... build response
w.Write(*buf)
}
3. Monitor Metrics
import "github.com/prometheus/client_golang/prometheus"
var (
httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
},
[]string{"method", "endpoint", "status"},
)
httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request latency",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "endpoint"},
)
)
func init() {
prometheus.MustRegister(httpRequestsTotal)
prometheus.MustRegister(httpRequestDuration)
}
func instrumentHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap response writer to capture status
ww := &statusWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(ww, r)
duration := time.Since(start)
httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path, fmt.Sprintf("%d", ww.status)).Inc()
httpRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration.Seconds())
})
}
type statusWriter struct {
http.ResponseWriter
status int
}
func (w *statusWriter) WriteHeader(status int) {
w.status = status
w.ResponseWriter.WriteHeader(status)
}
Common Pitfalls
1. Not Setting Timeouts
Wrong:
server := &http.Server{
Addr: ":8080",
Handler: mux,
// No timeouts!
}
Correct:
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
2. Creating New Clients
Wrong:
func handler(w http.ResponseWriter, r *http.Request) {
client := http.Client{} // New client per request!
resp, _ := client.Get(url)
// ...
}
Correct:
var client = &http.Client{
Timeout: 10 * time.Second,
}
func handler(w http.ResponseWriter, r *http.Request) {
resp, _ := client.Get(url)
// ...
}
3. Ignoring Connection Limits
Wrong:
transport := &http.Transport{
// Default limits can cause issues
}
Correct:
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
MaxConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
}
External Resources
Official Documentation
Performance Resources
Related Tools
- pprof - Profiling
- grafana - Metrics
- prometheus - Monitoring
Key Takeaways
- Hyper + Tokio provide the fastest HTTP performance
- fasthttp brings similar performance to Go
- HTTP/2 improves multiplexing and efficiency
- Connection pooling reduces overhead
- Timeouts are critical for production
- Metrics help identify performance issues
- Best practice: Reuse clients, set timeouts, monitor
Next Steps: Explore Go Microservices Architecture to build scalable distributed systems.
Comments