BotTx|Documentation

Advanced

Keep-Alive: Cutting Connection Overhead

Every new HTTPS connection costs a TCP handshake plus a TLS handshake — often 40-150ms before your first byte is even sent. For high-frequency workloads, reusing a single persistent connection eliminates this overhead entirely.

Why it matters

TCP handshake

Three round trips before any data flows. On a transatlantic path, that's 200ms+ of pure setup.

TLS handshake

Another round trip for TLS 1.3 (two for TLS 1.2), plus certificate validation — typically 30-80ms.

TCP slow start

A fresh connection ramps up congestion window slowly. A persistent connection delivers your payload at full speed immediately.

On a single transaction this matters less. On the hundredth transaction per minute, a persistent connection vs. a fresh one is the difference between landing on the current slot or the next one.

How BoltTx supports Keep-Alive

Every BoltTx endpoint supports HTTP/1.1 Keep-Alive and HTTP/2 multiplexing. Connections stay open for up to 60 seconds of idle time before the server closes them. To keep the connection warm across longer gaps, send a cheap probe to the health endpoint every 30-45 seconds:

Health probe endpoint

GET/v1/health
curl https://bolttx.io/v1/health
# -> 200 OK  "ok"

The health endpoint is unauthenticated, unrated, and free — it does not count toward your TPS quota or tip accounting. Use it freely.

Per-language setup

In every runtime, the goal is the same: create the HTTP client once, reuse it for every transaction. Never build a new client per request.

Node.js / Bun — native fetch

Modern fetch implementations reuse connections automatically via a global agent. Just use the same module-level reference for every call.

// bolttx-client.ts — created once at module load
const API_KEY = process.env.BOLTTX_API_KEY!;
const ENDPOINT = "https://bolttx.io/v1/send";

export async function sendTx(signedBase64: string) {
  // fetch reuses the global undici Agent — no per-call setup cost
  const res = await fetch(ENDPOINT, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
      "Connection": "keep-alive",
    },
    body: JSON.stringify({ transaction: signedBase64 }),
  });
  return res.json();
}

// Optional: warm probe every 30s for bursty workloads
setInterval(() => fetch("https://bolttx.io/v1/health"), 30_000);

Python — requests.Session

Create one Session object at module load and reuse it. requests.get/post without a session opens a new connection every call.

import requests, threading, time

# One session for the whole process
_session = requests.Session()
_session.headers.update({
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
})

def send_tx(signed_b64: str):
    return _session.post(
        "https://bolttx.io/v1/send",
        json={"transaction": signed_b64},
        timeout=5,
    ).json()

# Optional warm probe
def _keepalive():
    while True:
        _session.get("https://bolttx.io/v1/health", timeout=5)
        time.sleep(30)

threading.Thread(target=_keepalive, daemon=True).start()

Rust — reqwest::Client

Clone the Client — internally it's an Arc, so clones share the same connection pool. Never call Client::new() in a hot path.

use reqwest::Client;
use once_cell::sync::Lazy;

// Build once, clone cheaply — shared connection pool.
pub static HTTP: Lazy<Client> = Lazy::new(|| {
    Client::builder()
        .pool_idle_timeout(std::time::Duration::from_secs(60))
        .pool_max_idle_per_host(32)
        .http2_prior_knowledge()
        .build()
        .expect("reqwest client")
});

pub async fn send_tx(signed_b64: &str) -> reqwest::Result<serde_json::Value> {
    HTTP.post("https://bolttx.io/v1/send")
        .bearer_auth(&*API_KEY)
        .json(&serde_json::json!({ "transaction": signed_b64 }))
        .send()
        .await?
        .json()
        .await
}

Go — http.Client with Transport

Reuse a single http.Client (or the default http.DefaultClient). Go's default Transport pools connections automatically.

package bolttx

import (
    "bytes"
    "encoding/json"
    "net/http"
    "time"
)

// Package-level client — reuse for every call.
var client = &http.Client{
    Timeout: 5 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        64,
        MaxIdleConnsPerHost: 32,
        IdleConnTimeout:     60 * time.Second,
        ForceAttemptHTTP2:   true,
    },
}

func SendTx(signedB64 string) (map[string]any, error) {
    body, _ := json.Marshal(map[string]string{"transaction": signedB64})
    req, _ := http.NewRequest("POST", "https://bolttx.io/v1/send", bytes.NewReader(body))
    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("Content-Type", "application/json")

    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var out map[string]any
    return out, json.NewDecoder(resp.Body).Decode(&out)
}

Verifying it's working

Measure end-to-end latency of your first request versus the tenth request in quick succession. With Keep-Alive working correctly, the difference between them should be dominated by network RTT (single-digit milliseconds), not connection setup (tens to hundreds of milliseconds). If the tenth request is still slow, your client is opening fresh connections every time.

Common mistakes

  • Creating a new fetch/requests/reqwest client inside your send function — this defeats all pooling.
  • Using short-lived serverless functions (e.g., some edge runtimes) that cannot retain connections across invocations — consider a warm-worker architecture or accept the overhead.
  • Aggressive load balancers or proxies between you and BoltTx that terminate Keep-Alive — test the end-to-end path, not just your client's configuration.

Further reading