Skip to content
Back
Migrate

Migrate from Geyser gRPC to raw UDP shreds

Convert a Geyser gRPC pipeline into raw shred intake by rebuilding transport, reconstruction, filtering, overload control, and cutover observability.

Plan the receiver change

Moving from Geyser gRPC to shredstream.sh changes the transport boundary. A gRPC consumer receives decoded objects through a managed channel with order, flow control, status, and reconnect behavior that application code can observe. Raw UDP brings datagrams directly to your socket, where your software reconstructs and decodes them.

The commercial boundary is simple. One shredstream.sh endpoint carries a $100 price across 30 days. Among standalone raw-UDP Solana shred products, that figure is the public minimum. It points at the customer-nominated public endpoint. Customers pay with USDC or SOL. Frankfurt supplies the delivery, and an account may hold as many as five separately paid destinations.

Plan the receiver work before comparing monthly invoices. The new path needs a fast receive loop, socket tuning, drop telemetry, shred reconstruction, decoding, local filtering, and a confirmation source for later truth. Reuse the components your team already owns, then list the remaining work before setting the cutover date.

Replace channel health with packet evidence

A gRPC channel can be ready, transiently failing, or closed. UDP exposes no equivalent lifecycle. Binding a socket proves only that the local process obtained a port. That binding alone never establishes firewall passage, sender validity, or decoder progress.

Build health from observations. Record the last accepted packet time, the last decoded slot, the number of kernel drops, queue age, parse failures, and packets rejected for an unexpected source. Separate each counter. A single red light called “feed unhealthy” hides the component that needs attention.

Restarts also change meaning. There is no cursor and no remote replay. When the process is down, traffic arriving during that gap is lost. On startup, open the socket, restore local metrics, and continue with new datagrams. Do not put a gRPC-style reconnect loop around a connectionless transport.

Open and prove the UDP path

Choose the production IP and port first. Permit source 203.23.178.228 to reach that exact destination over UDP. Reject arbitrary senders. Where network address translation sits in front, confirm that the public mapping reaches the same socket the application binds.

Before delivery starts, shredstream.sh verifies control of the address. The preflight packet begins with SHREDCAST-VERIFY and supplies a token. Extract that value on the nominated machine, then return it. This proves control of the route but does not become a recurring heartbeat.

A quick capture can distinguish routing trouble from application trouble:

bash

sudo tcpdump -ni any 'udp dst port 9001 and src host 203.23.178.228'nc -u -l 9001

Use the listener only for verification or a brief smoke test. The production process needs proper buffering and metrics. If tcpdump sees traffic while the process reports none, investigate binding, privileges, or the handoff to the receive loop. If neither sees the verification packet, inspect the firewall and public forwarding path.

Give the kernel room

Every delivered packet respects the 1228-byte maximum. Allocate an application read buffer that cannot truncate that boundary. A 2,048-byte slice is convenient because it leaves room to detect anything outside the contract without assuming every valid packet has an identical length.

The per-read slice and kernel receive queue solve different problems. The slice holds one datagram. The socket queue absorbs packets while the process is briefly busy. Start with explicit settings, observe the real host, then tune from drop counters.

bash

sysctl net.core.rmem_maxsysctl net.core.rmem_defaultss -u -a -n -m | grep ':9001'

If policy permits a larger queue, manage it in the host configuration system. Confirm the value actually applied to the socket. Linux can cap an application request. Queue capacity cannot rescue a slow reader, and excessive buffering can turn packet loss into stale work.

Keep intake boring and fast

Keep the socket reader's duties minimal. Read one complete datagram, take an ingress timestamp, capture the sender, validate size plus source, increment counters, then hand an owned byte buffer to a bounded queue. Parsing, filtering, logging, and business actions belong elsewhere.

This Go sketch shows the boundary. Error handling is abbreviated, but the placement of timestamping and validation is intentional.

go

buf := make([]byte, 2048)for {    n, peer, err := conn.ReadFromUDP(buf)    received := time.Now()    if err != nil {        readErrors.Inc()        continue    }    if peer.IP.String() != "203.23.178.228" || n > 1228 {        rejected.Inc()        continue    }    packet := append([]byte(nil), buf[:n]...)    if !queue.TryPush(Item{Bytes: packet, At: received}) {        appQueueDrops.Inc()    }}

Do not pass the reusable read slice to another goroutine without copying or transferring ownership. The next read can overwrite bytes that the decoder still holds. That race often looks like random malformed data under load and disappears in small tests.

Turn datagrams into useful objects

Geyser gave the application records after substantial node-side work. UDP shreds arrive earlier in that chain. Group them by their protocol identity, handle data and coding shreds correctly, recover what can be recovered, rebuild entries, and decode the transaction material the consumer needs.

Treat the decoder as a versioned component with its own tests. Keep a small corpus of captured datagrams and expected parse results. Include shorter valid packets, malformed input, duplicate arrivals, incomplete recovery sets, and unsupported variants. A parser panic must not stop intake.

The output boundary should be explicit. Define a local decoded type rather than trying to imitate every field of the old protobuf. Include only data the raw path can establish at that stage. Keep execution results, account state, and confirmation information on a later source when those facts remain necessary.

Expect disorder, gaps, and duplication

Arrival order is not ledger order. UDP offers neither retransmission nor delivery guarantees. Store enough protocol identity to place each shred correctly regardless of when it reaches the host. Accept that a later packet may close an earlier hole, and that a repeated packet may carry no new information.

Deduplicate only once parsing yields a stable packet identity. Count repeats before discarding them. Track unresolved gaps separately from packets missing at first sight, since recovery may change the useful result. These measurements explain whether the decoder lacks data or simply needs more time.

Never make receive sequence the primary ordering key. Carrier paths, kernel scheduling, and worker concurrency can all rearrange a burst. The key comes from the shred, while the ingress timestamp describes transport performance.

Move predicates behind decoded data

Geyser subscription filters act on structured material. Program IDs, accounts, instructions, transaction outcomes, and vote classifications are not transport-level fields that shredstream.sh filters before sending. The raw feed goes to the destination as shreds.

Port the intent of each subscription, not its configuration text. For every existing predicate, name the decoded field it depends on and the stage where that field becomes available. Then apply the predicate after reconstruction. Test both positive and negative examples from the current production consumer.

This is also where semantic differences become visible. Some filters depend on execution results or confirmed account updates unavailable at the raw-shred stage. Leave those predicates with the later confirmation source while the raw feed handles the early data it can establish.

Design overload instead of inheriting flow control

gRPC backpressure can slow delivery or surface a blocked consumer. UDP keeps arriving. Once the kernel queue fills, the host drops packets. Once the application queue fills, your code must choose what to discard. The sender does not wait for recovery.

Bound every internal queue and record both depth and age. Depth shows stored work. Age shows whether the signal is still useful. For latency-sensitive consumers, dropping stale undecoded items can be better than processing a long historical backlog. Make that policy explicit and test it during a controlled CPU squeeze.

Keep intake, reconstruction, decode, filtering, and business logic in distinct metric stages. Rising socket drops with idle workers suggests host-network tuning. Stable intake with growing decode age points at CPU or decoder work. A healthy decoder followed by a blocked action queue belongs to the application, not the feed.

Instrument what the stream used to hide

Create a small dashboard before shadow traffic begins. Show source acceptance, sender rejection, size violations, kernel overflow, queue loss, parse failure, reconstruction progress, decoding delay, and useful-output silence as separate series.

Use rates and totals together. A resettable rate helps on call, while a monotonic count preserves incident evidence. Attach the receiver build and host to the metrics so comparisons survive deployments. Logs should sample repeated failures instead of printing once per packet and causing their own overload.

Alert on outcomes that matter. Ten malformed packets in isolation may be harmless noise, while zero slot advancement with a busy socket is urgent. Establish normal ranges during parallel operation. Do not copy thresholds from the old gRPC client because the observed layers are different.

Shadow the decisions, not just the bytes

Run both pipelines long enough to compare application output. Keep gRPC authoritative at first. Let the UDP route produce records and proposed actions that cannot mutate production state. Match equivalent decoded events, then inspect disagreements against the later confirmation path.

Packet arrival alone is an incomplete contest. One path may deliver a raw piece first but finish reconstruction later. Timestamp the moment each candidate enters the host and the moment it reaches the decision function. Compare matched items at both stages. Count unmatched output as a coverage issue rather than quietly excluding it from latency charts.

Write acceptance limits before seeing the result. Useful criteria include unresolved gaps, parse failure rate, tail decode delay, queue age, and agreement on shadow decisions. The correct numbers belong to your application. The discipline is deciding them without chasing a flattering sample.

Cut authority over with a switch

Place one explicit selector immediately before production side effects. The decoded UDP path can become authoritative only there. Keep the gRPC route running as confirmation, enrichment, or rollback until every field dependency has a proven owner.

Exercise both selector positions before the real cut. Watch receiver and business metrics through the change. If unexplained gaps, decoder errors, or action mismatches breach the written rule, return authority to gRPC and preserve the packet capture. Debug from evidence while the known path protects production.

The fee remains simple throughout the evaluation. A 30-day term for one destination costs $100, payable in USDC or SOL. Additional endpoints need their own purchases, with five allowed per customer. Frankfurt is the only origin, so test from the actual intended receiver rather than assuming another route behaves the same.

What a finished receiver owns

Completion means more than seeing packets in tcpdump. The nominated address has passed token verification. The firewall rule permits 203.23.178.228 alone as UDP sender. Reads preserve the complete 1228-byte maximum. Kernel and application drops are visible. Disorder and duplicates do not corrupt reconstruction. Decoder failures stay contained. Local predicates have documented semantics.

The team also owns operating behavior. Restarts resume without pretending missed traffic will replay. Alerts reflect packet and decode progress instead of channel state. Shadow results meet the prewritten rule. A tested switch can restore the former authority without rebuilding the deployment.

That is real engineering work, but it is finite. Separate the stages, keep the socket path small, and prove each boundary with counters. The result is a raw-shred pipeline your team understands, priced independently from the application logic built on top of it.

Questions

Can my existing Geyser gRPC client read the UDP feed?
No. The gRPC stub expects a channel and decoded protobuf messages. shredstream.sh places one Solana shred inside each UDP datagram. Add a socket receive loop, preserve packet boundaries, reconstruct and decode the shreds, then connect that output to the application that currently consumes gRPC objects.
What replaces gRPC channel health and reconnect alarms?
Use evidence from the local packet path. Alert when accepted traffic stops, and watch kernel drops, malformed shreds, stalled slot movement, queue age, and decode completion. UDP has no session to reconnect, so a green socket-open flag says little about whether useful data is reaching the consumer.
How do I move my Geyser subscription filters?
Those filters operate on structured records that already exist inside the Geyser pipeline. Before reconstruction, a shred exposes neither the same transaction nor account object. Rebuild enough ledger data, decode the relevant records, and apply equivalent predicates there. Verify the resulting event set against your current consumer before granting it production authority.
How large should the UDP read buffer be?
Allocate application reads for the published 1228-byte maximum without truncation. A slightly larger slice, such as 2,048 bytes, makes truncation checks straightforward. The kernel socket buffer is a separate setting and should absorb bursts. Measure kernel drops on the production host, then tune that buffer from observed load rather than guessing once.