Documentation menu

Latency & benchmarking

Latency depends mostly on where your server is relative to our infrastructure. Measure it from your own setup before going live and keep measuring in production.

What adds latency

SourceTypical sizeHow to reduce it
Physical distance to the endpointAbout 1 millisecond per 100 km of fiber, round tripHost in the same city or data center. See Regions & endpoints
New connections (TCP and TLS handshakes)One or more round tripsKeep long-lived connections and keepalive
Your own processingFrom microseconds to millisecondsKeep the receive loop minimal and decode on separate workers
Garbage collection and CPU contentionPauses of milliseconds or moreLeave CPU headroom, watch GC pauses, pin hot threads

1. Measure the network path

bash
# Host name of your endpoint, without https:// or a port
mtr --report --report-cycles 100 your-endpoint-host

Look at the last hop: the average and the worst round-trip time, and packet loss. Any loss on the last hop is worth reporting to your hosting provider.

2. Measure the Shreds head start

Shreds are delivered to a server in the same region with a median latency (p50) of 3 milliseconds. How much earlier that is than your current data source depends on the source, so measure it from your own setup.

The most useful comparison is how much earlier you see a transaction in Shreds than through your current data source, for example your RPC node. The example below records when each transaction of a busy program is first seen in each source. Both timestamps come from the same machine, so clock synchronization does not affect the result.

ts
import WebSocket from "ws";
import bs58 from "bs58";
import { Connection, PublicKey, VersionedTransaction } from "@solana/web3.js";

// Any busy program works. Raydium AMM v4 is used here as an example.
const PROGRAM = new PublicKey("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
const rpc = new Connection(process.env.RPC_URL!, { wsEndpoint: process.env.RPC_WS_URL });

const firstSeen = new Map<string, { shreds?: number; rpc?: number }>();
const leadMs: number[] = [];

function record(signature: string, source: "shreds" | "rpc") {
  const entry = firstSeen.get(signature) ?? {};
  if (entry[source] !== undefined) return;
  entry[source] = performance.now();
  firstSeen.set(signature, entry);
  if (entry.shreds !== undefined && entry.rpc !== undefined) {
    leadMs.push(entry.rpc - entry.shreds); // positive: Shreds was earlier
    firstSeen.delete(signature);
  }
}

// Source 1: Shreds
const ws = new WebSocket(process.env.MEV_SHREDS_WS!, {
  headers: { "x-api-key": process.env.MEV_ALPHA_KEY! },
});
ws.on("message", (data) => {
  const batch = JSON.parse(data.toString());
  for (const entry of batch.entries) {
    for (const b64 of entry.transactions) {
      const tx = VersionedTransaction.deserialize(Buffer.from(b64, "base64"));
      // Matches transactions that list the program among their static account keys. Swaps that
      // reach it through an aggregator with the program in a lookup table stay out of the comparison.
      if (tx.message.staticAccountKeys.some((k) => k.equals(PROGRAM))) {
        record(bs58.encode(tx.signatures[0]), "shreds");
      }
    }
  }
});

// Source 2: your RPC, at the fastest commitment level
rpc.onLogs(PROGRAM, (logs) => record(logs.signature, "rpc"), "processed");

// Report percentiles every 30 seconds
setInterval(() => {
  const sorted = [...leadMs].sort((a, b) => a - b);
  const p = (q: number) => sorted[Math.floor(q * (sorted.length - 1))]?.toFixed(1);
  console.log(`n=${sorted.length} p10=${p(0.1)}ms p50=${p(0.5)}ms p90=${p(0.9)}ms`);
  leadMs.length = 0;
  firstSeen.clear();
}, 30_000);

Run the comparison for at least an hour, including busy periods. Compare percentiles, not averages: a single slow message can distort an average.

Each Shreds message also carries created_at_us, the time the entries were reconstructed on our side. With a well-synchronized clock (PTP, or NTP with low offset), Date.now() * 1000 - created_at_us shows the delivery time from our infrastructure to your process.

3. Measure the Private Orderflow head start

Private Orderflow shows a transaction before it is executed, so the useful number is how long before execution you see it. Use the same approach as in the Shreds example above, with Private Orderflow as one more source:

  • Record the first time you see each signature in Private Orderflow: in your StartExpiringPacketStream handler over gRPC, or in the message handler over WebSocket.
  • Record the first time the same signature appears in Shreds or in your RPC at the processed level.
  • The difference is your head start for that transaction. Compare percentiles over at least an hour, including busy periods.

Only transactions that later execute produce a pair. Transactions that fail before execution or never land stay out of the comparison.

For the delivery time from our infrastructure to your process, compare header.ts (gRPC) or received_at_us (WebSocket) with your synchronized clock.

4. Measure Private TPU Send

For transaction delivery, two numbers matter:

  • Landing rate: the share of sent transactions that end up in a block, whether they succeed or fail on-chain.
  • Slots to land: the difference between the slot in which you sent the transaction and the slot in which it was included.
ts
import { Connection } from "@solana/web3.js";

// Call right before sending
export async function sentAtSlot(rpc: Connection): Promise<number> {
  return rpc.getSlot("processed");
}

// Call after the blockhash of the transaction has expired
export async function landedSlot(rpc: Connection, signature: string): Promise<number | null> {
  const tx = await rpc.getTransaction(signature, {
    commitment: "confirmed",
    maxSupportedTransactionVersion: 0,
  });
  return tx ? tx.slot : null; // null: the transaction did not land
}

To compare Private TPU Send with your current sending path, alternate between the two paths for transactions of the same kind, with the same priority fee, at the same time of day. Do not send the same signed transaction through both paths, because then you cannot tell which one landed it.

Measurement pitfalls

  • Unsynchronized clocks. Comparing timestamps from two machines without PTP or well-tuned NTP gives meaningless results. Prefer comparisons on a single machine, as above.
  • Logging on the hot path. Writing every message to the console can be slower than the latency you are trying to measure. Aggregate in memory and print summaries.
  • Cold connections. Discard the first seconds after connecting. The first requests include handshakes.
  • Quiet periods only. Latency and landing rate change under load. Measure during busy hours as well.

Reporting latency issues

If your results are worse than expected, send our support (@ZoriDev):

  • your hosting provider, data center and region
  • an mtr report to the endpoint
  • your percentiles and the time window of the measurement, in UTC

Need help or access? Contact our support on Telegram.

Contact