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:
- 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.
- Fee revenue became structurally more important. Miners now have a stronger financial incentive to prioritize high-fee transactions and, crucially, to keep blocks full.
- 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}
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())
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}
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())
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)
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,...}
# ...
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
- mempool.space API documentation
- Bitcoin Core
estimatesmartfeeRPC - Spark Research — Bitcoin Mining Economics 2026
- BIP 9 — Version bits with timeout and delay
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)