<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[RPC Plane]]></title><description><![CDATA[RPC Plane]]></description><link>https://rpcplane.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>RPC Plane</title><link>https://rpcplane.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 23 Sep 2026 21:35:42 GMT</lastBuildDate><atom:link href="https://rpcplane.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why Solana Transactions Don't Land (and How to Fix It)]]></title><description><![CDATA[You sign a transaction, call sendTransaction, get a signature back, and then… nothing. The signature never shows up on-chain. Your logs have no error. Your RPC provider returned a clean 200. The user ]]></description><link>https://rpcplane.hashnode.dev/why-solana-transactions-don-t-land-and-how-to-fix-it</link><guid isPermaLink="true">https://rpcplane.hashnode.dev/why-solana-transactions-don-t-land-and-how-to-fix-it</guid><category><![CDATA[Solana]]></category><category><![CDATA[RPC]]></category><category><![CDATA[Blockchain]]></category><category><![CDATA[transactions]]></category><dc:creator><![CDATA[Vikar Egor]]></dc:creator><pubDate>Tue, 28 Jul 2026 15:55:22 GMT</pubDate><content:encoded><![CDATA[<p>You sign a transaction, call <code>sendTransaction</code>, get a signature back, and then… nothing. The signature never shows up on-chain. Your logs have no error. Your RPC provider returned a clean <code>200</code>. The user is staring at a spinner asking why their swap didn't go through.</p>
<p>This is the most common and most misdiagnosed failure mode in Solana development, and almost all of the confusion comes from one conflated idea: <strong>a transaction that didn't land is not the same as a transaction that failed.</strong></p>
<h2>Dropped is not the same as failed</h2>
<p>There are two completely different outcomes that both look like "it didn't work":</p>
<p><strong>Reverted</strong> — the transaction reached a leader, landed in a block, executed, and hit an error. Insufficient funds, slippage exceeded, a failed constraint. This transaction <em>exists</em>: you can look up the signature, it consumed fees, and the error tells you what went wrong.</p>
<p><strong>Dropped</strong> — it never made it into a block. Nothing to look up, no fee charged. From the chain's perspective it never happened.</p>
<p>The signature <code>sendTransaction</code> returns tells you almost nothing, because it's computed locally when you sign. A node returning it means "I received your bytes and I'll try," not "this is on-chain." Treating that as success is the root bug in a surprising number of production apps.</p>
<p>Everything below is about the second category.</p>
<h2>The blockhash clock started before you called</h2>
<p>Every Solana transaction carries a <code>recentBlockhash</code>, and it's valid for <strong>150 slots</strong> — roughly a minute. This is the deduplication mechanism: it's what stops a transaction from being replayed forever.</p>
<p>The critical detail: the clock starts when the blockhash was <em>produced</em>, not when you signed. Fetch a blockhash, show a confirmation modal, wait for the user to click, then sign and submit — you may have burned 40 of those 150 slots before your bytes left the machine.</p>
<p>Once it expires the transaction is permanently unlandable. Not delayed, dead. So: <strong>fetch the blockhash as late as possible</strong>, immediately before signing, not at the top of your request handler.</p>
<h2>Where transactions actually get lost</h2>
<p>Assuming a valid, unexpired blockhash, there are a handful of places a submission disappears:</p>
<p><strong>The node never forwarded it.</strong> After accepting your transaction, an RPC node rebroadcasts it toward upcoming leaders on a timer until it confirms or the blockhash expires. That queue is finite, and under load submissions get evicted from it — silently, no error surfaced to you.</p>
<p><strong>You read the blockhash from a node that's ahead of the one you submitted to.</strong> This one bites multi-provider setups specifically. If provider A is at the chain tip and provider B is 30 slots behind, a blockhash fetched from A may be <em>unknown</em> to B. B rejects it — usually as "blockhash not found" — even though the blockhash is perfectly valid. The mirror case is worse: submit to a node that's behind and it may accept, sit on the transaction, and forward it too late.</p>
<p><strong>Preflight ran against a lagging node.</strong> By default <code>sendTransaction</code> simulates first, against <em>that node's</em> current state. A node several slots behind simulates on stale account data and rejects transactions that would have succeeded — a balance that's already been topped up, a PDA that's already initialized. The transaction was fine; the simulator's worldview wasn't.</p>
<p><strong>Congestion.</strong> Blocks have a compute ceiling, and under contention you compete on priority fee. This is the one everyone reaches for first. It's real — but unlike the others, you can't fix it with better plumbing.</p>
<p>Notice that three of those four are RPC-layer problems, not chain problems.</p>
<h2>Retrying is your job</h2>
<p>The most important mental shift: <strong>the RPC node's rebroadcasting is a courtesy, not a guarantee.</strong> If you want reliable landing, you own the retry loop.</p>
<p>The rules that matter:</p>
<ul>
<li><p><strong>Rebroadcast the exact same signed bytes.</strong> Don't rebuild, don't re-sign. Same signature means the network deduplicates for free. Rebuilding with a fresh blockhash creates a <em>second</em> transaction — and a double-execution risk.</p>
</li>
<li><p><strong>Poll</strong> <code>getSignatureStatuses</code><strong>, don't just wait.</strong> Confirmation subscriptions drop. Polling is boring and it works.</p>
</li>
<li><p><strong>Stop when the blockhash expires,</strong> not when a fixed timer runs out. Track the last valid block height and give up there — retrying past expiry is pure noise.</p>
</li>
<li><p><strong>Consider</strong> <code>maxRetries: 0</code> <strong>plus your own loop</strong> rather than leaving retry policy to whichever node you happened to hit.</p>
</li>
</ul>
<h2>One provider is a single point of failure on the write path</h2>
<p>Everything above assumes your submission reaches <em>a</em> healthy node. With one endpoint, every failure mode above is unmitigated: your transaction's fate depends entirely on whether that node is at the tip, has queue headroom, and simulates correctly right now.</p>
<p>The fix is to stop submitting to one node. Because Solana deduplicates by signature, <strong>sending identical signed bytes to several providers at once is safe</strong> — at most one copy lands, and the others are discarded by the network. It costs you a little extra request volume and buys you independence from any single node's queue, slot position, or mood.</p>
<p>That turns submission from "hope this node cooperates" into "first one to get it into a block wins."</p>
<p>Three things to get right if you do this:</p>
<ol>
<li><p><strong>Send with</strong> <code>skipPreflight: true</code><strong>.</strong> Otherwise every target runs its own simulation — that's N× the simulation cost, and providers sitting at slightly different slots will disagree with each other. One rejects, another accepts, and you get failures that depend on which node answered first. Simulate deliberately, once, on the read path instead.</p>
</li>
<li><p><strong>Don't bundle submissions into a JSON-RPC batch.</strong> A batch is an array of calls and gets forwarded as one unit to one endpoint. A <code>sendTransaction</code> buried inside a batch gets none of this. Submit transactions as individual requests.</p>
</li>
<li><p><strong>Keep your providers close together in slot height.</strong> Fanning out doesn't help if half the fleet is 50 slots behind — that's the blockhash mismatch problem again, just parallelized. Providers need to be health-checked on slot freshness, not just on whether they're answering.</p>
</li>
</ol>
<h2>Wiring it up</h2>
<p>You can build this yourself: a submission fan-out, a status poller keyed to blockhash expiry, per-provider slot tracking so you never read a blockhash from a node 40 slots ahead of the one you submit to.</p>
<p>Or put a proxy in front of your providers that already does it. <a href="https://rpcplane.dev">RPC Plane</a> is a self-hosted binary you point your app at instead of a single provider URL. Enable write broadcasting and <code>sendTransaction</code> fans out to every healthy provider at once, returning the fastest success. It scores providers on slot freshness continuously, so a node that drifts behind stops receiving your reads <em>and</em> your submissions — and it treats <code>-32005</code> "node is behind" as retryable, moving to the next provider instead of handing you a failure. The <a href="https://docs.rpcplane.dev/routing/">routing docs</a> cover the write path, including submission-only endpoints that accept <code>sendTransaction</code> and nothing else.</p>
<p>Either way the lesson holds: most "Solana is unreliable" stories trace back to a write path that assumed one RPC node would behave. Assume it won't, own your retry loop, and treat a signature as a receipt for an attempt — never as proof of anything.</p>
<hr />
<p><em>Related:</em> <a href="https://dev.to/vikar_egor/solana-rpc-failover-multi-provider-redundancy-done-right-2np7"><em>Solana RPC Failover: Multi-Provider Redundancy Done Right</em></a> <em>covers the read-path side — health scoring, circuit breakers, and routing strategies.</em></p>
]]></content:encoded></item></channel></rss>