DEV Community

Aturo Phil
Aturo Phil

Posted on

Post-Halving Bitcoin Mining Economics: What Changed for Protocol Developers

Bitcoin Development Series, Part 3 of 4


The April 2024 halving dropped Bitcoin's block subsidy from 6.25 BTC to 3.125 BTC. Two years on, the economic consequences for miners have stabilized into a new baseline and that baseline changes how you should think about mempool dynamics, fee estimation, and transaction throughput if you are building anything on or near the Bitcoin protocol.

This post covers what permanently changed, what it means for developers, and how to query the data yourself.


What the Halving Actually Did

Before the halving, miners earned a guaranteed 6.25 BTC per block regardless of how many transactions were in it. Fees were supplemental. After the halving, that guarantee dropped to 3.125 BTC and at current prices, the difference represents roughly $190,000 per block in lost guaranteed revenue at a $61,000 BTC price.

The economic effect was not subtle. Three things followed:

  1. Miners with high electricity costs or older ASIC hardware became unprofitable and either shut down or sold their operations to larger players. The mining industry consolidated.
  2. Fee revenue became structurally more important. Miners now have a stronger financial incentive to prioritize high-fee transactions and, crucially, to keep blocks full.
  3. The Ordinals and Runes wave of 2024–2025 was partly a direct response to this, high-fee, data-heavy transactions that padded miner revenue at a time when the subsidy halved.
# Block reward breakdown: pre vs post halving

def block_revenue(btc_price: float, subsidy_btc: float, avg_fee_btc: float) -> dict:
    subsidy_usd = subsidy_btc * btc_price
    fee_usd     = avg_fee_btc * btc_price
    total_usd   = subsidy_usd + fee_usd
    fee_pct     = (fee_usd / total_usd) * 100

    return {
        "subsidy_btc":  subsidy_btc,
        "avg_fee_btc":  avg_fee_btc,
        "subsidy_usd":  round(subsidy_usd, 2),
        "fee_usd":      round(fee_usd, 2),
        "total_usd":    round(total_usd, 2),
        "fee_share_pct": round(fee_pct, 2),
    }

BTC_PRICE = 65_000

pre_halving  = block_revenue(BTC_PRICE, subsidy_btc=6.25,  avg_fee_btc=0.08)
post_halving = block_revenue(BTC_PRICE, subsidy_btc=3.125, avg_fee_btc=0.08)

print("Pre-halving: ", pre_halving)
print("Post-halving:", post_halving)

# Pre-halving:  {'subsidy_btc': 6.25,  ..., 'total_usd': 411250.0, 'fee_share_pct': 1.26}
# Post-halving: {'subsidy_btc': 3.125, ..., 'total_usd': 208250.0, 'fee_share_pct': 2.50}
Enter fullscreen mode Exit fullscreen mode

Fee share effectively doubled as a percentage of total miner revenue. That ratio will keep climbing into the next halving in 2028.


What This Means for Fee Estimation

Fee estimation is harder than it looks. The naive approach is to look at the last few blocks and pick a fee rate in the middle, this breaks down during fee spikes because mempool backlogs can form and clear faster than your estimate refreshes.

Bitcoin Core uses a statistical model that tracks how long transactions at different fee rates take to confirm, across historical blocks. If you are building a wallet, exchange, or any system that constructs transactions, you should either call this directly or use a service that does.

Querying Fee Estimates via Bitcoin Core RPC

from bitcoinrpc.authproxy import AuthServiceProxy

RPC_URL = "http://rpcuser:rpcpassword@127.0.0.1:8332"

def get_fee_estimates() -> dict:
    rpc = AuthServiceProxy(RPC_URL)

    # estimatesmartfee returns sat/vByte estimate for N-block confirmation target
    # CONSERVATIVE mode is safer for time-sensitive transactions
    targets = {
        "next_block":    rpc.estimatesmartfee(1,  "CONSERVATIVE"),
        "3_blocks":      rpc.estimatesmartfee(3,  "CONSERVATIVE"),
        "6_blocks":      rpc.estimatesmartfee(6,  "ECONOMICAL"),
        "next_day":      rpc.estimatesmartfee(144, "ECONOMICAL"),
    }

    result = {}
    for label, est in targets.items():
        if "feerate" in est:
            # feerate is in BTC/kB; convert to sat/vByte
            feerate_btc_per_kb  = est["feerate"]
            feerate_sat_per_vb  = feerate_btc_per_kb * 100_000  # 1 BTC/kB = 100 sat/vB
            result[label] = {
                "sat_per_vbyte": round(feerate_sat_per_vb, 1),
                "blocks":        est.get("blocks"),
            }
        else:
            result[label] = {"error": est.get("errors")}

    return result

print(get_fee_estimates())
Enter fullscreen mode Exit fullscreen mode

Querying Fee Estimates Without a Node

import httpx

def get_mempool_fee_estimates() -> dict:
    """
    mempool.space provides real-time fee estimates
    without requiring a local node.
    """
    resp = httpx.get("https://mempool.space/api/v1/fees/recommended")
    resp.raise_for_status()
    fees = resp.json()

    # Response fields (all in sat/vByte):
    # fastestFee   — likely to confirm in next 1-2 blocks
    # halfHourFee  — likely within ~3 blocks
    # hourFee      — likely within ~6 blocks
    # economyFee   — low priority, may wait hours
    # minimumFee   — floor for relay

    return fees

print(get_mempool_fee_estimates())
# => {'fastestFee': 12, 'halfHourFee': 10, 'hourFee': 8, 'economyFee': 4, 'minimumFee': 1}
Enter fullscreen mode Exit fullscreen mode

Monitoring Mempool Depth

Mempool depth tells you how congested the network is at any moment. During high-fee periods (NFT mints, protocol launches, macro news events), the mempool can balloon to hundreds of MB and fee rates can spike 10–50x within minutes.

import httpx
from datetime import datetime

def get_mempool_stats() -> dict:
    resp = httpx.get("https://mempool.space/api/mempool")
    resp.raise_for_status()
    data = resp.json()

    vsize_mb = data["vsize"] / 1_000_000

    return {
        "tx_count":        data["count"],
        "total_size_mb":   round(vsize_mb, 2),
        "total_fee_btc":   round(data["total_fee"] / 100_000_000, 6),
        "congestion":      classify_congestion(vsize_mb),
        "timestamp":       datetime.utcnow().isoformat(),
    }

def classify_congestion(vsize_mb: float) -> str:
    if vsize_mb < 2:
        return "low"       # fees at minimum, confirms fast
    elif vsize_mb < 50:
        return "moderate"  # some backlog, est. fees reliable
    elif vsize_mb < 200:
        return "high"      # spike in progress, use fastestFee
    else:
        return "severe"    # mempool full, only high-fee txs get in

print(get_mempool_stats())
Enter fullscreen mode Exit fullscreen mode

Tracking Fee Revenue Per Block

If you want to track how fee dependency is evolving over time, here is how to calculate actual fee revenue per block:

import httpx

def get_recent_block_fee_stats(num_blocks: int = 10) -> list[dict]:
    """
    Fetches recent blocks and computes fee revenue and subsidy breakdown.
    Uses mempool.space block API.
    """
    resp   = httpx.get("https://mempool.space/api/v1/blocks")
    resp.raise_for_status()
    blocks = resp.json()[:num_blocks]

    stats = []
    for block in blocks:
        subsidy_sats    = block.get("reward", 0) - block.get("totalFees", 0)
        fee_sats        = block.get("totalFees", 0)
        total_sats      = block.get("reward", 0)
        fee_share       = (fee_sats / total_sats * 100) if total_sats else 0
        avg_fee_rate    = block.get("extras", {}).get("avgFeeRate", None)

        stats.append({
            "height":          block["height"],
            "subsidy_btc":     round(subsidy_sats / 1e8, 8),
            "fee_btc":         round(fee_sats / 1e8, 8),
            "total_reward_btc": round(total_sats / 1e8, 8),
            "fee_share_pct":   round(fee_share, 2),
            "avg_fee_rate_sat_vb": avg_fee_rate,
            "tx_count":        block.get("tx_count"),
        })

    return stats

for block in get_recent_block_fee_stats(5):
    print(block)
Enter fullscreen mode Exit fullscreen mode

The Long-Term Implication: Fee Market Maturity

Each halving compresses subsidy revenue and increases the weight of fees in miner economics. At the 2028 halving, the subsidy will drop to 1.5625 BTC. At 2032, it will be 0.78125 BTC.

The long-term security model of Bitcoin depends entirely on fees being sufficient to incentivize honest mining once the subsidy becomes negligible. That is not a solved problem. It is the reason the Ordinals debate is not just cultural, high-throughput data embedding increases fee density, which helps miners. Restricting it, as BIP-110 proposes, removes a source of fee revenue at exactly the period in Bitcoin's lifecycle when fee revenue matters most.

# Project subsidy and fee revenue requirements over future halvings

def halving_projection(btc_price: float, target_miner_revenue_usd: float) -> list[dict]:
    """
    Projects how much fee revenue per block miners would need
    to maintain a given total USD revenue target at each halving.
    """
    subsidy = 3.125  # current post-2024 halving
    rows    = []

    for halving in range(5):   # next 5 halvings
        subsidy_usd       = subsidy * btc_price
        required_fee_usd  = target_miner_revenue_usd - subsidy_usd
        required_fee_btc  = required_fee_usd / btc_price

        rows.append({
            "halving_number":       halving + 4,  # we are at halving #4 now
            "block_subsidy_btc":    round(subsidy, 8),
            "subsidy_usd":          round(subsidy_usd, 2),
            "fee_needed_usd":       round(max(required_fee_usd, 0), 2),
            "fee_needed_btc":       round(max(required_fee_btc, 0), 8),
        })
        subsidy /= 2

    return rows

TARGET_USD_PER_BLOCK = 200_000   # roughly today's total miner revenue at $65k BTC

for row in halving_projection(65_000, TARGET_USD_PER_BLOCK):
    print(row)

# Output:
# {'halving_number': 4, 'block_subsidy_btc': 3.125,   'subsidy_usd': 203125.0, 'fee_needed_usd': 0,        ...}
# {'halving_number': 5, 'block_subsidy_btc': 1.5625,  'subsidy_usd': 101562.5, 'fee_needed_usd': 98437.5,  ...}
# {'halving_number': 6, 'block_subsidy_btc': 0.78125, 'subsidy_usd': 50781.25, 'fee_needed_usd': 149218.75,...}
# ...
Enter fullscreen mode Exit fullscreen mode

At halving #5 (2028), miners would need roughly $98,000 per block in fees alone just to match today's total revenue at current prices. That is not a catastrophe, it is a design constraint that the ecosystem will have to grow into. But it frames every current argument about block space usage in a completely different light.


Further Reading


Next in this series: Lightning Network Development in 2026 LND v0.21, Core Lightning's quantum-resistant channels, and how to build on the Lightning stack today.

Top comments (0)