Appearance
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) | |
|---|---|---|
| Reliability | Reliable, retransmitted | Unreliable, fire-and-forget |
| Per-tx overhead | Open + 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 for | Reliable delivery, large bundles | Single 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.
| Region | IP:Port | Provider |
|---|---|---|
| Frankfurt | 64.130.40.195:8888 (fra.lucum.io) | Teraswitch |
| Frankfurt | 84.32.98.249:8888 (fra2.lucum.io) | Cherry Servers |
| Frankfurt | 70.40.184.133:8888 (fra3.lucum.io) | Allnodes |
| Amsterdam | 64.130.42.36:8888 (ams.lucum.io) | Teraswitch |
| Amsterdam | 108.61.188.23:8888 (ams2.lucum.io) | Vultr |
| New York | 206.223.233.127:8888 (ny.lucum.io) | Latitude |
| Tokyo | 198.13.42.17:8888 (jp.lucum.io) | Vultr |
| London | 64.34.86.109:8888 (lon.lucum.io) | Latitude |
See the full reference at Endpoints & Regions.
Wire format & auth (same as the stream API)
| Setting | Value |
|---|---|
| Transport | QUIC (RFC 9000) over UDP |
| ALPN | lucum-tpu (also accepted: astralane-tpu) |
| SNI server name | lucum |
| Auth | TLS client cert with CN = <api_key> |
| Server cert verification | Skip (server uses self-signed cert) |
| Datagram support | Both 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.
