Choosing between go net/http vs resty llm api clients in Go is less about features and more about how much boilerplate you want to own. Both can hit an OpenAI-compatible endpoint, stream tokens, and forward custom headers, but they diverge sharply in retry semantics, dependency footprint, and control over the request lifecycle.
Capabilities
At the wire level, both libraries are the same engine: net/http is the transport, and resty is a fluent wrapper around it. For a non-streaming chat completion, resty gives you automatic JSON encoding of the request body and decoding of the response, plus query parameter building and header chaining. net/http makes you marshal the body, set Content-Type, and decode the response manually.
Where LLM APIs get weird is streaming. The server replies with text/event-stream, and you must read resp.Body incrementally, parse SSE frames, and handle partial JSON. Neither library parses SSE for you. With net/http you get http.Response and an io.ReadCloser directly. With resty you must call SetDoNotParseResponse(true) to bypass its automatic consumption of the body, then read resp.RawBody().
An inference gateway such as n4n.ai honors client routing directives and forwards provider cache-control hints via headers; both libraries set those equally well, though net/http forces explicit Header.Set calls while resty collects them in a fluent chain.
Price and Dependency Cost
Neither library costs money. Both are MIT-licensed. The real cost is dependency surface and accidental spend.
net/http ships in the standard library. Zero external modules, zero supply-chain risk, no version drift.
resty is github.com/go-resty/resty/v2. In practice it has no mandatory third-party transitive dependencies—it is pure Go—but it still adds a package to your module graph and tens of kilobytes to a statically linked binary. That is negligible for a service, but matters if you are building a tiny CLI or a WASI plugin.
The stealth cost is retries. resty has built-in retry with SetRetryCount and default backoff. If you enable it naively, it may retry on HTTP 429 or 500 by re-sending the same completion request. Because LLM calls are not idempotent (different sampling, same token cost), a blind retry doubles spend. net/http forces you to write the retry loop, which makes the idempotency decision explicit.
Latency and Throughput
For a request/response completion, resty’s reflection-based binding adds a small allocation overhead—typically sub-millisecond on modern hardware. Not worth worrying about.
For long responses (e.g., 8K token completions), resty by default reads the entire body into memory and runs json.Unmarshal on it. net/http lets you stream-decode with json.Decoder or just scan the SSE stream, keeping memory flat. Under high concurrency proxying to an LLM, that difference becomes a GC pressure issue.
Both share the same http.Transport connection pooling, so keep-alive and HTTP/2 multiplexing are identical once you configure the client.
Ergonomics and Developer Experience
Here is a minimal non-streaming call with net/http:
body, _ := json.Marshal(map[string]any{
"model": "gpt-4o-mini",
"messages": []map[string]string{{"role": "user", "content": "hi"}},
})
req, _ := http.NewRequest("POST", "https://api.example.com/v1/chat/completions", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+os.Getenv("KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
// handle
}
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
With resty:
resp, err := resty.New().R().
SetHeader("Authorization", "Bearer "+os.Getenv("KEY")).
SetBody(map[string]any{
"model": "gpt-4o-mini",
"messages": []map[string]string{{"role": "user", "content": "hi"}},
}).
Post("https://api.example.com/v1/chat/completions")
The resty version is shorter and the intent is clearer. For a one-off script, that matters.
Streaming flips the equation. With net/http you already have the body; with resty you must remember the SetDoNotParseResponse flag or you will block on a never-ending response:
resp, err := resty.New().R().
SetDoNotParseResponse(true).
SetBody(streamReq).
Post(url)
if err != nil { /* ... */ }
defer resp.RawBody().Close()
scanner := bufio.NewScanner(resp.RawBody())
for scanner.Scan() {
// parse SSE line
}
Ecosystem and Community
net/http is the lingua franca. Every observability tracer, httptest server, and middleware framework targets it. If you need OpenTelemetry instrumentation on LLM calls, you wrap http.RoundTripper. If you need to record golden-file tests, httptest.ResponseRecorder is native.
resty has a decent following in CLI tools and small services, but you will occasionally fight its opinionated response handling when integrating with lower-level middleware. Its middleware model exists (resty.OnAfterResponse) but is less universal than RoundTripper.
Limits and Sharp Edges
restydefault timeout is zero (no timeout) unless you setSetTimeout.net/httpalso has no default timeout onDefaultClient, but the pattern of constructing ahttp.Client{Timeout: ...}is more idiomatic and visible.restyretries on network errors and, by default, on5xxif you set retry count. You must explicitly restrict toRetryConditionsto avoid resending generative POSTs.net/httpgives you full control ofRequest.Contextfor cancellation;restyexposesSetContextbut hides the underlying request until execution.- Both handle streaming poorly if you forget to flush or if you use a buffered scanner with lines longer than 64KB (SSE can send large JSON blobs). Use
bufio.Scannerwith a larger buffer orbufio.Reader.
Head-to-Head Summary
| Dimension | net/http | resty |
|---|---|---|
| Dependency footprint | stdlib, zero external | single module, no trans deps |
| JSON (de)serialization | manual encoding/json |
automatic via reflection |
| Streaming SSE | raw Body out of box |
raw via SetDoNotParseResponse |
| Retries | manual loop, explicit | built-in, needs guardrails |
| Request construction | verbose, explicit | fluent, concise |
| Header / routing control | Header.Set |
chainable SetHeader |
| Memory under long output | flat with streaming | buffers full body by default |
| Ecosystem fit | universal (otel, httptest) | narrower, CLI-oriented |
Which to Choose
Use net/http if:
- You are building a library, SDK, or long-running proxy that fronts multiple LLM providers.
- You need streaming completions with bounded memory.
- You want zero dependency surface and explicit control over retries and timeouts.
- You are already instrumenting
http.RoundTripperfor metrics.
Use resty if:
- You are writing a throwaway script, a small admin tool, or a test harness that fires a few non-streaming requests.
- You value concise request building over fine-grained lifecycle control.
- You will remember to disable auto-response parsing for streams and to scope retries to connection errors only.
For a production service that calls an LLM gateway with routing directives and cache hints: the client library is incidental. Pick net/http unless your team has standardized on resty for other HTTP calls—then isolate the LLM client behind an interface so the transport can be swapped without touching business logic.