The first time I shipped a Solana bot to mainnet, it worked great in my tests and then landed maybe half its transactions in production. No error I could act on. Just “Transaction was not confirmed” and a lot of confusion.

The problem was priority fees, or rather my complete lack of them. If you’re building anything on Solana that has to land during real network activity, this is the thing nobody explains clearly and everybody learns the hard way.

Here’s what’s actually going on and how I set fees now so my transactions land instead of vanishing.


Why “cheap transactions” is a trap

Solana’s base fee is tiny. 5000 lamports, a fraction of a cent. That’s the number everyone quotes and it’s why people think Solana transactions are basically free.

But that base fee doesn’t buy you priority. When the network is busy and there are more transactions than a block can hold, validators pick which ones to include. They order by the priority fee you attach, which is a separate, optional fee on top of the base. No priority fee means you’re at the back of the line, and during any real activity the back of the line doesn’t get in.

So your transaction isn’t failing because it’s wrong. It’s failing because you didn’t bid for a seat. That reframing is the whole thing.


The two knobs that matter

Priority fee on Solana is two settings, and you need both. This confused me for a while because people talk about “the priority fee” like it’s one number.

Compute unit limit. How much compute your transaction is allowed to use. Every instruction burns compute units. The default cap is 200k per instruction, and if your transaction needs more, it fails with an out-of-compute error even if everything else is correct.

Compute unit price. How many micro-lamports you’re willing to pay per compute unit. This is the actual bid. Your total priority fee is roughly limit * price, so both numbers feed the final cost.

You set them with two instructions at the front of your transaction:

import {
  ComputeBudgetProgram,
  Transaction,
} from "@solana/web3.js"

const tx = new Transaction()

tx.add(
  ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }),
)
tx.add(
  ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 50_000 }),
)

// ...then add your actual instructions

Miss the limit and you might run out of compute. Miss the price and you don’t get prioritized. I was setting neither. That was the bug.


Don’t guess the compute limit, measure it

Setting units to 300k because it’s a round number is how you overpay or underpay. What you want is your transaction’s real usage plus a small buffer.

You get the real number by simulating the transaction first. The simulation tells you how many units it actually consumed.

const sim = await connection.simulateTransaction(tx)
const used = sim.value.unitsConsumed ?? 200_000

// add ~10% headroom so you don't run out on a busy block
const limit = Math.ceil(used * 1.1)

Then set setComputeUnitLimit({ units: limit }) with that value. Now you’re paying for what you use, not a made-up ceiling. A tight, accurate limit is also cheaper, because remember, total fee scales with the limit.

Simulating first felt like extra work until I realized it also catches transactions that would fail for other reasons before I ever pay for them.


Setting the price: stop hardcoding it

Hardcoding microLamports: 50_000 works right up until the network gets busy and 50k isn’t enough anymore. Then you’re back to failing transactions and you don’t know why, because it worked yesterday.

The network’s going rate moves constantly. You want to read what’s actually being paid right now and match it. Solana RPCs expose recent priority fees for the accounts your transaction touches:

const recent = await connection.getRecentPrioritizationFees({
  lockedWritableAccounts: [somePubkey],
})

// take a high percentile of recent fees, not the average
const fees = recent.map((f) => f.prioritizationFee).sort((a, b) => a - b)
const p75 = fees[Math.floor(fees.length * 0.75)] || 0

const price = Math.max(p75, 1_000) // floor so you're never at zero

I use the 75th percentile, not the average. The average includes a pile of transactions that paid nothing and don’t care if they land. If I want mine to land, I bid above the crowd, not with it. The floor keeps me from bidding zero on a quiet network.

Some RPC providers also have their own priority-fee endpoints that give you tiers like low/medium/high. If yours does, those are usually better tuned than rolling your own. Use them.


The part that actually bit me: confirmation

Even with good fees, a transaction can be dropped before it lands, especially during congestion. Sending it once and hoping is not a strategy. I lost real transactions this way.

You need to keep the transaction alive: resend it until it either confirms or its blockhash expires.

const latest = await connection.getLatestBlockhash()
tx.recentBlockhash = latest.blockhash

const raw = tx.serialize()

// resend the same signed tx periodically until confirmed or expired
const sig = await connection.sendRawTransaction(raw, {
  skipPreflight: true,
  maxRetries: 0, // we handle retries ourselves
})

// then poll confirmation against the blockhash's validity window

The pattern is: sign once, send repeatedly, stop when it confirms or the blockhash is too old to be valid. Doing my own retry loop instead of trusting a single send is what took me from “lands half the time” to “lands basically always.”


What I actually run now

Put together, my send path is four steps every time. Simulate to get the real compute usage. Read recent fees and bid the 75th percentile. Set both compute budget instructions. Send and re-send until confirmed or expired.

None of it is complicated. It’s just four things, and skipping any one of them is a different flavor of “my transaction disappeared.”

For anything where landing matters and money’s involved, there’s also Jito bundles, which let you tip validators directly and get atomic execution. That’s a whole separate topic and I’ll write it up. But get the priority fee basics right first, because bundles won’t save a transaction that’s out of compute.


Your Solana transactions aren’t failing because Solana is broken. They’re failing because a transaction with no priority fee is a transaction that asked to be ignored. Set the compute limit from a real simulation, bid the going rate instead of a hardcoded guess, and resend until it lands. Do those three things and the “not confirmed” errors mostly go away. It’s not free, but neither is watching your transactions disappear.