HTTP Client Configuration
If you call api.basistheory.com with your own HTTP client, configure its connection reuse, timeouts, and retries as described on this page. These settings matter most for clients that call the API at volume. If you use a Basis Theory SDK, its default HTTP client already stays within the bounds the edge allows and needs no customization; the SDK section covers the cases where you want to change it.
Connection reuse
Keep one HTTP client, and therefore one connection pool, for the lifetime of your process. Creating a client per request pays a TCP and TLS handshake every time and defeats the pool.
Bound how long a pooled connection may sit idle and how long it may live in total:
- Set a maximum idle time for pooled connections. For a client you configure yourself, 30 seconds is a safe starting value. The SDK defaults are already inside the edge's idle window and do not need to be lowered.
- If the client offers a maximum total connection lifetime, set one. Start with 5 minutes. Clients without the setting rely on the idle bound and the retry below.
Requests to api.basistheory.com are handled by the Basis Theory edge before reaching the API. Traffic is encrypted in transit at every hop. The edge manages the keepalive session for each client connection and closes idle connections on its own schedule. That schedule is not part of the API contract: Basis Theory is responsible for the availability of the edge and the API, and your client is responsible for the health of its own connection pool. The values above are how it meets that responsibility. They are guidance rather than a connection-lifetime contract, so monitor connection errors and keep your client's idle window comfortably below the edge's observed idle timeout.
The SDKs' default HTTP clients retire idle connections after anywhere from a few seconds to a few minutes depending on the language, all inside the edge's idle window. To shorten the idle window, pass a configured client into the SDK as shown in Configuring a Basis Theory SDK.
Stale connection errors
If a client pool selects a connection after the edge has closed it, the next read can return an unexpected EOF, and the request might never reach the API. OpenSSL reports this as UNEXPECTED_EOF_WHILE_READING. Higher request volume makes this timing race more likely to appear. It does not indicate that an API response was truncated or that the API returned an error.
Timeouts
Set a connect timeout and a per-request timeout deliberately rather than relying on defaults. A connect timeout of 5 seconds separates a network problem from a slow response. Choose a request timeout that matches how long your caller can wait, and remember that a timed-out request may still have been processed. Use an idempotency key on requests you might repeat.
Each SDK exposes a request timeout option. The connect timeout belongs to the underlying HTTP client, which you can pass into the SDK as shown in Configuring a Basis Theory SDK.
Retries
The SDKs retry responses with status codes 408, 429, and 5xx with exponential backoff, honoring Retry-After on rate limited responses. Each exposes an option to change the retry count.
A request that fails before any response arrives, such as a stale connection EOF, has no status code. The Node, .NET, and Go SDKs do not retry it. The Python SDK retries connection and protocol errors up to twice, and the Java SDK's OkHttp client transparently retries a request that fails on a stale pooled connection. If you handle these failures yourself, retry once for idempotent requests, retry a non-idempotent request only when it carries an idempotency key, and record both attempts under the same operation so persistent failures stay visible.
Configuring your own HTTP client
If you call the API without a Basis Theory SDK, apply the same settings to your own HTTP client. Preserve a shared pool instead of creating a new client for every request.
- C#
- Node
- Python
- Ruby
- Go
- Java
var handler = new SocketsHttpHandler
{
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30),
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
ConnectTimeout = TimeSpan.FromSeconds(5),
};
var client = new HttpClient(handler)
{
BaseAddress = new Uri("https://api.basistheory.com"),
Timeout = TimeSpan.FromSeconds(30),
};
PooledConnectionIdleTimeout bounds idle time, and PooledConnectionLifetime bounds the total age of a connection. Reuse the HttpClient for the lifetime of the application.
import { Agent, fetch } from 'undici';
const agent = new Agent({
keepAliveTimeout: 30_000,
keepAliveMaxTimeout: 30_000,
connect: { timeout: 5_000 },
});
const response = await fetch('https://api.basistheory.com/tokens', {
dispatcher: agent,
headers: { 'BT-API-KEY': process.env.BT_API_KEY },
signal: AbortSignal.timeout(30_000),
});
keepAliveTimeout controls idle connections. keepAliveMaxTimeout caps the idle timeout when a server keepalive hint would otherwise increase it; it does not cap a connection's total age.
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
retry = Retry(
total=1,
connect=1,
read=1,
allowed_methods=Retry.DEFAULT_ALLOWED_METHODS,
)
session = requests.Session()
session.mount(
"https://api.basistheory.com",
HTTPAdapter(max_retries=retry, pool_connections=10, pool_maxsize=20),
)
urllib3 does not expose a pooled-connection idle timeout, so Retry handles a stale connection instead: one connection or read failure on its default set of idempotent methods. Pass timeout=(5, 30) on each request for the connect and read timeouts.
response = Typhoeus.get(
"https://api.basistheory.com/tokens",
headers: { "BT-API-KEY" => ENV.fetch("BT_API_KEY") },
forbid_reuse: true,
connecttimeout: 5,
timeout: 30,
)
forbid_reuse avoids the stale-pool race by closing the connection after the request. This costs an extra connection and TLS handshake on the next request. If connection reuse is required, maxconnects can cap libcurl's connection cache, but it does not set an idle timeout or total lifetime. connecttimeout limits only the time spent establishing a new connection.
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.IdleConnTimeout = 30 * time.Second
transport.MaxIdleConnsPerHost = 20
transport.DialContext = (&net.Dialer{Timeout: 5 * time.Second}).DialContext
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
IdleConnTimeout removes connections that remain idle, while MaxIdleConnsPerHost bounds the idle pool for api.basistheory.com. http.Transport has no total connection lifetime setting. Reuse the transport for the lifetime of the application.
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.util.concurrent.TimeUnit;
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(20, 30, TimeUnit.SECONDS))
.connectTimeout(5, TimeUnit.SECONDS)
.callTimeout(30, TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url("https://api.basistheory.com/tokens")
.header("BT-API-KEY", System.getenv("BT_API_KEY"))
.build();
try (Response response = client.newCall(request).execute()) {
// handle response
}
ConnectionPool takes the maximum number of idle connections and how long one may stay idle. OkHttp has no total connection lifetime setting; the idle bound and its default retry of a request that fails on a stale pooled connection (retryOnConnectionFailure) cover stale connections. Reuse the OkHttpClient for the lifetime of the application.
Configuring a Basis Theory SDK
The Basis Theory SDKs use default HTTP clients that keep idle connections inside the bounds the edge allows, so customization is not typically required. If you do need to customize HTTP client configuration, each SDK accepts a preconfigured HTTP client, or a custom fetcher in the case of Node, alongside its timeout and retry options. The examples below apply the same idle and timeout values as above for illustrative purposes.
- C#
- Node
- Python
- Go
- Java
using BasisTheory.Client;
var handler = new SocketsHttpHandler
{
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30),
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
ConnectTimeout = TimeSpan.FromSeconds(5),
};
var client = new BasisTheory(
"<API_KEY>",
clientOptions: new ClientOptions
{
HttpClient = new HttpClient(handler),
Timeout = TimeSpan.FromSeconds(30),
MaxRetries = 2,
});
Reuse the BasisTheory instance for the lifetime of the application.
import { Agent, setGlobalDispatcher } from 'undici';
import { BasisTheoryClient } from '@basis-theory/node-sdk';
setGlobalDispatcher(new Agent({
keepAliveTimeout: 30_000,
keepAliveMaxTimeout: 30_000,
connect: { timeout: 5_000 },
}));
const client = new BasisTheoryClient({
apiKey: '<API_KEY>',
timeoutInSeconds: 30,
maxRetries: 2,
});
The SDK uses the runtime's fetch, so the global undici dispatcher applies to it. To keep the pool private to the SDK, pass a custom fetcher instead. undici has no total connection lifetime setting.
import httpx
from basis_theory import BasisTheory
client = BasisTheory(
api_key="<API_KEY>",
timeout=30,
max_retries=2,
httpx_client=httpx.Client(
limits=httpx.Limits(keepalive_expiry=30.0),
timeout=httpx.Timeout(30.0, connect=5.0),
),
)
httpx defaults keepalive_expiry to 5 seconds, so the idle bound is already in place unless you raise it. httpx has no total connection lifetime setting.
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.IdleConnTimeout = 30 * time.Second
transport.MaxIdleConnsPerHost = 20
transport.DialContext = (&net.Dialer{Timeout: 5 * time.Second}).DialContext
bt := client.NewClient(
option.WithAPIKey("<API_KEY>"),
option.WithHTTPClient(&http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}),
option.WithMaxAttempts(3),
)
IdleConnTimeout removes connections that remain idle, and MaxIdleConnsPerHost bounds the idle pool for api.basistheory.com. http.Transport has no total connection lifetime setting. Reuse the SDK client for the lifetime of the application.
import com.basistheory.BasisTheoryApiClient;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import java.util.concurrent.TimeUnit;
OkHttpClient httpClient = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(20, 30, TimeUnit.SECONDS))
.connectTimeout(5, TimeUnit.SECONDS)
.build();
BasisTheoryApiClient client = BasisTheoryApiClient.builder()
.apiKey("<API_KEY>")
.httpClient(httpClient)
.timeout(30)
.maxRetries(2)
.build();
OkHttp has no total connection lifetime setting; its 30-second idle bound and its default retry of a request that fails on a stale pooled connection (retryOnConnectionFailure) cover stale connections. Reuse the BasisTheoryApiClient for the lifetime of the application.