BitChat Performance Characteristics: Throughput Benchmarks and Architectural Optimizations Explained
BitChat achieves up to 30,000 events/second for Nostr deduplication, 11,000 packets/second for BLE mesh processing, and sub-millisecond GCS filter operations through a combination of zero-copy protocols, LZ4 compression, and adaptive battery-aware scheduling.
The permissionlesstech/bitchat repository implements a dual-transport messaging protocol that bridges local Bluetooth-LE mesh networks with the global Nostr relay network. Understanding BitChat performance characteristics requires examining both the micro-benchmarks that define its throughput floors and the architectural decisions that enable efficient operation on resource-constrained mobile devices.
Performance Benchmarks from PerformanceBaselineTests
The PerformanceBaselineTests.swift suite establishes concrete throughput guarantees for critical code paths. These benchmarks run in CI with parallelism enabled to measure theoretical maximums rather than contention-limited behavior.
Nostr Inbound Processing
| Path | Throughput | Description |
|---|---|---|
| Fresh events | ~2,000 events/sec | Full processing including deduplication, bookkeeping, and scheduling for newly-seen geo-hash events |
| Duplicate events | ~30,000 events/sec | Fast-path deduplication for already-processed events, skipping cryptographic verification |
The dramatic difference between fresh and duplicate paths demonstrates the effectiveness of BitChat's deduplication layer. As implemented in testNostrInboundFresh and testNostrInboundDuplicate at PerformanceBaselineTests.swift#L14-L104, the duplicate path avoids all cryptographic operations and operates purely on compact key comparisons.
BLE Mesh Processing
The Bluetooth-LE pipeline measured in testBLEInboundPacketPipeline achieves ~11,000 packets/second for decode-plus-deduplication of 100-300 byte packets. This benchmark at PerformanceBaselineTests.swift#L38-L92 exercises:
- Binary protocol decoding via
BitchatPacket.from() MessageDeduplicator.isDuplicate()calls with O(1) key lookup- Round-trip validation without actual radio transmission
GCS Filter Operations
Golomb-Coded Set filters enable efficient mesh state synchronization with ~140 filters/second build-and-decode throughput for 1,000 packet ID sets. The benchmark at PerformanceBaselineTests.swift#L94-L122 validates the implementation in GCSFilter.swift, which provides sub-millisecond membership tests that limit bandwidth during peer synchronization.
Message Delivery Status Updates
| Component | Throughput | Notes |
|---|---|---|
| Coordinator path | ~9,000 updates/sec | Through DeliveryCoordinator.incrementalUpdate() with full orchestration |
| Direct store path | ~13,000 updates/sec | Bypassing coordinator overhead for raw store mutations |
These benchmarks at PerformanceBaselineTests.swift#L24-L94 demonstrate the cost abstraction: ~30% overhead for coordinator-mediated updates versus direct store access.
Message Processing Pipelines
- Private ingest: ~1,000 messages/second through
ChatViewModelfull private-message pipeline (PerformanceBaselineTests.swift#L148-L200) - Public ingest: ~900 messages/second with 50-sender multiplexing (PerformanceBaselineTests.swift#L202-L266)
- Message formatting: ~1,000 messages/second with mention/hashtag/URL parsing (PerformanceBaselineTests.swift#L96-L146)
The private/public throughput gap reflects additional validation and routing logic for public channel messages from multiple senders.
ConversationStore Append Performance
| Scenario | Throughput | Characteristics |
|---|---|---|
| General append | ~5,000 messages/sec | 1,000 appends with out-of-order inserts, binary-search positioning |
| Steady-state append | ~12,000 messages/sec | Appends under 1,337-message retention cap with eviction path |
The steady-state benchmark at PerformanceBaselineTests.swift#L306-L360 demonstrates that the eviction path—removing oldest messages to maintain the retention window—actually outperforms general insertion due to reduced sorting overhead for append-mostly workloads.
Store Audit Throughput
The invariant checker at PerformanceBaselineTests.swift#L362-L388 achieves ~80 audits/second over 5,000-message corpora. This validates timestamp ordering, ID uniqueness, and retention compliance—suitable for debug builds but disabled in production.
Architectural Optimizations Enabling BitChat Performance
Zero-Copy Binary Protocol
The BinaryProtocol implementation at BinaryProtocol.swift#L44-L86 uses Swift Data views rather than buffer copies:
// From BinaryProtocol.swift - zero-copy view creation
public func readBytes(count: Int) -> Data? {
guard position + count <= data.count else { return nil }
let view = data[position..<position+count] // No allocation, just slice
position += count
return Data(view) // Only copies when explicitly converted
}
This design minimizes heap pressure during high-frequency BLE packet processing, keeping garbage collection pauses negligible on mobile devices.
LZ4 Compression for BLE Payloads
As documented in README.md#L34, LZ4 compression reduces payload size before BLE transmission. The compression ratio typically achieves 2-4x for text messages, directly reducing:
- Radio-on time and energy consumption
- Packet fragmentation across BLE MTU limits
- Collision probability in shared mesh spectrum
Noise Protocol for Mesh Encryption
The NoiseEncryptionService at NoiseEncryptionService.swift#L78 implements the Noise Protocol Framework for forward-secure, zero-round-trip handshakes. Compared to TLS or naive ECDH, Noise provides:
- Smaller per-message overhead (16 bytes MAC vs. typical TLS record)
- No certificate parsing or validation latency
- Constant-time operations suitable for timing-attack resistance
Message Deduplication Architecture
Both Nostr and BLE paths share the MessageDeduplicator with O(1) key lookup. The deduplicator stores 64-bit SipHash outputs of message IDs, providing collision-resistant duplicate detection with minimal memory footprint. Keys expire based on time-to-live policies coordinated with mesh synchronization windows.
Adaptive Battery-Aware Scheduling
BitChat implements three power modes controlled by activity heuristics:
| Mode | BLE Scan Interval | Trigger | Use Case |
|---|---|---|---|
| Active | 30ms | User in conversation | Real-time responsiveness |
| Background | 300ms | App backgrounded, recent activity | Opportunistic sync |
| Dormant | 3000ms | Extended idle period | Battery preservation |
This adaptation at README.md#L34 maintains connectivity without the ~100mW continuous drain of aggressive BLE scanning.
Single-Source ConversationStore Design
The ConversationStore architecture documented in CONVERSATION-STORE-DESIGN.md eliminates the N+1 indexing problem common in ORM-based messaging apps. All mutations funnel through one ordered array with binary-search insertion:
// Representative usage pattern from PerformanceBaselineTests
let store = ConversationStore()
store.append(message, to: .mesh) // O(log n) via binary search on timestamp
The retention-capped steady state achieves 2.4x higher throughput than general insertion because eviction appends to the end rather than requiring position search.
Running and Interpreting Performance Tests
Local Benchmark Execution
# Skip performance tests for rapid functional iteration
BITCHAT_SKIP_PERF_BASELINES=1 swift test
# Run full performance suite with logging
BITCHAT_PERF_LOG=perf.log swift test --filter PerformanceBaselineTests
# Parse results
grep "PERF\[" perf.log
The BITCHAT_PERF_LOG environment variable triggers human-readable throughput reporting. The CI script at scripts/check-perf-floors.sh validates these against minimum thresholds to prevent performance regressions.
Understanding Benchmark Output Format
PERF[store.append]: 5400 messages/sec (avg 0.185 ms per pass of 1000, 5 passes)
- 5400 messages/sec: Sustained throughput across 5 measurement passes
- 0.185 ms per pass: Amortized time for 1,000 append operations
- 5 passes: Statistical sample count for variance reduction
Custom Throughput Measurement
import XCTest
@testable import bitchat
func measureCustomPipeline() {
let dedup = MessageDeduplicator()
let packets = (0..<1000).map { _ in generateTestPacket() }
let start = CFAbsoluteTimeGetCurrent()
for packet in packets {
let data = packet.toBinaryData()!
let decoded = BitchatPacket.from(data)!
_ = dedup.isDuplicate(dedupKey(for: decoded))
}
let elapsed = CFAbsoluteTimeGetCurrent() - start
let throughput = Double(packets.count) / elapsed
print("Custom throughput: \(Int(throughput)) packets/sec")
}
This mirrors the methodology in testBLEInboundPacketPipeline while allowing domain-specific extensions.
Key Source Files for Performance Analysis
| File | Purpose | Performance Relevance |
|---|---|---|
PerformanceBaselineTests.swift |
Micro-benchmark suite defining throughput floors | Source of all quantitative claims in this analysis |
BinaryProtocol.swift |
Zero-copy encoding/decoding | Enables 11k packets/sec BLE processing |
NoiseEncryptionService.swift |
Fast cryptographic operations | Sub-millisecond per-message overhead |
ConversationStore.swift |
Ordered message storage | O(log n) insertion with retention management |
MessageDeduplicator.swift |
O(1) duplicate detection | Powers 30k events/sec fast path |
GCSFilter.swift |
Bandwidth-efficient sync | 140 filters/sec build/decode |
ChatViewModel.swift |
Pipeline orchestration | 900-1,000 messages/sec end-to-end ingest |
scripts/check-perf-floors.sh |
CI regression prevention | Validates benchmarks against minima |
Summary
BitChat performance characteristics reflect deliberate architectural tradeoffs for dual-transport messaging:
- Nostr path: 2,000-30,000 events/second depending on deduplication hit rate, with cryptographic verification as the bottleneck
- BLE mesh path: 11,000 packets/second decode/deduplication, enabled by zero-copy binary protocol and fast Noise encryption
- Storage layer: 5,000-12,000 messages/second append with retention management, using binary-search ordered insertion
- Sync optimization: GCS filters at 140 operations/second for bandwidth-efficient mesh state reconciliation
- Power efficiency: Adaptive battery modes scaling from 30ms to 3000ms scan intervals based on activity heuristics
These figures derive from the PerformanceBaselineTests suite and represent upper bounds on modern hardware. Mobile devices typically achieve 30-60% of CI-measured throughput due to thermal throttling and background execution limits, still well within interactive latency requirements for real-time messaging.
Frequently Asked Questions
What limits BitChat's Nostr ingestion throughput?
Cryptographic verification of fresh events bounds fresh-event processing to ~2,000 events/second. The duplicate path at ~30,000 events/second demonstrates that parsing and deduplication are not bottlenecks—ED25519 signature verification and SHA-256 hashing dominate CPU usage for unseen events.
How does BitChat maintain performance as conversation history grows?
The ConversationStore uses fixed-size retention windows (default 1,337 messages per channel) with O(1) eviction of oldest entries. Steady-state benchmarks show 2.4x higher throughput than general insertion because append-mostly workloads with eviction avoid binary-search positioning costs.
Can performance benchmarks detect regressions in CI?
Yes. The scripts/check-perf-floors.sh script parses BITCHAT_PERF_LOG output and fails builds when any benchmark drops below established floors. These floors are set at ~80% of observed典型CI performance to account for runner variance while catching meaningful regressions.
Why is BLE throughput measured without actual radio transmission?
The testBLEInboundPacketPipeline benchmark isolates protocol processing from radio variability. Actual mesh performance depends on environmental factors (interference, distance, concurrent transmitters) that would introduce noise into software optimization measurements. The 11,000 packets/second figure represents the CPU-bound ceiling—radio limitations typically reduce effective throughput to 50-200 packets/second in dense urban environments.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →