> For a complete page index, fetch https://docs.conduit.xyz/llms.txt # Managing Nonces > Best practices for reliable nonce management on Arbitrum Nitro, with minimal viem examples and guidance to avoid optimistic nonce mismatches. This guide outlines strategies to manage transaction nonces safely on Arbitrum (Nitro), with a focus on ordering, concurrency, and recovery. It includes minimal `viem` examples and applies to both frontend and backend systems. ## What is a nonce? * In EVM-compatible blockchains, a nonce is the count of transactions sent from an address. * Each new transaction must use a unique, sequential nonce: * The first transaction uses nonce 0. * The next uses nonce 1, and so on. * If a transaction uses a nonce that’s already been used or skipped, the blockchain will reject it. ## Common challenges * Race conditions: Sending multiple transactions simultaneously may reuse the same nonce. * RPC delays: Fetching nonces from a slow RPC endpoint may lead to stale values. * Page reloads: If the app restarts, in-memory nonce tracking resets. To avoid these, implement a clear source of truth, local tracking, and safe concurrency controls. ## Strategy We’ll use a two-layer nonce management system: * Remote layer: Fetches the latest confirmed nonce from the blockchain via RPC. * Local layer: Tracks and increments nonces in localStorage or in-memory state for pending transactions. Flow: ```text Blockchain (RPC) ↓ Fetch latest confirmed nonce ↓ Store locally (nonce cache) ↓ Increment locally for each new tx ↓ Submit tx → update nonce tracker ``` ## Best practices ### Arbitrum (Nitro) specifics * Nonce mismatches on Arbitrum commonly happen when the client optimistically bumps the nonce before the prior transaction is confirmed as included. * When a transaction is submitted with a nonce that is too high on Nitro, the node will hold it briefly (about 1 second by default) to see if intermediate transactions arrive to fill the gap. If they don’t, you’ll receive a "nonce too high" error afterwards. * That 1-second hold can delay feedback and encourage clients to keep submitting higher nonces. Until client-side logic is improved, reduce this "gap-hold" time from \~1s to \~0 to get immediate feedback. This allows you to quickly detect incorrect nonces before sending further transactions with even higher nonces. * Practical approach: * Gate sending of nonce N+1 on evidence that N is included (or at least that N is accepted and visible in `pending`). * If you receive "nonce too high", immediately re-sync from RPC and pause higher-nonce submissions until the gap is resolved. * Ask your node/provider to minimize the hold window so errors surface immediately (Conduit can configure this on managed stacks). #### Example (TypeScript script): Gated nonces on Arbitrum Nitro **`copy`** ```ts copy import { createPublicClient, createWalletClient, http, parseEther, type Address, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arbitrum } from "viem/chains"; // 1) Configure clients for Arbitrum Nitro (Node.js script) const RPC_URL = process.env.RPC_URL || "https://arb1.arbitrum.io/rpc"; const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`; if (!PRIVATE_KEY) { throw new Error("Missing PRIVATE_KEY env var (hex string, 0x-prefixed)."); } const account = privateKeyToAccount(PRIVATE_KEY); const publicClient = createPublicClient({ chain: arbitrum, transport: http(RPC_URL), }); const walletClient = createWalletClient({ account, chain: arbitrum, transport: http(RPC_URL), }); const address = account.address; // 2) Helpers async function getPendingNonce(addr: Address) { return publicClient.getTransactionCount({ address: addr, blockTag: "pending", }); } async function waitUntilPendingNonceAtLeast( addr: Address, target: number, pollMs = 300, timeoutMs = 6000, ) { const start = Date.now(); while (Date.now() - start < timeoutMs) { const pending = await getPendingNonce(addr); if (pending >= target) return true; await new Promise((r) => setTimeout(r, pollMs)); } return false; } // 3) Gated send: explicit nonce, then wait for pending to advance export async function sendWithGatedNonce(to: Address, valueEth = "0.01") { let next = await getPendingNonce(address); try { const hash = await walletClient.sendTransaction({ to, value: parseEther(valueEth), nonce: next, }); // Wait for mempool to reflect acceptance before attempting N+1 await waitUntilPendingNonceAtLeast(address, next + 1); return hash; } catch (err: any) { const msg = String(err?.message || "").toLowerCase(); if (msg.includes("nonce too high")) { const resynced = await getPendingNonce(address); throw new Error(`Nonce too high. Resynced pending nonce=${resynced}`); } if (msg.includes("nonce too low")) { const resynced = await getPendingNonce(address); throw new Error(`Nonce too low. Resynced pending nonce=${resynced}`); } throw err; } } // 4) Example usage // RPC_URL="https://arb1.arbitrum.io/rpc" PRIVATE_KEY="0x..." node script.ts // await sendWithGatedNonce("0xRecipientAddressHere" as Address, "0.01"); ``` ### Source of truth: latest vs pending * Use `latest` to align with confirmed state (stable, conservative). * Use `pending` to include mempool (higher throughput, risk of drift). * For parallel send flows, prefer `pending` plus a local tracker to avoid reuse. ### Fetching the nonce (viem) **`copy`** ```ts copy // Latest confirmed nonce const latest = await client.getTransactionCount({ address, blockTag: "latest", }); // Include mempool transactions const pending = await client.getTransactionCount({ address, blockTag: "pending", }); ``` ### Local tracking and persistence Maintain a next-nonce per address locally Recommended algorithm (per address): 1. On startup, read `pending` nonce from RPC as the baseline. 2. Compare with any locally cached value and keep the max. 3. On each send: * Read current local next-nonce. * Reserve it (optimistically). * Submit tx with that nonce. * Increment and persist the next-nonce. 4. On error indicating nonce drift, re-sync from RPC and update local store. Minimal usage pattern: ```ts const next = await takeNextNonce(address); // reserves and returns next nonce await walletClient.sendTransaction({ ...tx, nonce: next }); ``` ### Concurrency control (avoid races) * Use a per-address mutex/lock (backend) or single-flight queue (frontend). * Only one "allocate nonce and send" critical section should run at a time. ```ts await withAddressLock(address, async () => { const next = await takeNextNonce(address); const hash = await walletClient.sendTransaction({ ...tx, nonce: next }); return hash; }); ``` ### Handling errors and re-sync * "nonce too low": Your local tracker is behind; re-fetch `pending`, set local to max, retry. * "replacement transaction underpriced": If intentionally replacing, bump fees sufficiently; otherwise choose a new higher nonce. * Dropped transactions: If a tx is dropped, you may reuse its nonce after confirming it’s not in mempool; safer is to send a replacement with higher fee. * Periodically re-sync from RPC (timer or after N sends). ### Replacement transactions (EIP-1559) * To replace a pending tx, send a new tx with the same nonce and a higher effective priority fee/tip. * Ensure the replacement increases fees enough to be accepted by the node/p2p policy. ### When to rely on node-managed nonces * It’s fine to omit `nonce` and let the node set it if: * You send strictly sequential transactions (no parallelism). * Throughput and latency requirements are modest. * If you need parallelism or precise control, manage nonces locally. ## Client implementation (React + viem) ### Install **`copy`** ```bash copy npm install viem ``` ### Hook: `useNonceManager` **`copy`** ```ts copy import { useCallback, useEffect, useRef, useState } from "react"; import type { Address, PublicClient } from "viem"; const NONCE_STORAGE_KEY = "nonce-tracker"; export function useNonceManager(client: PublicClient, address?: Address) { const [nonce, setNonce] = useState(null); const isFetching = useRef(false); const fetchOnChainNonce = useCallback(async () => { if (!address || isFetching.current) return; isFetching.current = true; try { const latest = await client.getTransactionCount({ address, blockTag: "latest", }); setNonce(latest); localStorage.setItem(`${NONCE_STORAGE_KEY}:${address}`, String(latest)); } finally { isFetching.current = false; } }, [client, address]); const loadLocalNonce = useCallback(() => { if (!address) return; const saved = localStorage.getItem(`${NONCE_STORAGE_KEY}:${address}`); if (saved) setNonce(Number(saved)); }, [address]); const getNextNonce = useCallback(() => { if (nonce === null) throw new Error("Nonce not initialized"); const next = nonce; const newNonce = nonce + 1; setNonce(newNonce); if (address) { localStorage.setItem(`${NONCE_STORAGE_KEY}:${address}`, String(newNonce)); } return next; }, [nonce, address]); const resetNonce = useCallback(() => { setNonce(null); if (address) localStorage.removeItem(`${NONCE_STORAGE_KEY}:${address}`); }, [address]); useEffect(() => { loadLocalNonce(); fetchOnChainNonce(); }, [loadLocalNonce, fetchOnChainNonce]); return { nonce, getNextNonce, fetchOnChainNonce, resetNonce }; } ``` ### Component: submit a transaction on Arbitrum **`copy`** ```tsx copy import React from "react"; import { createPublicClient, createWalletClient, custom, http, parseEther, type Address, } from "viem"; import { arbitrum } from "viem/chains"; import { useNonceManager } from "./hooks/useNonceManager"; export function TransactionSender() { const publicClient = createPublicClient({ chain: arbitrum, transport: http("https://arb1.arbitrum.io/rpc"), }); const walletClient = createWalletClient({ chain: arbitrum, // In a browser, prefer the injected provider (e.g., MetaMask) transport: typeof window !== "undefined" && (window as any).ethereum ? custom((window as any).ethereum) : http("https://arb1.arbitrum.io/rpc"), }); const address = "0xYourAddressHere" as Address; const { nonce, getNextNonce, fetchOnChainNonce } = useNonceManager( publicClient, address, ); const sendTransaction = async () => { // Avoid optimistic bumps: only allocate the next nonce when sending const currentNonce = getNextNonce(); const tx = { to: "0xRecipientAddress" as Address, value: parseEther("0.01"), nonce: currentNonce, gas: 21000n, }; const hash = await walletClient.sendTransaction(tx); console.log("Tx sent:", hash); // Optionally re-sync after submission fetchOnChainNonce(); }; return (

Current Nonce: {nonce ?? "Loading..."}

); } ``` Arbitrum tip: gate sending of nonce N+1 until N is visible in `pending` (or confirmed), and avoid incrementing local state until you actually send. If you receive "nonce too high", immediately re-sync from RPC and pause higher-nonce submissions; ask your provider to minimize the Nitro hold window so errors surface immediately. ## Summary | Task | Layer | Source | | --------------------- | ---------- | -------------------------------------------------------------- | | Initial nonce | Blockchain | `client.getTransactionCount({ address, blockTag: "pending" })` | | Cached nonce | Local | `localStorage` | | Increment between txs | Local | `nonce + 1` | | Sync periodically | Remote | `fetchOnChainNonce()` | By combining local caching and on-chain syncing, your dApp can reliably manage nonces even in complex transaction flows. > Best practices for reliable nonce management on Arbitrum Nitro, with minimal viem examples and guidance to avoid optimistic nonce mismatches.