DEV Community

Aturo Phil
Aturo Phil

Posted on

Mempool Policy: Why Your Transaction Gets Rejected

Every Bitcoin node maintains its own mempool, a local set of unconfirmed transactions it considers valid and is willing to relay and mine. When you broadcast a transaction and it does not propagate, or when your wallet says "transaction rejected", the reason is almost always mempool policy.

Mempool policy is not the same as consensus rules. Consensus rules are what the network agrees on: a transaction either is or is not valid by the protocol. Policy rules are what individual nodes choose to enforce: a transaction can be consensus-valid but still be rejected by most mempools.

Understanding this distinction is essential for anyone building Bitcoin applications.


Consensus vs. Policy

A consensus-invalid transaction will be rejected by every full node and can never confirm. Examples: spending a UTXO that does not exist, invalid signature, exceeding the block size limit.

A policy-invalid transaction is consensus-valid but does not meet the mempool's additional requirements. It can confirm if a miner includes it directly, some mining pools accept transactions out of band but it will not propagate through the peer-to-peer network.

Bitcoin Core's policy rules are defined primarily in src/policy/policy.cpp and src/validation.cpp. The defaults represent the rules most of the network enforces.


The Standard Script Policy

Bitcoin Core rejects non-standard scripts from its mempool. A standard script is one of a small, enumerated set:

Type Description
P2PK Pay-to-public-key (legacy, rare)
P2PKH Pay-to-public-key-hash
P2SH Pay-to-script-hash
P2WPKH Native SegWit v0
P2WSH Native SegWit v0 script hash
P2TR Taproot (SegWit v1)
Multisig Bare multisig up to 3-of-3
OP_RETURN Data carrier output (up to 83 bytes)
Witness v1–v16 Future SegWit versions (allowed as unknown versions)

Any output script that does not match one of these templates is non-standard. A transaction containing a non-standard output will be rejected by Bitcoin Core's mempool even if it is perfectly consensus-valid.

This matters if you are experimenting with custom scripts:

from bitcoin.core.script import CScript, OP_ADD, OP_5

# This script: <x> <y> OP_ADD OP_5 OP_EQUAL
# Spendable by any two numbers that sum to 5
# Consensus-valid, but non-standard
puzzle_script = CScript([OP_ADD, OP_5, OP_EQUAL])

# A transaction with this as an output will be rejected by most mempools
# It can confirm only if a miner includes it directly
Enter fullscreen mode Exit fullscreen mode

To use custom scripts on mainnet, you wrap them in P2SH or P2WSH, which are standard. The redeem script is only revealed when spending, not at output creation.


Minimum Relay Fee

Every mempool has a minimum fee rate it will accept. Bitcoin Core's default is 1 satoshi per virtual byte (minrelaytxfee=0.00001). A transaction below this rate is rejected outright, regardless of how small the mempool is.

Virtual bytes (vbytes) account for the SegWit discount:

def calculate_vsize(base_size: int, total_size: int) -> float:
    """
    Virtual size calculation per BIP 141.
    base_size: serialized size excluding witness data
    total_size: full serialized size including witness data
    """
    weight = base_size * 3 + total_size
    return weight / 4.0

# Example: typical P2WPKH to P2WPKH transaction
# base_size = 41 bytes (no witness)
# witness = 108 bytes (2 stack items: sig + pubkey)
# total_size = 41 + 108 = 149 bytes

base = 41
total = 149
vsize = calculate_vsize(base, total)
# vsize ≈ 110.5 vbytes

# At 10 sat/vbyte, fee = 1105 sat
fee = int(vsize * 10)
Enter fullscreen mode Exit fullscreen mode

A dynamic minimum also exists: when the mempool fills up, Bitcoin Core raises the minimum fee rate to shed low-fee transactions. During high-fee periods, the effective floor can be 10–50 sat/vbyte or higher.


Dust Limits

Bitcoin Core will not relay transactions that create outputs below the dust threshold. An output is "dust" if the cost of spending it exceeds its value.

The dust limit depends on the output type:

DUST_RELAY_FEE_RATE = 3  # sat/vbyte (Bitcoin Core default)

DUST_LIMITS = {
    'P2PKH':   546,   # 3 sat/vbyte * 182 vbyte spend cost
    'P2SH':    540,   # 3 sat/vbyte * 180 vbyte spend cost
    'P2WPKH':  294,   # 3 sat/vbyte * 98 vbyte spend cost
    'P2WSH':   330,   # 3 sat/vbyte * 110 vbyte spend cost
    'P2TR':    330,   # 3 sat/vbyte * 110 vbyte spend cost
    'OP_RETURN': 0,   # OP_RETURN outputs are unspendable; no dust limit
}

def is_dust(output_value_sat: int, output_type: str) -> bool:
    return output_value_sat < DUST_LIMITS.get(output_type, 546)
Enter fullscreen mode Exit fullscreen mode

The exception is OP_RETURN: it is explicitly unspendable, so there is no spending cost to calculate. Bitcoin Core allows OP_RETURN outputs regardless of value (as long as the value is zero, a non-zero OP_RETURN output is rejected).

A transaction that creates a dust output is rejected by the mempool. The dust threshold is enforced per output, a transaction with 10 outputs where one is dust is rejected entirely.


RBF: Replace-By-Fee

Replace-By-Fee (BIP 125) allows an unconfirmed transaction to be replaced by a higher-fee version. This is the primary mechanism for fee bumping when you underestimated the required fee rate.

A transaction is RBF-signaling if any of its inputs has nSequence < 0xFFFFFFFE. Bitcoin Core checks this to determine whether it will accept a replacement.

# Signaling RBF opt-in
# nSequence of 0xFFFFFFFD signals RBF
SEQUENCE_RBF_OPT_IN = 0xFFFFFFFD

def create_rbf_transaction(inputs, outputs, fee_rate):
    tx = Transaction()
    for utxo in inputs:
        tx_input = TxInput(
            txid=utxo.txid,
            vout=utxo.vout,
            sequence=SEQUENCE_RBF_OPT_IN  # enables RBF
        )
        tx.add_input(tx_input)
    for output in outputs:
        tx.add_output(output)
    return tx
Enter fullscreen mode Exit fullscreen mode

Rules for a valid replacement (all must be satisfied):

  1. The replacement must signal RBF (at least one input with nSequence < 0xFFFFFFFE)
  2. The replacement must not introduce new unconfirmed inputs (inputs not already in the mempool or chain)
  3. The replacement's absolute fee must be higher than the sum of fees of all transactions it displaces
  4. The replacement's fee must exceed the original's fee by at least minrelaytxfee * replacement_size
  5. The replacement must not evict more than 100 transactions from the mempool

Rule 3 is the important one: if your replacement bumps the fee rate but results in a lower absolute fee (because you removed some outputs), it is rejected. The mempool prioritizes absolute fee to protect against eviction attacks.

The replacement also spends the same inputs as the original, so it conflicts with the original and displaces it.


CPFP: Child-Pays-for-Parent

If a transaction cannot be RBF-bumped, either because it did not signal RBF or you cannot change the inputs, the alternative is CPFP.

CPFP creates a new transaction that spends an output from the stuck transaction and pays a fee high enough to pull both transactions through. Miners evaluate transaction packages, so a high-fee child can subsidize a low-fee parent.

def calculate_cpfp_child_fee(
    parent_vsize: int,
    parent_fee: int,
    child_vsize: int,
    target_package_fee_rate: float  # sat/vbyte
) -> int:
    """
    Calculate the fee the child must pay to achieve the target
    fee rate for the entire (parent + child) package.
    """
    total_package_vsize = parent_vsize + child_vsize
    total_needed_fee = target_package_fee_rate * total_package_vsize
    child_fee = total_needed_fee - parent_fee
    return max(0, int(child_fee))

# Example: parent stuck at 2 sat/vbyte, need 20 sat/vbyte
parent_vsize = 141     # typical P2WPKH transaction
parent_fee = 282       # 141 * 2 sat/vbyte
child_vsize = 110      # spending one P2WPKH output
target = 20.0          # sat/vbyte

child_fee = calculate_cpfp_child_fee(parent_vsize, parent_fee, child_vsize, target)
# child_fee = (141 + 110) * 20 - 282 = 5020 - 282 = 4738 sat
# child fee rate alone: 4738 / 110 ≈ 43 sat/vbyte
Enter fullscreen mode Exit fullscreen mode

CPFP requires that you have an output from the stuck transaction that you can spend. If all outputs go to other parties, you cannot CPFP without their cooperation.

Bitcoin Core's Package Relay (BIP 331) is now deployed, allowing wallets to submit parent-child packages together so the child's fee can be evaluated alongside the parent even before the parent enters the mempool. This is particularly important for Lightning's anchor output channel closes.


Timelock Rules

Two timelock mechanisms interact with mempool policy:

nLockTime: A transaction with nLockTime > 0 is not valid until the specified block height (or Unix time, if > 500,000,000). Bitcoin Core will not accept a transaction into its mempool if its locktime has not been reached.

# This transaction cannot enter the mempool before block 850000
tx.locktime = 850000
Enter fullscreen mode Exit fullscreen mode

nSequence / CSV: OP_CHECKSEQUENCEVERIFY enforces that the input's sequence number encodes a relative timelock (number of blocks or time since the UTXO was confirmed). A transaction spending a CSV-locked output is invalid until the relative timelock has elapsed.

# Spending a UTXO with CSV 144 (about 1 day)
# The UTXO must have at least 144 confirmations
CSV_BLOCKS = 144
tx_input.sequence = CSV_BLOCKS  # 0x00000090
Enter fullscreen mode Exit fullscreen mode

This is the mechanism used for Lightning's to_self_delay outputs after a force close.


OP_RETURN Size Limits

Bitcoin Core currently accepts OP_RETURN outputs up to 83 bytes (one byte for OP_RETURN, one byte for the data push opcode, 80 bytes of data). This limit is a policy rule, not a consensus rule.

In 2024, a policy debate emerged about raising or removing this limit. Some miners already accept larger OP_RETURN data. As of July 2026, Bitcoin Core's default remains 83 bytes, but nodes can override this with -datacarriersize.

def create_op_return_output(data: bytes) -> bytes:
    """
    Create an OP_RETURN output.
    data: up to 80 bytes (Bitcoin Core default policy)
    """
    if len(data) > 80:
        raise ValueError(f"OP_RETURN data too large for standard policy: {len(data)} > 80 bytes")
    script = bytes([0x6a, len(data)]) + data  # OP_RETURN + push
    return script
Enter fullscreen mode Exit fullscreen mode

Checking Mempool Policy Before Broadcasting

Query mempool.space or your own node to check current fee rates before constructing a transaction:

import httpx
import asyncio

async def get_current_fee_rates() -> dict:
    """
    Fetch current fee estimates from mempool.space.
    Returns fee rates in sat/vbyte for different confirmation targets.
    """
    async with httpx.AsyncClient() as client:
        response = await client.get("https://mempool.space/api/v1/fees/recommended")
        rates = response.json()
        return {
            'fastest': rates['fastestFee'],      # next block
            'half_hour': rates['halfHourFee'],   # ~3 blocks
            'hour': rates['hourFee'],            # ~6 blocks
            'economy': rates['economyFee'],      # ~144 blocks
            'minimum': rates['minimumFee'],      # dynamic floor
        }

async def get_mempool_stats() -> dict:
    """Check current mempool size and minimum fee."""
    async with httpx.AsyncClient() as client:
        response = await client.get("https://mempool.space/api/mempool")
        return response.json()
        # Returns: count, vsize, total_fee, fee_histogram

async def main():
    rates = await get_current_fee_rates()
    stats = await get_mempool_stats()

    print(f"Next block: {rates['fastest']} sat/vbyte")
    print(f"Economy: {rates['economy']} sat/vbyte")
    print(f"Mempool vsize: {stats['vsize']:,} vbytes")
    print(f"Pending transactions: {stats['count']:,}")
Enter fullscreen mode Exit fullscreen mode

What to Do When a Transaction Is Stuck

Checklist in order:

  1. Check if it was rejected at broadcast — node returned an error. Read the error code. Common rejections: min relay fee not met, dust, non-standard, already in mempool.

  2. Check if it is in the mempool — query /api/tx/{txid} on mempool.space. If not there, it was rejected or never reached the network.

  3. RBF bump — if inputs signal RBF, create a replacement with a higher fee. Most wallet software supports this.

  4. CPFP — if you control an output, spend it with a high enough fee to pull the parent through.

  5. Wait — if the mempool clears (Sunday nights UTC are historically low-fee periods), low-fee transactions often confirm without intervention.

  6. Accept abandonment — after approximately two weeks, Bitcoin Core removes transactions from its mempool. If no one confirms it and no one CPFP'd it, the UTXO returns to spendable.


Further Reading

Top comments (0)