Concurrent Transactions on Midnight: UTXO Race Conditions & Workarounds
The Problem: When Two Transactions Collide
You deploy your first Midnight dApp. Everything works in testing — single transactions sail through. Then you go live, and something strange happens: users report transactions randomly failing with "stale UTXO" or "UTXO already consumed."
This is the UTXO race condition. On Midnight (a UTXO-based chain), the same wallet trying to send two transactions simultaneously can break because both try to spend the same coin.
Pattern 1: Sequential Transaction Queue
The simplest fix is to stop sending transactions concurrently. Queue them:
class SequentialTxQueue {
private queue = [];
private processing = false;
enqueue(txFn) {
return new Promise((resolve, reject) => {
this.queue.push({ txFn, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
const { txFn, resolve, reject } = this.queue.shift();
try {
const hash = await txFn();
resolve(hash);
} catch (error) {
reject(error);
} finally {
this.processing = false;
this.processQueue();
}
}
}
Pattern 2: Multiple Wallet Instances
For higher throughput, spin up multiple wallets. Each wallet has its own UTXO set:
class WalletPool {
private wallets = [];
async acquire() {
const available = this.wallets.find(w => !w.busy);
if (available) {
available.busy = true;
return available;
}
return new Promise(resolve => this.waitQueue.push({ resolve }));
}
async release(instance) {
await this.waitForWalletSync(instance.wallet);
instance.busy = false;
}
}
Pattern 3: Retry with Fresh UTXOs
Add exponential backoff retry for occasional collisions:
async function submitWithRetry(wallet, buildTx, config = { maxRetries: 3 }) {
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
await wallet.waitForSync();
const recipe = await buildTx();
const proven = await wallet.proveTransaction(recipe);
return await wallet.submitTransaction(proven);
} catch (error) {
if (!isUtxoError(error) || attempt === config.maxRetries) throw error;
await sleep(Math.min(1000 * 2**attempt, 10000));
}
}
}
Key Takeaways
- Sequential queue for most dApps — simple, reliable
- Wallet pool for high-throughput services
- Retry + sync as safety net for all cases
- Always sync wallet between transactions
Full tutorial: https://gist.github.com/wilsonhoe/a5766903b8c99f9df222ea322e63418b
Midnight Network tutorial series. Bounty #301.
United States
NORTH AMERICA
Related News
UCP Variant Data: The #1 Reason Agent Checkouts Fail
7h ago
Amazon Employees Are 'Tokenmaxxing' Due To Pressure To Use AI Tools
21h ago
How Braze’s CTO is rethinking engineering for the agentic area
10h ago

Décryptage technique : Comment builder un téléchargeur de vidéos Reddit performant (DASH, HLS & WebAssembly)
17h ago
How AI Reduced Manual Driver Verification by 75% — Operations Case Study. Part 2
4h ago