Skip to content

Sending Transactions via QUIC Datagrams

QUIC datagrams are the fastest way to ship a Solana transaction to lucum.io. A datagram skips stream-open and stream-finish entirely — it's a single unreliable UDP-like packet inside the QUIC connection, written in microseconds on both ends.

Recommended transport

For latency-sensitive flows (arbitrage, MEV, market-making) we recommend QUIC datagrams. They beat both HTTP and the QUIC stream API on round-trip cost.

How Datagrams Work

Stream (open_uni)Datagram (send_datagram)
ReliabilityReliable, retransmittedUnreliable, fire-and-forget
Per-tx overheadOpen + write + finish (3 syscalls)Single write
Typical latency cost~10–30 µs~1 µs
Max payload~MTU bytes (>1232 fits)Negotiated max_datagram_size (≥1232 once advertised)
Best forReliable delivery, large bundlesSingle tx, latency-critical

Solana transactions max out at 1232 bytes — well under the QUIC datagram limit, so every signed tx fits in one datagram.

The recommended pattern is datagram-first with stream fallback: try the datagram path; if the peer hasn't negotiated datagram support yet (rare), fall back to a unidirectional stream automatically.

Endpoints

Connect to any regional IP on port 8888. Using raw IPs skips DNS resolution.

RegionIP:PortProvider
Frankfurt64.130.40.195:8888 (fra.lucum.io)Teraswitch
Frankfurt84.32.98.249:8888 (fra2.lucum.io)Cherry Servers
Frankfurt70.40.184.133:8888 (fra3.lucum.io)Allnodes
Amsterdam64.130.42.36:8888 (ams.lucum.io)Teraswitch
Amsterdam108.61.188.23:8888 (ams2.lucum.io)Vultr
New York206.223.233.127:8888 (ny.lucum.io)Latitude
Tokyo198.13.42.17:8888 (jp.lucum.io)Vultr
London64.34.86.109:8888 (lon.lucum.io)Latitude

See the full reference at Endpoints & Regions.

Wire format & auth (same as the stream API)

SettingValue
TransportQUIC (RFC 9000) over UDP
ALPNlucum-tpu (also accepted: astralane-tpu)
SNI server namelucum
AuthTLS client cert with CN = <api_key>
Server cert verificationSkip (server uses self-signed cert)
Datagram supportBoth sides must advertise it (see code)

The server only inspects the cert's CN to identify the API key — there's no separate handshake message.

Rust

The full client below is drop-in: copy it into your project as lucum_quic_client.rs, then use the example at the bottom. It does connect-with-retry, datagram-first send with stream fallback, automatic reconnect, 0-RTT resume, and clean shutdown.

Cargo.toml

toml
[dependencies]
anyhow = "1"
arc-swap = "1"
bytes = "1"
quinn = { version = "0.11", features = ["rustls"] }
rcgen = "0.13"
rustls = { version = "0.23", features = ["ring"] }
tokio = { version = "1", features = ["full"] }
solana-sdk = "2.0"
solana-client = "2.0"
bincode = "1.3"

lucum_quic_client.rs

rust
//! Lucum QUIC Client — datagram-first, with stream fallback.

use anyhow::{anyhow, Context, Result};
use arc_swap::ArcSwap;
use quinn::{
    crypto::rustls::QuicClientConfig, ClientConfig, Connection, Endpoint, IdleTimeout,
    TransportConfig,
};
use rcgen::{CertificateParams, DnType, DnValue, KeyPair};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use std::{net::SocketAddr, sync::Arc, time::Duration};
use tokio::sync::Mutex;

const ALPN_LUCUM_TPU: &[u8] = b"lucum-tpu";
const SNI_SERVER_NAME: &str = "lucum";
const KEEP_ALIVE: Duration = Duration::from_secs(15);
const IDLE_TIMEOUT: Duration = Duration::from_secs(60);
const SEND_MAX_ATTEMPTS: u32 = 5;
const CONNECT_MAX_ATTEMPTS: u32 = 5;

pub const MAX_TX_BYTES: usize = 1232;

pub struct LucumClient {
    endpoint: Endpoint,
    config: ClientConfig,
    addr: SocketAddr,
    sni: &'static str,
    connection: ArcSwap<Connection>,
    reconnecting: Mutex<()>,
}

impl LucumClient {
    pub async fn connect(endpoint_addr: &str, api_key: &str) -> Result<Self> {
        let _ = rustls::crypto::ring::default_provider().install_default();

        let addr = resolve_endpoint(endpoint_addr).await?;
        let config = build_client_config(api_key)?;

        let mut endpoint = Endpoint::client("0.0.0.0:0".parse().unwrap())
            .context("failed to bind QUIC endpoint")?;
        endpoint.set_default_client_config(config.clone());

        let connection = handshake_with_retries(
            &endpoint,
            &config,
            addr,
            SNI_SERVER_NAME,
            CONNECT_MAX_ATTEMPTS,
        )
        .await
        .context("initial QUIC handshake failed after retries")?;

        Ok(Self {
            endpoint,
            config,
            addr,
            sni: SNI_SERVER_NAME,
            connection: ArcSwap::from_pointee(connection),
            reconnecting: Mutex::new(()),
        })
    }

    /// Datagram-first send with stream fallback. Reconnects + retries on failure.
    #[inline]
    pub async fn send_transaction(&self, tx_bytes: &[u8]) -> Result<()> {
        let mut last_err: Option<anyhow::Error> = None;
        for attempt in 0..SEND_MAX_ATTEMPTS {
            let conn = self.connection.load_full();
            match write_uni(&conn, tx_bytes).await {
                Ok(()) => return Ok(()),
                Err(e) => last_err = Some(e),
            }
            if attempt + 1 < SEND_MAX_ATTEMPTS {
                tokio::time::sleep(backoff(attempt)).await;
                if let Err(e) = self.reconnect().await {
                    last_err = Some(e);
                }
            }
        }
        Err(last_err.unwrap_or_else(|| anyhow!("send_transaction exhausted retries")))
    }

    pub async fn reconnect(&self) -> Result<()> {
        let _guard = self.reconnecting.lock().await;
        if self.connection.load().close_reason().is_none() {
            return Ok(());
        }
        let new_conn = handshake_with_retries(
            &self.endpoint,
            &self.config,
            self.addr,
            self.sni,
            SEND_MAX_ATTEMPTS,
        )
        .await
        .context("reconnect failed after retries")?;
        self.connection.store(Arc::new(new_conn));
        Ok(())
    }

    pub fn is_connected(&self) -> bool {
        self.connection.load().close_reason().is_none()
    }

    pub async fn close(&self) {
        self.connection
            .load()
            .close(0u32.into(), b"client shutdown");
        self.endpoint.wait_idle().await;
    }
}

impl Drop for LucumClient {
    fn drop(&mut self) {
        self.connection.load().close(0u32.into(), b"dropped");
    }
}

async fn resolve_endpoint(addr: &str) -> Result<SocketAddr> {
    if let Ok(parsed) = addr.parse::<SocketAddr>() {
        return Ok(parsed);
    }
    tokio::net::lookup_host(addr)
        .await
        .with_context(|| format!("failed to resolve {addr}"))?
        .next()
        .ok_or_else(|| anyhow!("no addresses returned for {addr}"))
}

/// Datagram first, stream fallback.
#[inline]
async fn write_uni(connection: &Connection, payload: &[u8]) -> Result<()> {
    if let Some(max_dgram) = connection.max_datagram_size() {
        if payload.len() <= max_dgram
            && connection
                .send_datagram(bytes::Bytes::copy_from_slice(payload))
                .is_ok()
        {
            return Ok(());
        }
    }
    let mut stream = connection.open_uni().await?;
    stream.write_all(payload).await?;
    stream.finish()?;
    Ok(())
}

async fn handshake_with_retries(
    endpoint: &Endpoint,
    config: &ClientConfig,
    addr: SocketAddr,
    sni: &'static str,
    max_attempts: u32,
) -> Result<Connection> {
    let mut last_err: Option<anyhow::Error> = None;
    for attempt in 0..max_attempts {
        match endpoint.connect_with(config.clone(), addr, sni) {
            Ok(connecting) => match connecting.await {
                Ok(conn) => return Ok(conn),
                Err(e) => last_err = Some(anyhow!(e)),
            },
            Err(e) => last_err = Some(anyhow!(e)),
        }
        if attempt + 1 < max_attempts {
            tokio::time::sleep(backoff(attempt)).await;
        }
    }
    Err(last_err.unwrap_or_else(|| anyhow!("handshake_with_retries exhausted")))
}

#[inline]
fn backoff(attempt: u32) -> Duration {
    match attempt {
        0 => Duration::from_micros(0),
        1 => Duration::from_millis(1),
        2 => Duration::from_millis(4),
        3 => Duration::from_millis(16),
        _ => Duration::from_millis(64),
    }
}

fn build_client_config(api_key: &str) -> Result<ClientConfig> {
    let (cert, key) = generate_client_certificate(api_key)?;

    let mut crypto = rustls::ClientConfig::builder()
        .dangerous()
        .with_custom_certificate_verifier(Arc::new(AcceptAnyServer))
        .with_client_auth_cert(vec![cert], key)
        .context("rustls client auth setup failed")?;
    crypto.alpn_protocols = vec![ALPN_LUCUM_TPU.to_vec()];
    crypto.enable_early_data = true;

    let quic_crypto = QuicClientConfig::try_from(crypto)
        .context("rustls -> quic config conversion failed")?;
    let mut client_config = ClientConfig::new(Arc::new(quic_crypto));

    let mut transport = TransportConfig::default();
    transport.keep_alive_interval(Some(KEEP_ALIVE));
    transport.max_idle_timeout(Some(IdleTimeout::try_from(IDLE_TIMEOUT)?));
    transport.initial_rtt(Duration::from_millis(20));
    // Advertise datagram support so the peer negotiates it.
    transport.datagram_receive_buffer_size(Some(1024 * 1024));
    client_config.transport_config(Arc::new(transport));

    Ok(client_config)
}

/// Self-signed ECDSA P-256 cert with `CN = <api_key>`.
fn generate_client_certificate(
    api_key: &str,
) -> Result<(CertificateDer<'static>, PrivateKeyDer<'static>)> {
    let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)
        .context("ecdsa key generation failed")?;

    let mut params = CertificateParams::new(Vec::<String>::new())
        .context("cert params init failed")?;
    params
        .distinguished_name
        .push(DnType::CommonName, DnValue::Utf8String(api_key.to_string()));

    let cert = params.self_signed(&key_pair).context("self-sign failed")?;
    let cert_der = CertificateDer::from(cert.der().to_vec());
    let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der()));
    Ok((cert_der, key_der))
}

/// Trust whatever cert the server presents. Trust is established by the
/// operator publishing the right `host:port` — no public CA chain.
#[derive(Debug)]
struct AcceptAnyServer;

impl rustls::client::danger::ServerCertVerifier for AcceptAnyServer {
    fn verify_server_cert(
        &self,
        _end_entity: &CertificateDer<'_>,
        _intermediates: &[CertificateDer<'_>],
        _server_name: &rustls::pki_types::ServerName<'_>,
        _ocsp: &[u8],
        _now: rustls::pki_types::UnixTime,
    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
        Ok(rustls::client::danger::ServerCertVerified::assertion())
    }

    fn verify_tls12_signature(
        &self,
        _msg: &[u8],
        _cert: &CertificateDer<'_>,
        _dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
    }

    fn verify_tls13_signature(
        &self,
        _msg: &[u8],
        _cert: &CertificateDer<'_>,
        _dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
    }

    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
        use rustls::SignatureScheme::*;
        vec![
            ECDSA_NISTP256_SHA256,
            ECDSA_NISTP384_SHA384,
            ED25519,
            RSA_PSS_SHA256,
            RSA_PSS_SHA384,
            RSA_PSS_SHA512,
            RSA_PKCS1_SHA256,
            RSA_PKCS1_SHA384,
            RSA_PKCS1_SHA512,
        ]
    }
}

Usage

rust
mod lucum_quic_client;
use lucum_quic_client::LucumClient;

use solana_sdk::{
    signature::{Keypair, Signer},
    system_instruction,
    transaction::Transaction,
};
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = "YOUR_API_KEY";
    let endpoint = "64.130.40.195:8888"; // fra.lucum.io — raw IP avoids DNS

    // One-time connect. Cheap to share across tasks via Arc.
    let client = Arc::new(LucumClient::connect(endpoint, api_key).await?);

    // Build a tx with the required tip
    let sender = Keypair::new();
    let recipient = solana_sdk::pubkey!("RECIPIENT_PUBKEY");
    let tip_wallet = solana_sdk::pubkey!("Lucum3sDVsPmHnQVaRKGpLXVPQLhcUqJqmcN5Tn9xuR");

    let rpc = solana_client::rpc_client::RpcClient::new(
        "https://api.mainnet-beta.solana.com",
    );
    let blockhash = rpc.get_latest_blockhash()?;

    let tx = Transaction::new_signed_with_payer(
        &[
            system_instruction::transfer(&sender.pubkey(), &recipient, 10_000_000),
            // Tip: 0.001 SOL minimum
            system_instruction::transfer(&sender.pubkey(), &tip_wallet, 1_000_000),
        ],
        Some(&sender.pubkey()),
        &[&sender],
        blockhash,
    );
    let tx_bytes = bincode::serialize(&tx)?;

    // Hot path: datagram-first, stream fallback. ~1µs per send.
    client.send_transaction(&tx_bytes).await?;
    println!("sent");

    // Reuse the same client for subsequent sends — do not reconnect per tx.
    Ok(())
}

Multi-server send (highest land rate)

Send each tx through its own LucumClient. The client only knows about one endpoint; the multi-server fan-out is your application's job:

rust
// One client per server, reused across many sends.
let clients = [
    Arc::new(LucumClient::connect("64.130.40.195:8888", api_key).await?), // fra
    Arc::new(LucumClient::connect("84.32.98.249:8888", api_key).await?),  // fra2
    Arc::new(LucumClient::connect("70.40.184.133:8888", api_key).await?), // fra3
];

// Build N independently-signed transactions. Use a fresh nonce/blockhash
// per tx so the validator treats them as distinct — otherwise the second
// and third arrivals are deduplicated.
let txs: Vec<Vec<u8>> = build_independent_txs(&clients.len()).await?; // your code

// Fan out: each server gets its own tx.
let _: Vec<()> = futures::future::try_join_all(
    clients.iter().zip(txs.iter()).map(|(c, tx)| c.send_transaction(tx)),
)
.await?;

Go

go
// go.mod
//   require github.com/quic-go/quic-go v0.48.0
package main

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"errors"
	"fmt"
	"math/big"
	"sync"
	"time"

	"github.com/quic-go/quic-go"
	"golang.org/x/sync/errgroup"
)

const (
	alpn      = "lucum-tpu"
	sni       = "lucum"
	keepAlive = 15 * time.Second
	idle      = 60 * time.Second
)

type LucumClient struct {
	addr       string
	apiKey     string
	tlsConfig  *tls.Config
	quicConfig *quic.Config

	mu   sync.RWMutex
	conn quic.Connection
}

func Connect(ctx context.Context, addr, apiKey string) (*LucumClient, error) {
	cert, err := generateClientCert(apiKey)
	if err != nil {
		return nil, fmt.Errorf("generate cert: %w", err)
	}
	tlsCfg := &tls.Config{
		Certificates:       []tls.Certificate{cert},
		InsecureSkipVerify: true, // server uses self-signed cert
		NextProtos:         []string{alpn},
		ServerName:         sni,
	}
	quicCfg := &quic.Config{
		MaxIdleTimeout:        idle,
		KeepAlivePeriod:       keepAlive,
		EnableDatagrams:       true,
		MaxIncomingUniStreams: -1,
	}
	c := &LucumClient{addr: addr, apiKey: apiKey, tlsConfig: tlsCfg, quicConfig: quicCfg}
	if err := c.dial(ctx); err != nil {
		return nil, err
	}
	return c, nil
}

func (c *LucumClient) SendTransaction(ctx context.Context, txBytes []byte) error {
	var lastErr error
	for attempt := 0; attempt < 5; attempt++ {
		c.mu.RLock()
		conn := c.conn
		c.mu.RUnlock()

		if conn == nil {
			lastErr = errors.New("not connected")
		} else if err := writeFastest(ctx, conn, txBytes); err == nil {
			return nil
		} else {
			lastErr = err
		}
		if attempt+1 < 5 {
			time.Sleep(backoff(attempt))
			if err := c.reconnect(ctx); err != nil {
				lastErr = err
			}
		}
	}
	return lastErr
}

func writeFastest(ctx context.Context, conn quic.Connection, payload []byte) error {
	if maxDgram, err := conn.MaxDatagramSize(); err == nil && len(payload) <= int(maxDgram) {
		if err := conn.SendDatagram(payload); err == nil {
			return nil
		}
	}
	stream, err := conn.OpenUniStreamSync(ctx)
	if err != nil {
		return fmt.Errorf("open uni: %w", err)
	}
	if _, err := stream.Write(payload); err != nil {
		_ = stream.Close()
		return fmt.Errorf("stream write: %w", err)
	}
	return stream.Close()
}

func (c *LucumClient) dial(ctx context.Context) error {
	conn, err := quic.DialAddr(ctx, c.addr, c.tlsConfig, c.quicConfig)
	if err != nil {
		return fmt.Errorf("dial %s: %w", c.addr, err)
	}
	c.mu.Lock()
	c.conn = conn
	c.mu.Unlock()
	return nil
}

func (c *LucumClient) reconnect(ctx context.Context) error {
	dialCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()
	return c.dial(dialCtx)
}

func (c *LucumClient) Close() {
	c.mu.RLock()
	conn := c.conn
	c.mu.RUnlock()
	if conn != nil {
		_ = conn.CloseWithError(0, "client shutdown")
	}
}

func backoff(attempt int) time.Duration {
	switch attempt {
	case 0:
		return 0
	case 1:
		return 1 * time.Millisecond
	case 2:
		return 4 * time.Millisecond
	case 3:
		return 16 * time.Millisecond
	default:
		return 64 * time.Millisecond
	}
}

func generateClientCert(apiKey string) (tls.Certificate, error) {
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return tls.Certificate{}, err
	}
	tmpl := &x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject:      pkix.Name{CommonName: apiKey},
		NotBefore:    time.Now(),
		NotAfter:     time.Now().Add(365 * 24 * time.Hour),
	}
	der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
	if err != nil {
		return tls.Certificate{}, err
	}
	return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key, Leaf: tmpl}, nil
}

// Usage — single endpoint, reuse one client across many sends.
func main() {
	ctx := context.Background()
	apiKey := "YOUR_API_KEY"

	client, err := Connect(ctx, "64.130.40.195:8888", apiKey) // fra.lucum.io
	if err != nil {
		panic(err)
	}
	defer client.Close()

	// txBytes = bincode-serialized signed Solana transaction (max 1232 bytes)
	// Must include a tip transfer to one of the Lucum tip wallets.
	txBytes := []byte{ /* your transaction */ }

	if err := client.SendTransaction(ctx, txBytes); err != nil {
		fmt.Println("send failed:", err)
		return
	}
	fmt.Println("sent")
}

Tips

  • Reuse one client per server. Don't reconnect per tx — the QUIC connection amortizes across thousands of sends.
  • Use raw IPs to skip the DNS lookup (~1 ms saved on reconnect).
  • Datagrams are unreliable, but Solana transactions retry naturally via blockhash expiry. The win from skipping a stream open/close beats the rare datagram drop.
  • 0-RTT resume (Rust client) kicks in automatically on reconnects within the session ticket lifetime.

Built with VitePress