diff --git a/internal/autocomplete/service.go b/internal/autocomplete/service.go
index bc51026..fd3d8ea 100644
--- a/internal/autocomplete/service.go
+++ b/internal/autocomplete/service.go
@@ -25,6 +25,8 @@ import (
"net/url"
"strings"
"time"
+
+ "github.com/metamorphosis-dev/samsa/internal/httpclient"
)
// Service fetches search suggestions from upstream or Wikipedia OpenSearch.
@@ -39,7 +41,7 @@ func NewService(upstreamURL string, timeout time.Duration) *Service {
}
return &Service{
upstreamURL: strings.TrimRight(upstreamURL, "/"),
- http: &http.Client{Timeout: timeout},
+ http: httpclient.NewClient(timeout),
}
}
diff --git a/internal/engines/factory.go b/internal/engines/factory.go
index ff0b925..f91cf1b 100644
--- a/internal/engines/factory.go
+++ b/internal/engines/factory.go
@@ -22,13 +22,14 @@ import (
"time"
"github.com/metamorphosis-dev/samsa/internal/config"
+ "github.com/metamorphosis-dev/samsa/internal/httpclient"
)
// NewDefaultPortedEngines returns the Go-native engine registry.
// If cfg is nil, API keys fall back to environment variables.
func NewDefaultPortedEngines(client *http.Client, cfg *config.Config) map[string]Engine {
if client == nil {
- client = &http.Client{Timeout: 10 * time.Second}
+ client = httpclient.NewClient(10 * time.Second)
}
var braveAPIKey, braveAccessToken, youtubeAPIKey string
diff --git a/internal/httpclient/client.go b/internal/httpclient/client.go
new file mode 100644
index 0000000..450d121
--- /dev/null
+++ b/internal/httpclient/client.go
@@ -0,0 +1,61 @@
+// samsa — a privacy-respecting metasearch engine
+// Copyright (C) 2026-present metamorphosis-dev
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package httpclient
+
+import (
+ "net/http"
+ "sync"
+ "time"
+)
+
+var (
+ defaultTransport http.RoundTripper
+ once sync.Once
+)
+
+// Default returns a shared, pre-configured http.RoundTripper suitable for
+// outgoing engine requests. It is safe for concurrent use across goroutines.
+// All fields are tuned for a meta-search engine that makes many concurrent
+// requests to a fixed set of upstream hosts:
+//
+// - MaxIdleConnsPerHost = 20 (vs default of 2; keeps more warm connections
+// to each host, avoiding repeated TCP+TLS handshakes)
+// - MaxIdleConns = 100 (total idle connection ceiling)
+// - IdleConnTimeout = 90s (prunes connections before they go stale)
+// - DialContext timeout = 5s (fails fast on DNS/connect rather than
+// holding a goroutine indefinitely)
+func Default() http.RoundTripper {
+ once.Do(func() {
+ defaultTransport = &http.Transport{
+ MaxIdleConnsPerHost: 20,
+ MaxIdleConns: 100,
+ IdleConnTimeout: 90 * time.Second,
+ DialContext: dialWithTimeout(5 * time.Second),
+ }
+ })
+ return defaultTransport
+}
+
+// NewClient returns an http.Client that uses DefaultTransport and the given
+// request timeout. The returned client reuses the shared connection pool,
+// so all clients created via this function share the same warm connections.
+func NewClient(timeout time.Duration) *http.Client {
+ return &http.Client{
+ Transport: Default(),
+ Timeout: timeout,
+ }
+}
diff --git a/internal/httpclient/dial.go b/internal/httpclient/dial.go
new file mode 100644
index 0000000..c646e33
--- /dev/null
+++ b/internal/httpclient/dial.go
@@ -0,0 +1,30 @@
+// samsa — a privacy-respecting metasearch engine
+// Copyright (C) 2026-present metamorphosis-dev
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package httpclient
+
+import (
+ "context"
+ "net"
+ "time"
+)
+
+// dialWithTimeout returns a DialContext function for http.Transport that
+// respects the given connection timeout.
+func dialWithTimeout(timeout time.Duration) func(context.Context, string, string) (net.Conn, error) {
+ d := &net.Dialer{Timeout: timeout}
+ return d.DialContext
+}
diff --git a/internal/search/service.go b/internal/search/service.go
index 29d7010..5759f60 100644
--- a/internal/search/service.go
+++ b/internal/search/service.go
@@ -18,11 +18,11 @@ package search
import (
"context"
- "net/http"
"sync"
"time"
"github.com/metamorphosis-dev/samsa/internal/cache"
+ "github.com/metamorphosis-dev/samsa/internal/httpclient"
"github.com/metamorphosis-dev/samsa/internal/config"
"github.com/metamorphosis-dev/samsa/internal/contracts"
"github.com/metamorphosis-dev/samsa/internal/engines"
@@ -49,7 +49,7 @@ func NewService(cfg ServiceConfig) *Service {
timeout = 10 * time.Second
}
- httpClient := &http.Client{Timeout: timeout}
+ httpClient := httpclient.NewClient(timeout)
var up *upstream.Client
if cfg.UpstreamURL != "" {
diff --git a/internal/upstream/client.go b/internal/upstream/client.go
index 816b713..7c48311 100644
--- a/internal/upstream/client.go
+++ b/internal/upstream/client.go
@@ -28,6 +28,7 @@ import (
"time"
"github.com/metamorphosis-dev/samsa/internal/contracts"
+ "github.com/metamorphosis-dev/samsa/internal/httpclient"
)
type Client struct {
@@ -56,9 +57,7 @@ func NewClient(baseURL string, timeout time.Duration) (*Client, error) {
return &Client{
baseURL: base,
- http: &http.Client{
- Timeout: timeout,
- },
+ http: httpclient.NewClient(timeout),
}, nil
}