binder / Docs / Performance

Performance

Binding from path, query, cookie or header costs a few hundred nanoseconds and a handful of allocations. Binding a body costs more, because the body has to be read and parsed before any field can be converted.

How these were taken

Measured on an Apple M-series laptop with Go 1.27, -benchtime=200ms -count=10, reporting the median of ten runs. Each benchmark times binding alone: the request is built once and its body re-armed between iterations, so httptest.NewRequest is not folded into the figures — it costs more memory than the binding itself.

make bench

Results

Benchmarkns/opB/opallocs/op
BindPathOnly105723
BindCookieOnly1842805
BindNoQueryParams1862805
BindQueryOnly2284966
BindOmitEmpty2505286
BindBodyOnly/JSONBody8861,82431
BindParallel9042,60025
BindManyQueryParams91883220
BindBodyOnly/FormBody1,0302,60025
Bind1,2542,15229
BindMixed/WithJSON1,3042,54439
BindMixed/WithForm1,6323,71236
BindWithoutCache1,9023,46435
BindMultipart8,85038,06991

Reading the numbers

The tag cache

Bind against BindWithoutCache measures the per-type tag cache: 1,254 ns and 29 allocations with it warm, against 1,902 ns and 35 allocations when it is cleared before every iteration. Reflecting over a type's tags happens once per type, not once per request, which is why the first request against a handler is the expensive one and no other is.

The query string is parsed lazily

BindManyQueryParams binds eight query parameters and BindNoQueryParams binds none. The gap shows that the query string is parsed once per call and only when some field asks for it — a struct with no query tag never pays for url.Values.

Bodies cost more than the rest

A body has to be read before any field can be converted, which is why every body benchmark starts higher than the ones that read a header or a path value. A JSON body goes straight from the parser into your fields, and a member no field binds is skipped without being decoded at all; a form body still goes through a map, which is why it allocates more despite being simpler to parse. A handler that reads only a path parameter and a header touches none of it.

Multipart is an order of magnitude dearer

BindMultipart carries two text fields and a 4 KB file. The gap is inherent to the encoding rather than to binding: the parser copies each part, and the file is held in memory rather than spilled to disk. It is the price of an upload, not of the tag lookup.

Context

Setting the upload aside, the slowest case here is under two microseconds. On any endpoint that touches a database, a cache or another service, binding is not the thing to optimise — these numbers exist to show that it stays out of the way, not to invite tuning.

Allocation counts are not part of the compatibility contract. A release may bind in a different order or with a different number of allocations without that being a breaking change. See Compatibility.