# How to Receive Solana Shreds over UDP: Rust and Go Examples

To receive Solana shreds over UDP, run a UDP listener on a reachable server, allow traffic to its receiving port, and configure your shred provider with the server's public IPv4 address and port. Start by proving that packets reach your application. Then add shred validation, deduplication, reconstruction and the state checks your searcher needs.

This guide gets you through that first boundary with small Rust and Go receivers. It also explains what successful reception does **not** prove: a datagram is not a complete transaction, and a reconstructed transaction is not proof of execution or finality.

## What are Solana shreds?

Shreds are pieces of Solana's block-distribution data. Data shreds carry serialized ledger data; coding shreds provide redundancy that can help recover missing data. A transaction can span shred boundaries, and an individual datagram is not a JSON transaction event.

For a searcher, raw shreds are an input to a reconstruction pipeline. They are not a public mempool of user-submitted pending transactions, and receiving them does not give you private order flow or guaranteed execution priority.

See the [Agave shred implementation](https://github.com/anza-xyz/agave/blob/master/ledger/src/shred.rs) for the protocol structures and the [Turbine overview](https://beta.docs.solanalabs.com/consensus/turbine-block-propagation) for the propagation model. Use a parser compatible with the network and shred variants you actually receive rather than copying fixed byte offsets from an old blog post.

## Before you start

You need:

- A server you control, with a reachable public IPv4 address and permission to receive traffic there.
- An unused UDP port. We use `20000` as an example; it is not a required Shredo port.
- Access to your host firewall and any provider-level firewall or NAT configuration.
- Rust or Go installed for the receiver you choose.
- An active shred feed configured to deliver to that destination.

[Shredo Raw Shreds](https://shredo.xyz/products/raw-shreds/) costs **99 USDC per calendar month** and delivers deduplicated raw UDP shreds to one IPv4:port destination. You do not need to run a validator to receive the feed. Transaction reconstruction, historical replay and a delivery SLA are not included in the base subscription. The [Raw Shreds documentation](https://docs.shredo.xyz/products/raw-shreds) is the current integration contract.

## 1. Choose the receiving address and UDP port

Use the server's **public IPv4** in the provider's destination settings. The listener usually binds to `0.0.0.0:20000`, which means all local IPv4 interfaces. Do not enter `0.0.0.0`, `127.0.0.1` or a private address as your public delivery destination.

If the public IP belongs to a NAT gateway, forward UDP port `20000` to the receiver's private address and port. A TCP port-forwarding rule will not work for a UDP feed.

When your firewall uses source allowlists, obtain the current relay source addresses from the provider. Do not invent them or assume a website's IP is the feed's source IP. Use an appropriately scoped UDP ingress rule and do not disable your firewall to debug reception.

Before starting a listener on Linux, check whether another process owns the port:

```sh
ss -u -a -n -p 'sport = :20000'
```

Run only one of the following receivers at a time on a given address and port.

## 2. Start a minimal Rust receiver

Save this as `receiver.rs`:

```rust
use std::env;
use std::io::{self, ErrorKind};
use std::net::UdpSocket;
use std::time::{Duration, Instant};

fn main() -> io::Result<()> {
    let bind = env::args()
        .nth(1)
        .unwrap_or_else(|| "0.0.0.0:20000".to_owned());
    let socket = UdpSocket::bind(&bind)?;
    socket.set_read_timeout(Some(Duration::from_secs(1)))?;
    eprintln!("Listening on {}", socket.local_addr()?);

    let mut buffer = [0_u8; 65_535];
    let mut packets = 0_u64;
    let mut bytes = 0_u64;
    let mut since = Instant::now();

    loop {
        match socket.recv_from(&mut buffer) {
            Ok((n, _peer)) => {
                packets += 1;
                bytes += n as u64;
                // Inspect only &buffer[..n], not the unused tail.
                // A production consumer must validate and reconstruct shreds here.
            }
            Err(error) if error.kind() == ErrorKind::Interrupted => continue,
            Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => {}
            Err(error) => return Err(error),
        }

        let elapsed = since.elapsed();
        if elapsed >= Duration::from_secs(1) {
            println!(
                "interval_s={:.2} packets={} bytes={}",
                elapsed.as_secs_f64(),
                packets,
                bytes
            );
            packets = 0;
            bytes = 0;
            since = Instant::now();
        }
    }
}
```

Compile and start it:

```sh
rustc --edition=2021 -O receiver.rs -o shred-receiver-rust
./shred-receiver-rust 0.0.0.0:20000
```

The receiver uses only Rust's standard library. It counts datagrams and UDP payload bytes, reporting at approximately one-second intervals. A quiet socket also produces a report, so a stopped feed does not look like a frozen terminal.

The 65,535-byte application buffer avoids assuming a particular shred-payload size. It is **not** the kernel socket receive queue and does not protect against bursts that overflow that queue. Rust's [`recv_from`](https://doc.rust-lang.org/std/net/struct.UdpSocket.html#method.recv_from) can truncate a datagram if the supplied buffer is too small.

## 3. Or use a minimal Go receiver

Save this as `receiver.go`:

```go
package main

import (
	"errors"
	"fmt"
	"net"
	"os"
	"time"
)

func run(bind string) error {
	address, err := net.ResolveUDPAddr("udp4", bind)
	if err != nil {
		return err
	}
	conn, err := net.ListenUDP("udp4", address)
	if err != nil {
		return err
	}
	defer conn.Close()
	fmt.Fprintln(os.Stderr, "Listening on", conn.LocalAddr())
	buffer := make([]byte, 65_535)
	var packets, bytes uint64
	since := time.Now()
	nextReport := since.Add(time.Second)
	if err := conn.SetReadDeadline(nextReport); err != nil {
		return err
	}

	for {
		n, _, err := conn.ReadFromUDP(buffer)
		if err != nil {
			var netError net.Error
			if !errors.As(err, &netError) || !netError.Timeout() {
				return err
			}
		} else {
			packets++
			bytes += uint64(n)
			// Inspect only buffer[:n]. Do not retain it after the next read.
			// Validation and reconstruction are separate production stages.
		}

		now := time.Now()
		if !now.Before(nextReport) {
			fmt.Printf("interval_s=%.2f packets=%d bytes=%d\n", now.Sub(since).Seconds(), packets, bytes)
			packets, bytes = 0, 0
			since = now
			nextReport = now.Add(time.Second)
			if err := conn.SetReadDeadline(nextReport); err != nil {
				return err
			}
		}
	}
}

func main() {
	bind := "0.0.0.0:20000"
	if len(os.Args) > 1 {
		bind = os.Args[1]
	}
	if err := run(bind); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}
```

Build and run it:

```sh
go build -o shred-receiver-go receiver.go
./shred-receiver-go 0.0.0.0:20000
```

This version uses Go's standard [`net` package](https://pkg.go.dev/net#UDPConn.ReadFromUDP). It reads continuously and reports aggregate counts instead of printing every packet. Read deadlines let it report quiet intervals as well.

Both examples are **reception diagnostics**, not production deshredders. They do not validate signatures, recognize shred identities, recover missing data, decode transactions, or send traffic back to the source. The counters include any UDP datagram that reaches the socket, not just valid Solana shreds.

## 4. Configure delivery in the Shredo console

1. Open [Raw Shreds in the console](https://console.shredo.xyz/?product=raw-shreds), purchase access and pay the invoice in native USDC on Solana.
2. Press **Check payment** after your transfer finalizes.
3. Open Raw Shreds under **My services** and save your receiving server's public IPv4 and UDP port.
4. Check that delivery is not paused. Compare the selected destination with the relay-confirmed destination shown in the service settings.
5. Watch the receiver's packet and byte counters.

There is no HTTP endpoint to poll for raw shreds, and these UDP receivers do not need a Shredo API key. Delivery is configured for the destination on your subscription. Pausing delivery does not pause the paid subscription period.

## 5. Verify the network path independently

On Linux, use a bounded packet capture on the receiving host:

```sh
sudo tcpdump -ni any -c 20 'udp dst port 20000'
```

This stops after 20 matching packets; press Ctrl+C if none arrive. Avoid dumping payloads or capturing unrelated traffic on a shared host.

Compare three separate observations:

| Observation | What it tells you |
| --- | --- |
| A confirmed destination in the console | The service has applied a route to that address and port |
| Matching packets in the host capture | Datagrams reach the capture point on that host |
| Increasing application counters | Your process receives datagrams on its socket |

A packet capture does not prove application delivery, and application delivery does not prove valid, complete ledger data. Keep those boundaries separate when troubleshooting.

### No packets in the capture

Check the public IP, UDP port, service access and pause state, provider firewall, host firewall and NAT forwarding. If you need provider support, send the destination, UTC interval and observed counters, not wallet keys or API secrets.

### Packets in the capture, but no application traffic

Check the listener's bind address and port, whether it is inside a different container/network namespace, competing listeners and the firewall path after the capture point. A listener bound only to loopback cannot receive traffic arriving on the public interface.

### Packets arrive, but reconstructed transactions are missing

That is a different failure boundary. Check malformed or unsupported shreds, shred-version expectations, incomplete recovery groups, conflicting payloads, downstream queues and transaction-parser compatibility. Do not infer a network outage solely from an empty decoded output.

## 6. Check for drops before adding more work

UDP does not provide ordered, retransmitted delivery. A successful `recv_from` or `ReadFromUDP` call says nothing about packets that never reached the process.

On Linux, inspect socket memory and UDP counters while the feed runs:

```sh
ss -u -a -n -m 'sport = :20000'
nstat -az UdpInErrors UdpRcvbufErrors
sysctl net.core.rmem_max
```

Compare counter changes over the same interval. The `nstat` values are shared by the network namespace, so they are not automatically attributable to this receiver. See the official manuals for [`ss`](https://man7.org/linux/man-pages/man8/ss.8.html) and [`nstat`](https://man7.org/linux/man-pages/man8/nstat.8.html). Missing tools can be installed through your distribution's normal packages.

The `rmem_max` value limits an application's normal receive-buffer request. Raising that ceiling alone does not resize an existing socket's queue or solve a sustained processing deficit. A production receiver should request an appropriate `SO_RCVBUF`, verify the effective value and measure drops under bursts. Linux reports a doubled buffer value to include bookkeeping overhead; see the [`socket(7)` reference](https://www.man7.org/linux/man-pages/man7/socket.7.html).

Avoid per-packet terminal output, synchronous database writes and remote calls in the receiving loop. If you hand packets to workers, use bounded queues with an explicit overflow policy. Do not hand another worker a slice of the reusable read buffer and then overwrite it on the next receive; transfer ownership or copy into a bounded buffer pool.

The snippets above are a first connectivity check. They are not a throughput benchmark, and their packet counters do not measure end-to-end completeness or latency.

## 7. Turn packets into a searcher input

A production raw-shred pipeline usually needs the following stages:

1. **Validate the transport payload.** Enforce size and format bounds, the expected network/shred version and the applicable signature/Merkle checks using a compatible parser and trusted leader information.
2. **Track identities and conflicts.** Bound retained state by slot/window. Do not merge different payloads merely because their slot and index match; distinguish shred type and handle conflicts explicitly.
3. **Recover and reconstruct.** Group compatible data and coding shreds, recover where enough information is present and reconstruct complete supported ledger ranges. Missing data may remain unrecoverable.
4. **Parse signed transactions.** Handle supported message versions and Address Lookup Tables; unresolved addresses must remain explicit rather than silently producing incomplete filters.
5. **Update and verify strategy state.** A transaction observation alone is not a fresh set of executable pool balances. Use the state and execution checks your strategy requires.
6. **Simulate and submit separately.** Receiving a transaction does not reserve a position, guarantee a backrun, or guarantee landing or profit.

[Pool Catalogue](https://shredo.xyz/products/pool-catalogue/) can supply an inventory of pool identities and dependency accounts. It does not replace a live pool-state feed. See [our public-order-flow guide](https://shredo.xyz/blog/solana-public-order-flow/) for the distinction between raw shreds and reconstructed transaction observations.

## When Decoded Transactions is the simpler choice

Use raw shreds when owning the reconstruction layer is important to your system. If your next task is filtering signed transaction observations by program or account, maintaining a deshredder may not be the best first step.

[Shredo Decoded Transactions](https://shredo.xyz/products/decoded-transactions/) costs **199 USDC per calendar month** and exposes reconstructed signed transactions through Shredo's versioned TLS gRPC protocol, with program/account filters and explicit gap events. Your client initiates the connection, so no public receiving IP or UDP port is required. It is a separate product; a Raw Shreds subscription is not a prerequisite.

Decoded observations still do not prove execution, success or finality. DEX interpretation, current executable state and submission remain separate concerns. Read the [gRPC integration documentation](https://docs.shredo.xyz/products/decoded-transactions) before choosing your client.

## Frequently asked questions

### Do I need a Solana validator to receive shreds?

Not for Shredo Raw Shreds. You need a receiving server, a reachable public IPv4 and UDP port, and active access configured to that destination.

### Is every UDP packet a transaction?

No. Shreds carry pieces of ledger data and recovery information. Reconstruct complete supported data ranges and parse transactions before using them as transaction events.

### Can I replay missed raw shreds from Shredo?

Historical replay and UDP retransmission are not included in the base Raw Shreds subscription. Make gaps visible and design a safe recovery or resynchronization path.

### Is ShredStream the same as private order flow?

No. A feed of observed block-distribution shreds is not a private feed of transactions before leader inclusion. Do not infer private access, ordering rights or execution guarantees from the name of a data product.

### Which product should a new searcher start with?

Choose the layer you are prepared to operate: raw UDP when you want to own reconstruction, or Decoded Transactions when you want a signed-transaction interface. Compare the integration work and the rest of the strategy pipeline, not just the subscription price. [View current pricing](https://shredo.xyz/pricing/).
