How to Process High-Volume Signatures in the Background with a Wallet
author By Admin
calendar 2026-05-22

How to Process High-Volume Signatures in the Background with a Wallet

Signing a single transaction feels instant. Signing fifty at once each with its own cryptographic work is a different problem entirely. Naive main-thread signing freezes the UI; naïve background signing loses ordering guarantees and wastes resources. This post covers why the problem exists, how Bitcoin and EVM signing differ, and how to build a background worker system that handles it cleanly.

Why High-Volume Signing Is a Challenge

Every signature involves ECDSA over secp256k1 (or Schnorr for Bitcoin Taproot) a CPU-bound operation that cannot be cached or delegated to the network. In normal use, one signature per user action is fine. High-volume scenarios break that assumption:

  • Exchange wallets sweeping hundreds of UTXOs into a cold wallet
  • DeFi bots submitting batches of signed ERC-20 approvals
  • NFT minting tools signing metadata for thousands of tokens
  • Hardware wallet emulators processing queued offline transactions
  • Multi-party signing coordinators aggregating signatures from multiple keys

CPU-bound signing on the main thread blocks JavaScript's single-threaded event loop: the UI stalls, animations freeze, users see a blank wallet. The solution is to move signing into isolated background workers and process batches in parallel but first you need to understand what the per-chain signing work actually looks like.

Signing Differences: Bitcoin vs. EVM

Bitcoin One Signature Per UTXO Input

Bitcoin transactions consume UTXOs (Unspent Transaction Outputs) as inputs; each input must be signed independently with the private key that controls it. A transaction spending five UTXOs requires five separate ECDSA signatures, each over a different sighash derived from that specific input. This makes Bitcoin signing embarrassingly parallel each input is independent and can be dispatched to a separate worker without coordination overhead. Taproot (BIP-340) switches the signature algorithm to Schnorr but preserves the per-input structure; MuSig2 aggregates multiple co-signers but does not eliminate the per-party signing work.

Bitcoin tx (5 inputs):

Input 0 → sighash_0 → ECDSA(privKey_A) → sig_0
Input 1 → sighash_1 → ECDSA(privKey_A) → sig_1
Input 2 → sighash_2 → ECDSA(privKey_B) → sig_2
Input 3 → sighash_3 → ECDSA(privKey_A) → sig_3
Input 4 → sighash_4 → ECDSA(privKey_C) → sig_4

All inputs independent → assemble → broadcast

EVM One Signature Per Transaction, with Complications

EVM chains require one ECDSA signature per transaction, but three patterns introduce real signing volume at scale: (1) EIP-712 permit batches each permit for a DeFi operation needs a separate signTypedData call over a unique domain-separated hash; (2) Multicall sub-calls may each require their own approval signature before the transaction is assembled; (3) Multi-sig wallets such as Gnosis Safe require M-of-N keyholders to each independently sign the same transaction hash, with a coordinator collecting and assembling the threshold.

EIP-712 batch (10 permits):

permit_hash_0 → sig_0
permit_hash_1 → sig_1
...
permit_hash_9 → sig_9

→ multicall payload → 1 tx → broadcast

Multi-sig (3-of-5):

tx_hash → ECDSA(key_1) → sig_1 ■
tx_hash → ECDSA(key_2) → sig_2 ■ → execute
tx_hash → ECDSA(key_3) → sig_3 ■

JavaScript Runtime: Event Loop and Web Workers

A JavaScript runtime has a call stack, a heap for memory, a task queue, and an event loop that pulls tasks from the queue only when the stack is empty. CPU-bound ECDSA signing occupies the call stack continuously the event loop cannot process any UI events, timers, or network callbacks until it finishes. That is why a synchronous signing loop on the main thread freezes the wallet.

A Web Worker is a fully isolated JS runtime its own call stack, heap, event loop, and global scope (self, not window) running on a separate OS thread. This means it executes truly in parallel with the main thread, not interleaved. Workers cannot access the DOM, but they can use SubtleCrypto, fetch, IndexedDB, and WebAssembly everything needed for cryptographic signing work.

The only communication channel between the main thread and a worker is postMessage(), which serialises messages using the structured clone algorithm. Every message is a deep copy in the receiving heap objects are not passed by reference. Large buffers such as key material and transaction data should be sent as Transferable ArrayBuffer objects to move them zero-copy rather than cloning.

Background Worker Architecture for Signing

Worker Pool Design

Rather than spawning one worker per task (expensive), maintain a fixed pool of N = navigator.hardwareConcurrency − 1 workers, leaving one thread for the UI. Workers are reused across batches and never torn down mid-session. A dispatcher on the main thread assigns free workers from the queue and awaits all results via Promise.all().

Main Thread

SigningQueue: [task_0, task_1, … task_N]
WorkerPool: [worker_0, worker_1, worker_2, …]

Dispatcher → postMessage() → workers
← onmessage ← results

Each worker runs ECDSA/Schnorr on its own OS thread truly parallel.

Bitcoin UTXO Batch Signing

Bitcoin's per-input independence maps directly onto the worker pool. Chunk the inputs evenly, dispatch one chunk per worker, and reassemble the complete transaction once all Promise.all() branches resolve. Wall time is approximately the time to sign a single chunk, not all inputs combined.

20 inputs, 4 workers:

[0-4]→worker_0
[5-9]→worker_1
[10-14]→worker_2
[15-19]→worker_3

■■■■■■■■■■■■■■■■■■■■ Promise.all() ■■■■■■■■■■■■■■■■■■■■

sigs[0..19] reassembled → build & broadcast tx

Wall time ≈ signing 5 inputs (not 20)

EVM EIP-712 Batch Signing

Each EIP-712 hash is independent and can be dispatched in parallel. Signs within a single worker are kept serial to preserve nonce integrity. Once all workers report back, the main thread assembles the multicall payload and adds one final transaction signature before broadcast.

Performance Comparison

Scenario                                Approach                    Wall Time     UI Blocked?

BTC 20-input tx                         Main thread (serial)        ~2000 ms      YES
BTC 20-input tx                         4 workers (parallel)        ~500 ms       No
EVM 10 EIP-712 permits                  Main thread (serial)        ~1000 ms      YES
EVM 10 EIP-712 permits                  2 workers (parallel)        ~500 ms       No
EVM multi-sig (5 keys)                  Main thread (serial)        ~500 ms       YES
EVM multi-sig (5 keys)                  5 workers (parallel)        ~100 ms       No

Timings are estimates based on ~100 ms per ECDSA secp256k1 op in WebAssembly.

Memory Isolation and Security Implications

Worker isolation is not just a performance property it has direct security implications for wallet design. A compromised worker cannot read the main thread's memory or another worker's heap. A crashing worker does not crash the main thread. Key material is copied into the worker heap during signing, however.

Mitigate this by: (1) zeroing the ArrayBuffer after signing completes; (2) using Transferable objects to move rather than copy; (3) using SubtleCrypto's non-extractable CryptoKey objects where possible these live in the browser's secure key store and cannot be read from JavaScript memory, even from within the worker that invokes them. The signing operation happens inside the browser engine; your code only ever sees the output signature bytes.

Putting It Together

High-volume signing in a wallet is a systems design problem first, a cryptography problem second. The algorithm is fixed ECDSA or Schnorr as the chain demands. What you control is the execution architecture:

User Action
↓
Main Thread
1. Build unsigned tx(s) / hashes
2. Chunk work across N = hardwareConcurrency − 1 workers
3. Dispatch via postMessage()
4. Await Promise.all() from worker pool ← UI stays live
5. Reassemble signed tx
6. Broadcast

Worker Pool [Worker 0] [Worker 1] [Worker 2] …
SubtleCrypto ECDSA/Schnorr per chain

Bitcoin: parallel per UTXO input
EVM: parallel per EIP-712 hash / per keyholder

The same architecture serves both chains. Bitcoin's per-input parallelism maps naturally; EVM's higher-level scenarios (permit batches, multi-sig) require a thin coordination layer on the main thread before and after the parallel work, but the signing itself is identical. Get this right and a wallet can sign fifty transactions in the same wall time as one without the user ever seeing a frozen UI.

What's the highest-volume signing scenario you've had to handle in production? Drop it in the comments.

Share: