Skip to content

Performance Optimization

HTTPC is designed for high performance: connection-pool reuse, HTTP/2 multiplexing, object pooling, and single-allocation result objects. In most scenarios, using a preset configuration directly delivers excellent performance; when further tuning is needed, understanding the underlying mechanics lets you target the right lever.

Preset Comparison

HTTPC provides 5 preset configurations, each systematically tuned for a different scenario. The tables below list the exact values of key fields by category, so you can compare and choose.

Timeout Configuration

FieldDefaultSecurePerformanceTestingMinimal
Timeouts.Request180s15s60s180s180s
Timeouts.Dial10s5s15s5s5s
Timeouts.TLSHandshake10s5s15s5s5s
Timeouts.ResponseHeader0 (disabled)10s0 (disabled)0 (disabled)0 (disabled)
Timeouts.IdleConn90s30s120s30s30s

Connection Configuration

FieldDefaultSecurePerformanceTestingMinimal
MaxIdleConns50201001010
MaxConnsPerHost1052052
EnableHTTP2OnOnOnOffOn
EnableCookiesOffOffOnOnOff
EnableDoHOffOffOffOffOff

Security Configuration

FieldDefaultSecurePerformanceTestingMinimal
MaxResponseBodySize10MB5MB50MB10MB1MB
MaxDecompressedBodySize100MB100MB100MB100MB100MB
ValidateURLOnOnOnOffOn
ValidateHeadersOnOnOnOffOn
StrictContentLengthOnOnOffOnOn
AllowPrivateIPsfalsefalsefalsetruefalse
InsecureSkipVerifyfalsefalsefalsetruefalse

Retry Configuration

FieldDefaultSecurePerformanceTestingMinimal
MaxRetries31310
Delay1s2s500ms100ms0
BackoffFactor2.02.01.52.01.0
MaxRetryDelay30s30s30s30s30s
EnableJitterOnOnOnOffOff

Request Defaults

FieldDefaultSecurePerformanceTestingMinimal
FollowRedirectsOnOffOnOnOff
MaxRedirects1010101010
UserAgenthttpc/1.0httpc/1.0httpc/1.0httpc-test/1.0httpc/1.0

Never use TestingConfig in production

TestingConfig() disables URL/header validation, TLS certificate verification, and SSRF protection — it is for local development and testing only. It emits a security warning when called outside a test environment. Use SecureConfig() or DefaultConfig() in production.

Scenario Selection

ScenarioRecommended PresetAdjustment Suggestion
General web servicesDefault
Handling user-provided URLsSecure
Internal microservices, high concurrencyPerformanceIncrease MaxIdleConns per backend count
One-off scriptsMinimal
File download servicePerformanceIncrease MaxResponseBodySize
Financial/medical APIsSecure + customAdd audit middleware
Local dev / unit testingTestingNever deploy to production
go
// High throughput scenario — use the preset directly
client, _ := httpc.New(httpc.PerformanceConfig())

// Fine-tune individual fields on top of the preset
cfg := httpc.PerformanceConfig()
cfg.Timeouts.Request = 120 * time.Second
cfg.Connection.MaxIdleConns = 200
client, _ := httpc.New(cfg)

Connection Pool Tuning Internals

The connection pool is the core of HTTP client performance. HTTPC's pool is built on the Go standard library's http.Transport, but adds auto-computation logic and secure defaults on top.

Idle Connection Auto-Computation

MaxIdleConnsPerHost (the per-host idle connection cap) does not need to be set manually — HTTPC derives it from MaxConnsPerHost:

idle conns = MaxConnsPerHost / 2, clamped to [2, 10]

Specific rules (calculateIdleConnsPerHost):

MaxConnsPerHostAuto idle connsNotes
0 (unlimited)10Uses the upper-bound default
12No lower than the floor
22Exactly the floor
52Half rounds down to floor
105Default preset
2010Performance preset, hits the cap
10010Above the cap, uses 10

Why MaxConnsPerHost / 2

Idle connections are a "cache of connections" — established but currently unused. Setting it to half the max connections strikes a balance between "reuse existing connections" (cache hit) and "create new connections" (re-handshake on cache miss), avoiding idle connections consuming too many server-side resources.

TCP Keep-Alive

HTTPC's connection pool uses a fixed 30-second TCP keep-alive interval (defaultKeepAlive = 30 * time.Second). After a connection is established, the OS periodically sends keep-alive probes to detect dead connections. The IdleConn timeout controls how long idle connections live in the pool (90s for Default); the two work together.

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

func main() {
    // High-QPS microservice scenario: enlarge the pool
    cfg := httpc.PerformanceConfig()
    cfg.Connection.MaxIdleConns = 200   // Global idle connection cap
    cfg.Connection.MaxConnsPerHost = 50 // Max conns per host (idle auto-computed to 10)
    cfg.Timeouts.IdleConn = 300 * time.Second // Idle conns live longer, higher reuse rate

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Hot-path requests reuse pooled connections directly
    for i := 0; i < 100; i++ {
        result, err := client.Get("https://api.example.com/data")
        if err != nil {
            log.Printf("request %d failed: %v", i, err)
            continue
        }
        fmt.Printf("request %d: %d\n", i, result.StatusCode())
    }
}

HTTP/2 Performance Advantages

HTTP/2 is enabled by default (EnableHTTP2 = true), providing three major performance gains:

FeatureHTTP/1.1HTTP/2
MultiplexingOne connection per requestMultiple requests share one connection
Header compressionPlaintext, resent every timeHPACK-compressed headers
Connection reuseKeep-alive, serialParallel streams

HTTP/2 and the connection pool

HTTP/2 multiplexing lets a single TCP connection carry multiple simultaneous requests, drastically reducing connection-setup overhead. Under high concurrency to the same host, HTTP/2 throughput far exceeds HTTP/1.1. It only falls back to HTTP/1.1 when using TestingConfig() (which explicitly disables HTTP/2) or when the connection does not support ALPN negotiation.

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

func main() {
    // HTTP/2 is enabled by default
    cfg := httpc.DefaultConfig()
    cfg.Connection.EnableHTTP2 = true // true by default; explicit for clarity

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Concurrent requests to an HTTP/2-capable host (most CDNs/cloud services)
    // A single TCP connection is reused; no new connection per request
    start := time.Now()
    for i := 0; i < 10; i++ {
        result, err := client.Get("https://http2.golang.org/")
        if err != nil {
            log.Printf("request %d failed: %v", i, err)
            continue
        }
        // Proto() returns the protocol version, e.g. "HTTP/2.0"
        fmt.Printf("request %d: %s, status %d\n", i, result.Proto(), result.StatusCode())
    }
    fmt.Printf("10 requests took: %v\n", time.Since(start))
}

Memory Optimization Mechanisms

HTTPC applies multiple layers of memory optimization; the core idea is to reduce heap allocations and reuse objects.

resultBundle Single Allocation

Every request returns a *Result carrying three nested structs: RequestInfo (request info), ResponseInfo (response info), and RequestMeta (metadata such as duration). The traditional approach allocates separately for Result and each nested struct — four heap allocations. HTTPC packs them into a single resultBundle, so one heap allocation covers everything:

Traditional: 4 separate allocations (Result + RequestInfo + ResponseInfo + RequestMeta)
HTTPC: 1 allocation (resultBundle); Result's three pointers reference the same memory block

The caller receives a *Result whose Request, Response, and Meta fields (pointers) reference the corresponding structs inside the bundle — fully transparent. Because the caller may hold *Result long-term, an object pool is not appropriate here (pooling would cause data races); instead it is reclaimed by GC.

Engine Object Pool

HTTPC's engine layer makes extensive use of sync.Pool to reuse short-lived objects, reducing GC pressure:

Pooled objectPurposeNotes
engine.ResponseResponse objectReturned to pool after request, reused next time
engine.RequestRequest objectSame as above
strings.BuilderString buildingURL building, error formatting, Config serialization
http.HeaderHTTP header mapRequest/response header processing
bytes.BufferJSON/multipart encodingPre-allocated by initial capacity
time.TimerRetry timerAvoids frequent timer creation
gzip/flate readerDecompressionReuses decompressors

Division of labor: object pool vs resultBundle

Engine-internal objects (Response/Request/Builder) have short lifetimes and complete their borrow-return cycle within a single request, so they are good fits for pooling. The *Result returned to the caller has an unpredictable lifetime, so it uses single allocation + GC reclamation. The two complement each other, each playing to its strength.

You Don't Need to Worry About This

The optimizations above are fully transparent to the caller. You just use the API normally; connection reuse, object pooling, and single allocation all happen automatically:

go
package main

import (
    "fmt"
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    client, err := httpc.NewDefault()
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Result is created fresh per request, reclaimed by GC — no manual release
    result, err := client.Get("https://api.example.com/data")
    if err != nil {
        log.Fatal(err)
    }

    // On hot paths prefer RawBody() over Body()
    // RawBody() returns the raw byte slice; Body() returns a pre-stored string; String() is debug formatting (highest overhead)
    data := result.RawBody()
    fmt.Printf("response size: %d bytes\n", len(data))
    fmt.Printf("request duration: %v\n", result.Meta.Duration)
}

Workload Tuning Examples

AI API Long Polling

AI inference APIs can take minutes to respond, so you need to relax timeout limits:

go
// An AI API may need 5–15 minutes to respond — don't get cut off by the default 180s timeout
result, err := httpc.Post("https://api.ai.example.com/v1/completions",
    httpc.WithJSON(payload),
    httpc.WithTimeout(900*time.Second), // 15 minutes
)

Why Default's ResponseHeader is 0

Timeouts.ResponseHeader = 0 means the transport layer does not enforce a response-header timeout; the context-level timeout (Timeouts.Request or WithTimeout) controls everything. This ensures WithTimeout() has full control over long-running responses. For transport-layer defense against slowloris attacks, use SecureConfig() (which sets it to 10s).

Microservice High QPS

High-frequency calls between internal microservices need a large connection pool:

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.PerformanceConfig()
    // Tune the pool to the number of backend instances
    cfg.Connection.MaxIdleConns = 300   // Total idle connections
    cfg.Connection.MaxConnsPerHost = 30 // Per backend instance
    // Microservice responses are usually fast — shorten timeouts for fast failure
    cfg.Timeouts.Request = 10 * time.Second
    cfg.Retry.Delay = 200 * time.Millisecond
    cfg.Retry.BackoffFactor = 2.0
    cfg.Retry.MaxRetries = 2

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    start := time.Now()
    // High-frequency requests reuse the pool — no TCP/TLS re-establishment
    for i := 0; i < 50; i++ {
        result, err := client.Get("http://user-service:8080/api/users")
        if err != nil {
            log.Printf("request %d failed: %v", i, err)
            continue
        }
        _ = result
    }
    fmt.Printf("50 requests took: %v\n", time.Since(start))
}

Large File Download (Streaming)

When downloading large files, use WithStreamBody(true) to avoid loading the entire response into memory; combine with the Download() method for resumable transfers:

go
package main

import (
    "context"
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.PerformanceConfig()
    cfg.Security.MaxResponseBodySize = 500 * 1024 * 1024 // 500MB cap

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    dcfg := httpc.DefaultDownloadConfig()
    dcfg.FilePath = "/tmp/large-file.zip"
    dcfg.ResumeDownload = true // Resumable download

    result, err := client.Download(
        context.Background(),
        "https://example.com/large-file.zip",
        dcfg,
    )
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("download complete: %d bytes", result.BytesWritten)
}

Crawler and Proxy Pool

Crawler scenarios rotate IPs via a proxy pool. HTTPC automatically raises the retry count to ensure each proxy is tried at least once (see Retry and Fault Tolerance):

go
cfg := httpc.DefaultConfig()
cfg.Connection.ProxyPool = []string{
    "http://proxy1:8080",
    "http://proxy2:8080",
    "http://proxy3:8080",
    "http://proxy4:8080",
    "http://proxy5:8080",
}
cfg.Connection.ProxyRotateOnStatus = []int{403} // 403 triggers proxy rotation
cfg.Connection.ProxyPoolStrategy = httpc.ProxyStrategyRoundRobin
// MaxRetries is automatically raised to 4 (proxy count - 1), ensuring all 5 proxies are tried

Performance Anti-Patterns

Anti-PatternCauseCorrect Approach
Creating a Client per requestConnections can't be reused; TCP/TLS handshake every timeReuse a single Client instance globally
Overly large MaxResponseBodySizeNeedlessly raises the memory ceilingSet it to the actual response size
Using result.String() on hot pathsExtra string-building overheadUse result.Body() or result.RawBody()
Connection pool too smallHigh concurrency exhausts connections; requests queueTune MaxConnsPerHost to concurrency
Disabling HTTP/2Degrades to serial HTTP/1.1 requestsKeep it on by default
Ignoring Close()Connection leakdefer client.Close()
Globally shared but not reusedRepeatedly creating/destroying the ClientCreate once, hold long-term

The Client must be reused

The foundation of HTTP performance is connection reuse. Creating a Client per request means going through the TCP three-way handshake + TLS handshake every time — latency jumps from sub-millisecond to tens of milliseconds. In microservice scenarios, inject the Client as a singleton into the service struct so it lives as long as the service.

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

// Anti-pattern demo: creating a Client per request
func main() {
    start := time.Now()

    for i := 0; i < 5; i++ {
        // WRONG: new Client every iteration — connections can't be reused
        client, err := httpc.NewDefault()
        if err != nil {
            log.Fatal(err)
        }
        result, err := client.Get("https://httpbin.org/get")
        client.Close() // Closed every time; pool drained
        if err != nil {
            log.Printf("request %d failed: %v", i, err)
            continue
        }
        _ = result
    }
    // 5 requests take far longer than the reuse approach
    fmt.Printf("anti-pattern took: %v\n", time.Since(start))

    // CORRECT: reuse the Client
    client, err := httpc.NewDefault()
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    start = time.Now()
    for i := 0; i < 5; i++ {
        result, err := client.Get("https://httpbin.org/get")
        if err != nil {
            log.Printf("request %d failed: %v", i, err)
            continue
        }
        _ = result
    }
    fmt.Printf("reuse pattern took: %v\n", time.Since(start))
}

Next Steps