- Transaction batching:
fetch_transactions_batch()used extensively - Address history batching:
fetch_address_histories_batch() - Balance batching:
get_balances_batch()exists - Parallel batch execution: Pool splits large batches (>50 requests) across multiple servers
Current: Batches split across servers only when >50 requests Issue: Smaller batches (10-49 requests) still go to single server Solution: Lower threshold to 10 requests for better parallelization
Impact: 5x faster for medium-sized batches (10-50 items)
# electrum_pool.py
async def execute_batch(self, requests: List[Tuple[str, List[Any]]], parallel: bool = False):
# Current: if parallel and len(requests) > 50:
# Optimized: if parallel and len(requests) > 10:
if parallel and len(requests) > 10: # NEW THRESHOLD
return await self._execute_batch_parallel(requests)
else:
return await self._execute_batch_single(requests)Current: Splits batches evenly across all healthy servers Issue: Can create tiny batches if many servers available Solution: Use optimal batch size (20-30 requests per server)
# electrum_pool.py
async def _execute_batch_parallel(self, requests: List[Tuple[str, List[Any]]]):
healthy = [c for c in self.connections if c.state == ConnectionState.CONNECTED]
# Current: chunk_size = (len(requests) + len(healthy) - 1) // len(healthy)
# Optimized: Use optimal batch size (20-30 per server)
OPTIMAL_BATCH_SIZE = 25
num_servers_needed = (len(requests) + OPTIMAL_BATCH_SIZE - 1) // OPTIMAL_BATCH_SIZE
servers_to_use = healthy[:num_servers_needed]
chunk_size = (len(requests) + len(servers_to_use) - 1) // len(servers_to_use)Current: parallel parameter must be explicitly set to True
Issue: Some callers forget to set parallel=True
Solution: Auto-enable parallel for large batches
# electrum_multiplexer.py
async def _batch_call(self, requests: List[tuple]) -> List[Any]:
# Auto-enable parallel for large batches
parallel = len(requests) > 10
return await self.pool.execute_batch(requests, parallel=parallel)Status: Method exists but not used everywhere
Locations to update:
fetch_address_info()could batch balance lookups- Bulk address endpoint could use
get_balances_batch()
Status: Not implemented
Add new method:
async def get_utxos_batch(self, addresses: List[str]) -> List[List[Dict]]:
"""Get UTXOs for multiple addresses in batch"""
from app.services.electrum_client import ElectrumClient
requests = [
("blockchain.scripthash.listunspent", [ElectrumClient._address_to_scripthash(addr)])
for addr in addresses
]
return await self._batch_call(requests)Current: Cache checks are sequential Issue: Miss large batching opportunity Solution: Batch fetch all uncached items at once
# blockchain_data.py
async def fetch_transactions_batch(self, txids: List[str]) -> List[Transaction]:
# Group cache checks
cached_results = {}
uncached_txids = []
for txid in txids:
cache_key = f"tx:{txid}"
cached = await self._get_cache(cache_key)
if cached:
cached_results[txid] = Transaction(**json.loads(cached))
else:
uncached_txids.append(txid)
# Batch fetch ALL uncached at once (already doing this)
if uncached_txids:
tx_data_list = await electrum.get_transactions_batch(uncached_txids, verbose=True)
# ...Current: Fetches input TXs as needed Opportunity: Collect ALL needed TXIDs first, then batch fetch
# CURRENT (already good):
input_txids = [inp.txid for inp in start_tx.inputs if inp.txid]
input_txs = await blockchain_service.fetch_transactions_batch(input_txids)
# Already optimal! ✅Current: All requests in a batch must be same type Opportunity: Allow mixed-type batches
Note: Electrum protocol supports mixed requests in single batch!
# Example: Batch different request types together
requests = [
("blockchain.transaction.get", [txid1, True]),
("blockchain.transaction.get", [txid2, True]),
("blockchain.scripthash.get_balance", [script_hash1]),
("blockchain.scripthash.get_history", [script_hash2]),
]
results = await electrum._batch_call(requests)Current: No limit on batch size Issue: Very large batches (>100) may timeout Solution: Automatic batch chunking
# electrum_multiplexer.py
async def get_transactions_batch(self, txids: List[str], verbose: bool = True):
MAX_BATCH_SIZE = 100
if len(txids) <= MAX_BATCH_SIZE:
# Normal batch
requests = [("blockchain.transaction.get", [txid, verbose]) for txid in txids]
return await self._batch_call(requests)
else:
# Auto-chunk large batches
chunks = [txids[i:i + MAX_BATCH_SIZE] for i in range(0, len(txids), MAX_BATCH_SIZE)]
tasks = [self.get_transactions_batch(chunk, verbose) for chunk in chunks]
chunk_results = await asyncio.gather(*tasks)
return [item for chunk in chunk_results for item in chunk]Current: May fetch same TX/address multiple times Opportunity: Deduplicate before fetching
async def fetch_transactions_batch(self, txids: List[str]) -> List[Transaction]:
# Deduplicate input
unique_txids = list(dict.fromkeys(txids)) # Preserve order
# Fetch unique items
unique_results = await self._fetch_unique(unique_txids)
# Map back to original order (with duplicates)
result_map = {tx.txid: tx for tx in unique_results}
return [result_map.get(txid) for txid in txids]Current: Round-robin selection per request Opportunity: Batch-aware server selection
# electrum_pool.py
async def _execute_batch_parallel(self, requests: List[Tuple[str, List[Any]]]):
healthy = [c for c in self.connections if c.state == ConnectionState.CONNECTED]
# Sort servers by current load (in-flight requests)
servers_by_load = sorted(healthy, key=lambda c: c.metrics.in_flight_requests)
# Assign chunks to least-loaded servers first
# ... distribute batches based on server load ...- ✅ Lower parallel threshold (50 → 10)
- ✅ Auto-enable parallel for large batches
- ✅ Add batch size limit (100 requests max)
- ✅ Optimize batch splitting algorithm
- ✅ Add missing batch methods (UTXOs)
- ✅ Smart batch grouping by type
- ✅ Batch request deduplication
- ✅ Load-aware batch distribution
- ✅ Batch cache warming optimization
10 transactions: Sequential → 1,000ms (10 x 100ms)
50 transactions: Single batch → 500ms
100 transactions: Single batch → 1,000ms (timeout risk)
10 transactions: Parallel batch (2 servers) → 100ms (10x faster)
50 transactions: Parallel batch (5 servers) → 100ms (5x faster)
100 transactions: Auto-chunked parallel → 200ms (5x faster + reliable)
- Average batch size: Should increase as more calls use batching
- Parallel batch ratio: % of batches using parallel execution
- Batch latency: Average time per batch (target: <200ms)
- Cache hit rate: Should improve with better batch warming
- Server load distribution: Should be even across pool
- Unit tests: Test batch methods with 1, 10, 50, 100, 500 requests
- Integration tests: Trace heavy TX (348 inputs) with batching
- Load tests: 1000 concurrent requests using batched APIs
- Benchmark: Compare before/after for common operations
✅ All batches >10 requests use parallel execution ✅ No single server handles >30 requests in a batch ✅ Batch operations 5-10x faster than sequential ✅ No timeouts on large batches (auto-chunking works) ✅ Cache hit rate >80% for repeated requests