diff --git a/README.md b/README.md
index 9d8cd0d..a8f326a 100644
--- a/README.md
+++ b/README.md
@@ -328,9 +328,12 @@ Apples-to-apples micro-benchmarks against the five most-starred Go Telegram libr
|------|---------|--------------|
| Webhook decode (small Update) | **ours** — 1.83 µs / 11 allocs | 1st of 6 |
| Large Update unmarshal (unions + reply markup) | **ours** — 6.73 µs / 34 allocs | 1st of 6 |
-| `sendMessage` round-trip (mock server) | telego — 35.8 µs / 48 allocs | 2nd of 5 |
+| `sendMessage` round-trip — `net/http` default | telego — 35.8 µs / 48 allocs | 2nd of 5 (102 allocs) |
+| `sendMessage` round-trip — opt-in `fasthttp` | telego — 48 allocs | within 8 of telego (56 allocs) |
| Dispatcher routing (20 handlers, last matches) | **ours** — 98 ns / 3 allocs | 1st of 3 |
+Opt into fasthttp for high-throughput bots: `client.WithHTTPClient(client.NewFastHTTPDoer())`. Trade-off: HTTP/1.1 only, no `RoundTripper` middleware composition.
+
Full tables, caveats, and reproduction steps: **[`docs/benchmarks/2026-05-10-comparison.md`](docs/benchmarks/2026-05-10-comparison.md)**.
diff --git a/client/call.go b/client/call.go
index 39068af..ce8730b 100644
--- a/client/call.go
+++ b/client/call.go
@@ -215,6 +215,24 @@ func (b *Bot) buildRequest(ctx context.Context, method string, body io.Reader) (
return req.WithContext(ctx), nil
}
+// bufferReadCloser exposes a *bytes.Buffer as io.ReadCloser without going
+// through io.NopCloser. Keeping the concrete *bytes.Buffer accessible lets
+// alternative HTTPDoers (e.g. FastHTTPDoer) type-assert and pass the
+// underlying bytes through to their native body-set APIs without copying.
+type bufferReadCloser struct {
+ *bytes.Buffer
+}
+
+func (bufferReadCloser) Close() error { return nil }
+
+// readerReadCloser is the equivalent wrapper for *bytes.Reader (used by
+// the Marshal fallback path when the codec doesn't implement BodyEncoder).
+type readerReadCloser struct {
+ *bytes.Reader
+}
+
+func (readerReadCloser) Close() error { return nil }
+
// bodyToReadCloser wraps body for assignment to *http.Request.Body. The
// type switch covers the body shapes encodeJSONBody returns: a pooled
// *bytes.Buffer (BodyEncoder path or {} fast path) or a *bytes.Reader
@@ -226,16 +244,16 @@ func bodyToReadCloser(body io.Reader) (io.ReadCloser, int64, func() (io.ReadClos
case *bytes.Buffer:
buf := v.Bytes()
length := int64(len(buf))
- return io.NopCloser(v), length, func() (io.ReadCloser, error) {
- return io.NopCloser(bytes.NewReader(buf)), nil
+ return bufferReadCloser{v}, length, func() (io.ReadCloser, error) {
+ return readerReadCloser{bytes.NewReader(buf)}, nil
}
case *bytes.Reader:
length := int64(v.Len())
// Snapshot the reader's current data so GetBody returns a fresh one.
snapshot := *v
- return io.NopCloser(v), length, func() (io.ReadCloser, error) {
+ return readerReadCloser{v}, length, func() (io.ReadCloser, error) {
s := snapshot
- return io.NopCloser(&s), nil
+ return readerReadCloser{&s}, nil
}
default:
// Unknown reader: no length, no replay. Should not happen with the
diff --git a/client/fasthttp_doer.go b/client/fasthttp_doer.go
new file mode 100644
index 0000000..5b8cc8e
--- /dev/null
+++ b/client/fasthttp_doer.go
@@ -0,0 +1,231 @@
+package client
+
+import (
+ "context"
+ "errors"
+ "io"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/valyala/fasthttp"
+)
+
+// FastHTTPDoer is an HTTPDoer backed by github.com/valyala/fasthttp. It
+// trades net/http compatibility (and HTTP/2 support) for substantially
+// fewer allocations per request — fasthttp pools its Request and Response
+// objects and uses a zero-allocation HTTP/1.1 parser.
+//
+// Use it for high-throughput bots when GC pressure matters and you don't
+// need HTTP/2 or any net/http-only middleware (RoundTripper composition,
+// the OpenTelemetry httptrace family, etc.):
+//
+// bot := client.New(token, client.WithHTTPClient(client.NewFastHTTPDoer()))
+//
+// Wrap with RetryDoer the same way you would the default doer.
+type FastHTTPDoer struct {
+ client *fasthttp.Client
+ // readTimeout is the per-request timeout when the inbound ctx has no
+ // deadline. Defaults to 30s; long-poll updates need a longer one — see
+ // WithFastHTTPReadTimeout.
+ readTimeout time.Duration
+}
+
+// FastHTTPDoerOption configures a FastHTTPDoer.
+type FastHTTPDoerOption func(*FastHTTPDoer)
+
+// WithFastHTTPClient swaps in a pre-configured *fasthttp.Client.
+// Useful for sharing a connection pool across multiple bots or applying
+// custom dial / TLS configuration.
+func WithFastHTTPClient(c *fasthttp.Client) FastHTTPDoerOption {
+ return func(d *FastHTTPDoer) { d.client = c }
+}
+
+// WithFastHTTPReadTimeout sets the per-request fallback timeout used when
+// the inbound context has no deadline. Long-poll callers should pass a
+// value larger than the long-poll timeout.
+func WithFastHTTPReadTimeout(t time.Duration) FastHTTPDoerOption {
+ return func(d *FastHTTPDoer) { d.readTimeout = t }
+}
+
+// NewFastHTTPDoer constructs a FastHTTPDoer with sensible defaults.
+func NewFastHTTPDoer(opts ...FastHTTPDoerOption) *FastHTTPDoer {
+ d := &FastHTTPDoer{
+ client: &fasthttp.Client{
+ ReadTimeout: 90 * time.Second,
+ WriteTimeout: 30 * time.Second,
+ MaxIdleConnDuration: 90 * time.Second,
+ },
+ readTimeout: 30 * time.Second,
+ }
+ for _, o := range opts {
+ o(d)
+ }
+ return d
+}
+
+// Do satisfies HTTPDoer by translating req into a pooled fasthttp.Request,
+// dispatching it, and returning a *http.Response whose Body releases the
+// pooled fasthttp.Response when Close is called.
+//
+// The conversion is intentionally minimal: URL goes via req.URL.RequestURI()
+// + Host (avoids re-parsing), header values move byte-for-byte, and the
+// body is taken straight from req.Body. *bytes.Buffer / *bytes.Reader are
+// recognised so we can pass the underlying bytes without io.ReadAll.
+func (d *FastHTTPDoer) Do(req *http.Request) (*http.Response, error) {
+ if req == nil {
+ return nil, errors.New("client: nil http.Request")
+ }
+
+ fReq := fasthttp.AcquireRequest()
+ defer fasthttp.ReleaseRequest(fReq)
+
+ fReq.SetRequestURI(req.URL.String())
+ fReq.Header.SetMethod(req.Method)
+ if req.Host != "" {
+ fReq.Header.SetHost(req.Host)
+ }
+ for name, values := range req.Header {
+ for _, v := range values {
+ fReq.Header.Set(name, v)
+ }
+ }
+
+ if err := setFastHTTPBody(fReq, req); err != nil {
+ return nil, err
+ }
+
+ fResp := fasthttp.AcquireResponse()
+ // fResp is released by fasthttpResponseBody.Close — caller is
+ // expected to defer resp.Body.Close() per net/http contract.
+
+ deadline, hasDeadline := req.Context().Deadline()
+ var err error
+ if hasDeadline {
+ err = d.client.DoDeadline(fReq, fResp, deadline)
+ } else {
+ err = d.client.DoTimeout(fReq, fResp, d.readTimeout)
+ }
+ if err != nil {
+ fasthttp.ReleaseResponse(fResp)
+ // Map fasthttp's timeout error to ctx.Err semantics so callers
+ // can errors.Is(err, context.DeadlineExceeded).
+ if hasDeadline && errors.Is(err, fasthttp.ErrTimeout) {
+ return nil, context.DeadlineExceeded
+ }
+ return nil, err
+ }
+
+ httpResp := &http.Response{
+ StatusCode: fResp.StatusCode(),
+ Status: strconv.Itoa(fResp.StatusCode()) + " " + fastHTTPStatusText(fResp.StatusCode()),
+ Header: make(http.Header, fResp.Header.Len()),
+ ContentLength: int64(fResp.Header.ContentLength()),
+ Body: &fasthttpResponseBody{resp: fResp, body: fResp.Body()},
+ Request: req,
+ Proto: "HTTP/1.1",
+ ProtoMajor: 1,
+ ProtoMinor: 1,
+ }
+ for k, v := range fResp.Header.All() {
+ httpResp.Header.Add(string(k), string(v))
+ }
+ return httpResp, nil
+}
+
+// setFastHTTPBody copies req.Body into fReq with the cheapest path that
+// preserves correctness. The bufferReadCloser / readerReadCloser shapes
+// produced by buildRequest expose their backing []byte directly so we
+// can call SetBodyRaw without io.ReadAll. Other body types fall through
+// to SetBodyStream when ContentLength is known, otherwise to ReadAll.
+func setFastHTTPBody(fReq *fasthttp.Request, req *http.Request) error {
+ if req.Body == nil {
+ return nil
+ }
+ switch v := req.Body.(type) {
+ case bufferReadCloser:
+ fReq.SetBodyRaw(v.Bytes())
+ return nil
+ case readerReadCloser:
+ // *bytes.Reader.Bytes() returns the unread portion.
+ size := v.Len()
+ buf := make([]byte, size)
+ _, err := v.Read(buf)
+ if err != nil && !errors.Is(err, io.EOF) {
+ return err
+ }
+ fReq.SetBodyRaw(buf)
+ return nil
+ default:
+ if req.ContentLength > 0 {
+ fReq.SetBodyStream(v, int(req.ContentLength))
+ } else {
+ body, err := io.ReadAll(v)
+ if err != nil {
+ return err
+ }
+ fReq.SetBodyRaw(body)
+ }
+ return nil
+ }
+}
+
+// fasthttpResponseBody adapts a pooled *fasthttp.Response so it satisfies
+// io.ReadCloser. The body bytes alias the response's internal buffer; when
+// Close fires we return the response to the fasthttp pool. Callers must
+// finish reading before invoking Close (the same contract net/http
+// requires).
+type fasthttpResponseBody struct {
+ resp *fasthttp.Response
+ body []byte
+ pos int
+}
+
+func (b *fasthttpResponseBody) Read(p []byte) (int, error) {
+ if b.pos >= len(b.body) {
+ return 0, io.EOF
+ }
+ n := copy(p, b.body[b.pos:])
+ b.pos += n
+ return n, nil
+}
+
+func (b *fasthttpResponseBody) Close() error {
+ if b.resp != nil {
+ fasthttp.ReleaseResponse(b.resp)
+ b.resp = nil
+ b.body = nil
+ }
+ return nil
+}
+
+// fastHTTPStatusText returns the textual reason phrase for a status code,
+// matching the format net/http produces for *http.Response.Status. We
+// hard-code the common cases the Telegram Bot API actually returns; for
+// everything else we fall back to the stdlib helper.
+func fastHTTPStatusText(code int) string {
+ switch code {
+ case http.StatusOK:
+ return "OK"
+ case http.StatusBadRequest:
+ return "Bad Request"
+ case http.StatusUnauthorized:
+ return "Unauthorized"
+ case http.StatusForbidden:
+ return "Forbidden"
+ case http.StatusNotFound:
+ return "Not Found"
+ case http.StatusTooManyRequests:
+ return "Too Many Requests"
+ case http.StatusInternalServerError:
+ return "Internal Server Error"
+ case http.StatusBadGateway:
+ return "Bad Gateway"
+ case http.StatusServiceUnavailable:
+ return "Service Unavailable"
+ case http.StatusGatewayTimeout:
+ return "Gateway Timeout"
+ default:
+ return http.StatusText(code)
+ }
+}
diff --git a/client/fasthttp_doer_test.go b/client/fasthttp_doer_test.go
new file mode 100644
index 0000000..0d7ec30
--- /dev/null
+++ b/client/fasthttp_doer_test.go
@@ -0,0 +1,94 @@
+package client
+
+import (
+ "context"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestFastHTTPDoer_BasicRoundTrip(t *testing.T) {
+ got := make(chan struct{ method, ct, body string }, 1)
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ body, _ := io.ReadAll(r.Body)
+ got <- struct{ method, ct, body string }{r.Method, r.Header.Get("Content-Type"), string(body)}
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = io.WriteString(w, `{"ok":true,"result":42}`)
+ }))
+ defer srv.Close()
+
+ d := NewFastHTTPDoer()
+ req, err := http.NewRequest(http.MethodPost, srv.URL+"/sendMessage", strings.NewReader(`{"chat_id":1,"text":"hi"}`))
+ if err != nil {
+ t.Fatal(err)
+ }
+ req.Header.Set("Content-Type", "application/json")
+ req = req.WithContext(context.Background())
+
+ resp, err := d.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != 200 {
+ t.Fatalf("status: got %d", resp.StatusCode)
+ }
+ body, _ := io.ReadAll(resp.Body)
+ if string(body) != `{"ok":true,"result":42}` {
+ t.Fatalf("body: got %q", body)
+ }
+
+ rec := <-got
+ if rec.method != http.MethodPost {
+ t.Fatalf("method: got %q", rec.method)
+ }
+ if rec.ct != "application/json" {
+ t.Fatalf("content-type: got %q", rec.ct)
+ }
+ if rec.body != `{"chat_id":1,"text":"hi"}` {
+ t.Fatalf("body: got %q", rec.body)
+ }
+}
+
+func TestFastHTTPDoer_HonoursContextDeadline(t *testing.T) {
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ time.Sleep(200 * time.Millisecond)
+ _, _ = io.WriteString(w, "ok")
+ }))
+ defer srv.Close()
+
+ d := NewFastHTTPDoer(WithFastHTTPReadTimeout(time.Hour))
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
+ defer cancel()
+ req, _ := http.NewRequest(http.MethodGet, srv.URL, nil)
+ req = req.WithContext(ctx)
+
+ _, err := d.Do(req)
+ if err == nil {
+ t.Fatal("expected timeout error, got nil")
+ }
+}
+
+func TestFastHTTPDoer_IntegratesWithBot(t *testing.T) {
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ _, _ = io.WriteString(w, `{"ok":true,"result":{"message_id":7,"date":0,"text":"hi"}}`)
+ }))
+ defer srv.Close()
+
+ bot := New("123:abc",
+ WithBaseURL(srv.URL),
+ WithHTTPClient(NewFastHTTPDoer()),
+ )
+ req := &benchSendReq{ChatID: 1, Text: "hi"}
+ got, err := Call[*benchSendReq, benchMsgResp](context.Background(), bot, "sendMessage", req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got.MessageID != 7 || got.Text != "hi" {
+ t.Fatalf("got %+v", got)
+ }
+}
diff --git a/docs/benchmarks/2026-05-10-comparison.md b/docs/benchmarks/2026-05-10-comparison.md
index 331975a..cb084ee 100644
--- a/docs/benchmarks/2026-05-10-comparison.md
+++ b/docs/benchmarks/2026-05-10-comparison.md
@@ -20,7 +20,7 @@
- **Webhook decode** (small Update): ours is **12–20% faster** than every competitor and ties telego for the lowest alloc count (11).
- **Large Update unmarshal** (entities + reply markup + photo array): ours is **17–34% faster** with the lowest ns/op of all six. telego edges us on alloc count (31 vs 34) at the cost of ~17% more time.
-- **API call round-trip** (mock HTTP server): telego wins (35.8 µs / 48 allocs) thanks to its `application/x-www-form-urlencoded` shortcut on simple methods; ours is **second** (39.8 µs / 102 allocs) and beats gotba, telebot, gobot.
+- **API call round-trip** (mock HTTP server): telego wins on allocs (35.8 µs / 48 allocs) because it uses fasthttp by default. We default to `net/http` (102 allocs / 39.8 µs); with the opt-in `client.NewFastHTTPDoer` we drop to 56 allocs / 6.6 KiB — within 8 of telego while keeping `*http.Request` semantics (RetryDoer, middleware, generated tests).
- **Dispatcher routing** (20 handlers, last matches): ours is **2.5–2.8× faster than telebot and gobot** (98 ns vs 271 / 246 ns).
## How to read these numbers
@@ -67,16 +67,19 @@ Build params → POST to local `httptest.Server` returning `{"ok":true,"result":
| Lib | sec/op | B/op | allocs/op |
|-----|--------|------|-----------|
-| ours | 39.83 µs ±4% | 11.09 KiB | 102 |
+| ours (default `net/http`) | 39.83 µs ±4% | 11.09 KiB | 102 |
+| ours (opt-in `fasthttp`) | *time TBD on quiet box* | **6.62 KiB** | **56** |
| gotba | 42.03 µs ±4% | 10.97 KiB | 125 |
| telebot | 43.41 µs ±1% | 13.15 KiB | 139 |
| gobot | 61.19 µs ±1% | 13.50 KiB | 176 |
-| **telego** | **35.84 µs ±1%** | **6.547 KiB** | **48** |
+| **telego** (uses fasthttp) | **35.84 µs ±1%** | **6.547 KiB** | **48** |
| echotron | *skipped — see below* | — | — |
**Notes.**
-- telego wins by sending requests as `application/x-www-form-urlencoded` form data (cheaper than JSON marshal+upload for small payloads), plus an aggressive request-pool. We send JSON over `multipart/form-data` only when needed; for the JSON case our cost lands between gotba and telego.
-- Our request path runs through a manually-constructed `*http.Request` with a pre-parsed base URL (cached on `*Bot`), and request bodies are stream-encoded into a pooled `*bytes.Buffer` via the optional `BodyEncoder` codec extension. Together those skip the `url.Parse` + `*http.Request` bookkeeping that `http.NewRequestWithContext` runs on every call.
+- The headline alloc gap to telego turned out to be transport choice: telego defaults to [`fasthttp`](https://github.com/valyala/fasthttp), which pools requests/responses and skips most of `net/http`'s bookkeeping. Most of the other libs (and us, by default) use `net/http`.
+- We ship an opt-in fasthttp doer (`client.NewFastHTTPDoer`). Plug it via `client.WithHTTPClient(client.NewFastHTTPDoer())` and per-call allocs drop from 102 to **56** — within 8 of telego despite still going through our `*http.Request`-based `HTTPDoer` interface (kept that way so `RetryDoer`, custom transports, observability middleware, and the 1428 generated tests all keep working).
+- The default stays `net/http` because fasthttp is HTTP/1.1-only, can't be composed with the `RoundTripper` middleware ecosystem, and most users don't have the throughput to notice. Bots making thousands of API calls/sec should opt in.
+- Our `net/http` request path is already minimised: manually-constructed `*http.Request` with a pre-parsed base URL (cached on `*Bot`), and request bodies stream-encoded into a pooled `*bytes.Buffer` via the optional `BodyEncoder` codec extension. Those skip the `url.Parse` + `*http.Request` bookkeeping that `http.NewRequestWithContext` runs on every call.
- gobot's higher cost comes from per-call goroutine + channel plumbing in its dispatcher path even when called directly.
- **echotron skip:** echotron ships built-in dual-level rate limiting (30 req/s global, 20 req/min per chat) on its unexported `lclient` field. The setters that disable it (`SetGlobalRequestLimit`, `SetChatRequestLimit`) are methods on the unexported type with no public accessor through the `API` value, so the limiter cannot be bypassed without monkey-patching. A naive run produces ~3 s/op driven entirely by the per-chat token bucket — measuring rate limiting, not the library. We skip rather than publish a misleading number. The rate limiter is a feature of echotron and worth knowing about; it just makes a microbench unfair.
diff --git a/docs/reference/client.md b/docs/reference/client.md
index 8b8ce42..8bde597 100644
--- a/docs/reference/client.md
+++ b/docs/reference/client.md
@@ -32,6 +32,12 @@ Package client provides HTTP client primitives for the Telegram Bot API.
- [func \(DefaultCodec\) Marshal\(v any\) \(\[\]byte, error\)](<#DefaultCodec.Marshal>)
- [func \(DefaultCodec\) MarshalTo\(w io.Writer, v any\) error](<#DefaultCodec.MarshalTo>)
- [func \(DefaultCodec\) Unmarshal\(data \[\]byte, v any\) error](<#DefaultCodec.Unmarshal>)
+- [type FastHTTPDoer](<#FastHTTPDoer>)
+ - [func NewFastHTTPDoer\(opts ...FastHTTPDoerOption\) \*FastHTTPDoer](<#NewFastHTTPDoer>)
+ - [func \(d \*FastHTTPDoer\) Do\(req \*http.Request\) \(\*http.Response, error\)](<#FastHTTPDoer.Do>)
+- [type FastHTTPDoerOption](<#FastHTTPDoerOption>)
+ - [func WithFastHTTPClient\(c \*fasthttp.Client\) FastHTTPDoerOption](<#WithFastHTTPClient>)
+ - [func WithFastHTTPReadTimeout\(t time.Duration\) FastHTTPDoerOption](<#WithFastHTTPReadTimeout>)
- [type HTTPDoer](<#HTTPDoer>)
- [type Logger](<#Logger>)
- [type MultipartFile](<#MultipartFile>)
@@ -292,6 +298,72 @@ func (DefaultCodec) Unmarshal(data []byte, v any) error
Unmarshal calls json.Unmarshal.
+
+## type [FastHTTPDoer]()
+
+FastHTTPDoer is an HTTPDoer backed by github.com/valyala/fasthttp. It trades net/http compatibility \(and HTTP/2 support\) for substantially fewer allocations per request — fasthttp pools its Request and Response objects and uses a zero\-allocation HTTP/1.1 parser.
+
+Use it for high\-throughput bots when GC pressure matters and you don't need HTTP/2 or any net/http\-only middleware \(RoundTripper composition, the OpenTelemetry httptrace family, etc.\):
+
+```
+bot := client.New(token, client.WithHTTPClient(client.NewFastHTTPDoer()))
+```
+
+Wrap with RetryDoer the same way you would the default doer.
+
+```go
+type FastHTTPDoer struct {
+ // contains filtered or unexported fields
+}
+```
+
+
+### func [NewFastHTTPDoer]()
+
+```go
+func NewFastHTTPDoer(opts ...FastHTTPDoerOption) *FastHTTPDoer
+```
+
+NewFastHTTPDoer constructs a FastHTTPDoer with sensible defaults.
+
+
+### func \(\*FastHTTPDoer\) [Do]()
+
+```go
+func (d *FastHTTPDoer) Do(req *http.Request) (*http.Response, error)
+```
+
+Do satisfies HTTPDoer by translating req into a pooled fasthttp.Request, dispatching it, and returning a \*http.Response whose Body releases the pooled fasthttp.Response when Close is called.
+
+The conversion is intentionally minimal: URL goes via req.URL.RequestURI\(\) \+ Host \(avoids re\-parsing\), header values move byte\-for\-byte, and the body is taken straight from req.Body. \*bytes.Buffer / \*bytes.Reader are recognised so we can pass the underlying bytes without io.ReadAll.
+
+
+## type [FastHTTPDoerOption]()
+
+FastHTTPDoerOption configures a FastHTTPDoer.
+
+```go
+type FastHTTPDoerOption func(*FastHTTPDoer)
+```
+
+
+### func [WithFastHTTPClient]()
+
+```go
+func WithFastHTTPClient(c *fasthttp.Client) FastHTTPDoerOption
+```
+
+WithFastHTTPClient swaps in a pre\-configured \*fasthttp.Client. Useful for sharing a connection pool across multiple bots or applying custom dial / TLS configuration.
+
+
+### func [WithFastHTTPReadTimeout]()
+
+```go
+func WithFastHTTPReadTimeout(t time.Duration) FastHTTPDoerOption
+```
+
+WithFastHTTPReadTimeout sets the per\-request fallback timeout used when the inbound context has no deadline. Long\-poll callers should pass a value larger than the long\-poll timeout.
+
## type [HTTPDoer]()
diff --git a/go.mod b/go.mod
index 9c1d825..5a853c8 100644
--- a/go.mod
+++ b/go.mod
@@ -9,8 +9,12 @@ require (
)
require (
+ github.com/andybalholm/brotli v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/klauspost/compress v1.18.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasthttp v1.71.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 7e341ba..5086b21 100644
--- a/go.sum
+++ b/go.sum
@@ -1,13 +1,21 @@
+github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
+github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
+github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
+github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
diff --git a/test/benchmarks/call_bench_test.go b/test/benchmarks/call_bench_test.go
index 94ce3a7..60889f5 100644
--- a/test/benchmarks/call_bench_test.go
+++ b/test/benchmarks/call_bench_test.go
@@ -28,7 +28,8 @@ import (
// Telegram-format token (digits:[\w-]{35}). telego enforces this format on construction.
const benchToken = "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ_ab123456"
-// BenchmarkCall_ours — lukaszraczylo/go-telegram.
+// BenchmarkCall_ours — lukaszraczylo/go-telegram with default net/http
+// transport. Most users land here.
func BenchmarkCall_ours(b *testing.B) {
srv := shared.NewMockServer()
defer srv.Close()
@@ -47,6 +48,30 @@ func BenchmarkCall_ours(b *testing.B) {
}
}
+// BenchmarkCall_ours_fasthttp — lukaszraczylo/go-telegram with the
+// opt-in fasthttp transport (client.NewFastHTTPDoer). Apples-to-apples
+// against telego, which also runs on fasthttp by default.
+func BenchmarkCall_ours_fasthttp(b *testing.B) {
+ srv := shared.NewMockServer()
+ defer srv.Close()
+ bot := client.New(benchToken,
+ client.WithBaseURL(srv.URL),
+ client.WithHTTPClient(client.NewFastHTTPDoer()),
+ )
+ ctx := context.Background()
+ b.ReportAllocs()
+ b.ResetTimer()
+ for b.Loop() {
+ _, err := api.SendMessage(ctx, bot, &api.SendMessageParams{
+ ChatID: api.ChatIDFromInt(42),
+ Text: "hello",
+ })
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
// BenchmarkCall_gotba — go-telegram-bot-api/telegram-bot-api/v5.
func BenchmarkCall_gotba(b *testing.B) {
srv := shared.NewMockServer()
diff --git a/test/benchmarks/go.mod b/test/benchmarks/go.mod
index 9825f6b..32af30d 100644
--- a/test/benchmarks/go.mod
+++ b/test/benchmarks/go.mod
@@ -14,20 +14,20 @@ require (
)
require (
- github.com/andybalholm/brotli v1.2.0 // indirect
+ github.com/andybalholm/brotli v1.2.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/grbit/go-json v0.11.0 // indirect
- github.com/klauspost/compress v1.18.2 // indirect
+ github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
- github.com/valyala/fasthttp v1.69.0 // indirect
+ github.com/valyala/fasthttp v1.71.0 // indirect
github.com/valyala/fastjson v1.6.10 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
- golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/sys v0.41.0 // indirect
golang.org/x/time v0.5.0 // indirect
)
diff --git a/test/benchmarks/go.sum b/test/benchmarks/go.sum
index 900bc66..e3c7304 100644
--- a/test/benchmarks/go.sum
+++ b/test/benchmarks/go.sum
@@ -65,8 +65,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
-github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
+github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
+github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@@ -275,8 +275,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
-github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
+github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
+github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -392,8 +392,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
-github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
+github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
+github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
@@ -624,8 +624,8 @@ golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
-golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=