diff --git a/clientutil/backend.go b/clientutil/backend.go
index 9e08fa01ab2c1906dfa36a5c74555ab0e4d7e2ad..cdde33b45ffe954a37fcf9d53dfd1601e8430876 100644
--- a/clientutil/backend.go
+++ b/clientutil/backend.go
@@ -16,6 +16,13 @@ type BackendConfig struct {
 	TLSConfig *TLSClientConfig `yaml:"tls"`
 	Sharded   bool             `yaml:"sharded"`
 	Debug     bool             `yaml:"debug"`
+
+	// Connection timeout (if unset, use default value).
+	ConnectTimeout string `yaml:"connect_timeout"`
+
+	// Maximum timeout for each individual request to this backend
+	// (if unset, use the Context timeout).
+	RequestMaxTimeout string `yaml:"request_max_timeout"`
 }
 
 // Backend is a runtime class that provides http Clients for use with
diff --git a/clientutil/balancer.go b/clientutil/balancer.go
index d2ca8270fcf27deb3e48e3526a91bb2c8cf004a1..8f6e99f197e648ea18bf4024874ec0f5c5594c9a 100644
--- a/clientutil/balancer.go
+++ b/clientutil/balancer.go
@@ -60,10 +60,11 @@ func newExponentialBackOff() *backoff.ExponentialBackOff {
 type balancedBackend struct {
 	*backendTracker
 	*transportCache
-	baseURI  *url.URL
-	sharded  bool
-	resolver resolver
-	log      logger
+	baseURI           *url.URL
+	sharded           bool
+	resolver          resolver
+	log               logger
+	requestMaxTimeout time.Duration
 }
 
 func newBalancedBackend(config *BackendConfig, resolver resolver) (*balancedBackend, error) {
@@ -80,17 +81,36 @@ func newBalancedBackend(config *BackendConfig, resolver resolver) (*balancedBack
 		}
 	}
 
+	var connectTimeout time.Duration
+	if config.ConnectTimeout != "" {
+		t, err := time.ParseDuration(config.ConnectTimeout)
+		if err != nil {
+			return nil, fmt.Errorf("error in connect_timeout: %v", err)
+		}
+		connectTimeout = t
+	}
+
+	var reqTimeout time.Duration
+	if config.RequestMaxTimeout != "" {
+		t, err := time.ParseDuration(config.RequestMaxTimeout)
+		if err != nil {
+			return nil, fmt.Errorf("error in request_max_timeout: %v", err)
+		}
+		reqTimeout = t
+	}
+
 	var logger logger = &nilLogger{}
 	if config.Debug {
 		logger = log.New(os.Stderr, fmt.Sprintf("backend %s: ", u.Host), 0)
 	}
 	return &balancedBackend{
-		backendTracker: newBackendTracker(u.Host, resolver, logger),
-		transportCache: newTransportCache(tlsConfig),
-		sharded:        config.Sharded,
-		baseURI:        u,
-		resolver:       resolver,
-		log:            logger,
+		backendTracker:    newBackendTracker(u.Host, resolver, logger),
+		transportCache:    newTransportCache(tlsConfig, connectTimeout),
+		requestMaxTimeout: reqTimeout,
+		sharded:           config.Sharded,
+		baseURI:           u,
+		resolver:          resolver,
+		log:               logger,
 	}, nil
 }
 
@@ -115,6 +135,9 @@ func (b *balancedBackend) Call(ctx context.Context, shard, path string, req, res
 	if deadline, ok := ctx.Deadline(); ok {
 		innerTimeout = time.Until(deadline) / time.Duration(seq.Len())
 	}
+	if b.requestMaxTimeout > 0 && innerTimeout > b.requestMaxTimeout {
+		innerTimeout = b.requestMaxTimeout
+	}
 
 	// Call the backends in the sequence until one succeeds, with an
 	// exponential backoff policy controlled by the outer Context.
diff --git a/clientutil/dialer.go b/clientutil/dialer.go
new file mode 100644
index 0000000000000000000000000000000000000000..c8e7390c48eac029bf572fe2a61a5ab68befe6b5
--- /dev/null
+++ b/clientutil/dialer.go
@@ -0,0 +1,20 @@
+// +build go1.9
+
+package clientutil
+
+import (
+	"context"
+	"net"
+	"time"
+)
+
+func netDialContext(addr string, connectTimeout time.Duration) func(context.Context, string, string) (net.Conn, error) {
+	dialer := &net.Dialer{
+		Timeout:   connectTimeout,
+		KeepAlive: 30 * time.Second,
+		DualStack: true,
+	}
+	return func(ctx context.Context, net string, _ string) (net.Conn, error) {
+		return dialer.DialContext(ctx, net, addr)
+	}
+}
diff --git a/clientutil/dialer_legacy.go b/clientutil/dialer_legacy.go
new file mode 100644
index 0000000000000000000000000000000000000000..f257990557b41aac26bbbb050ee1be02be806347
--- /dev/null
+++ b/clientutil/dialer_legacy.go
@@ -0,0 +1,23 @@
+// +build !go1.9
+
+package clientutil
+
+import (
+	"context"
+	"net"
+	"time"
+)
+
+// Go < 1.9 does not have net.DialContext, reimplement it in terms of
+// net.DialTimeout.
+func netDialContext(addr string, connectTimeout time.Duration) func(context.Context, string, string) (net.Conn, error) {
+	return func(ctx context.Context, net string, _ string) (net.Conn, error) {
+		if deadline, ok := ctx.Deadline(); ok {
+			ctxTimeout := time.Until(deadline)
+			if ctxTimeout < connectTimeout {
+				connectTimeout = ctxTimeout
+			}
+		}
+		return net.DialTimeout(network, addr, connectTimeout)
+	}
+}
diff --git a/clientutil/transport.go b/clientutil/transport.go
index 1cc6c6d34594869c8be446fd325c27d1ad2c3289..39b1b3d632ff5380e2da7b35e1abaf6af24a5ef5 100644
--- a/clientutil/transport.go
+++ b/clientutil/transport.go
@@ -1,9 +1,7 @@
 package clientutil
 
 import (
-	"context"
 	"crypto/tls"
-	"net"
 	"net/http"
 	"sync"
 	"time"
@@ -11,31 +9,42 @@ import (
 	"git.autistici.org/ai3/go-common/tracing"
 )
 
+var defaultConnectTimeout = 30 * time.Second
+
 // The transportCache is just a cache of http transports, each
 // connecting to a specific address.
 //
 // We use this to control the HTTP Host header and the TLS ServerName
 // independently of the target address.
 type transportCache struct {
-	tlsConfig *tls.Config
+	tlsConfig      *tls.Config
+	connectTimeout time.Duration
 
 	mx         sync.RWMutex
 	transports map[string]http.RoundTripper
 }
 
-func newTransportCache(tlsConfig *tls.Config) *transportCache {
+func newTransportCache(tlsConfig *tls.Config, connectTimeout time.Duration) *transportCache {
+	if connectTimeout == 0 {
+		connectTimeout = defaultConnectTimeout
+	}
 	return &transportCache{
-		tlsConfig:  tlsConfig,
-		transports: make(map[string]http.RoundTripper),
+		tlsConfig:      tlsConfig,
+		connectTimeout: connectTimeout,
+		transports:     make(map[string]http.RoundTripper),
 	}
 }
 
 func (m *transportCache) newTransport(addr string) http.RoundTripper {
 	return tracing.WrapTransport(&http.Transport{
 		TLSClientConfig: m.tlsConfig,
-		DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
-			return netDialContext(ctx, network, addr)
-		},
+		DialContext:     netDialContext(addr, m.connectTimeout),
+
+		// Parameters match those of net/http.DefaultTransport.
+		MaxIdleConns:          100,
+		IdleConnTimeout:       90 * time.Second,
+		TLSHandshakeTimeout:   10 * time.Second,
+		ExpectContinueTimeout: 1 * time.Second,
 	})
 }
 
@@ -55,13 +64,3 @@ func (m *transportCache) getTransport(addr string) http.RoundTripper {
 
 	return t
 }
-
-// Go < 1.9 does not have net.DialContext, reimplement it in terms of
-// net.DialTimeout.
-func netDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
-	timeout := 60 * time.Second // some arbitrary max timeout
-	if deadline, ok := ctx.Deadline(); ok {
-		timeout = time.Until(deadline)
-	}
-	return net.DialTimeout(network, addr, timeout)
-}