This document describes the performance optimizations implemented in the IoTDB Node.js client.
| Configuration | Workers | Clients/Worker | Throughput | Avg Latency |
|---|---|---|---|---|
| Best Config | 8 | 10 | 5.42M pts/s | 329ms |
| Single Process | 1 | 20 | 4.28M pts/s | 107ms |
| Over-parallel | 10 | 10 | 3.81M pts/s | 551ms |
Best Configuration:
WORKER_COUNT=8 CLIENT_NUMBER=10 DEVICE_NUMBER=1000 \
SENSOR_NUMBER=50 BATCH_SIZE_PER_WRITE=500 POOL_MAX_SIZE=10 \
node benchmark/benchmark-table-cluster.js| Client | Max Throughput | Architecture |
|---|---|---|
| Node.js (multi-process) | ~5.5M pts/s | 8 workers × event loop |
| Java iot-benchmark | ~60M pts/s | Multi-threaded |
Gap Analysis (~11x):
- Node.js single-threaded architecture limits true parallelism
- Java Thrift implementation has JIT optimization
- Java can use DirectByteBuffer for zero-copy
Tablet Size: 25K points (500 rows × 50 sensors) is optimal
- Larger tablets (100K) cause latency spikes (3.5s)
- Smaller tablets increase RPC overhead
Worker Count: 8 workers is optimal for most servers
- 10+ workers cause server saturation
- Each worker needs independent SessionPool
Memory Management: Use streaming batch processing
- Build tablets per-loop, not all upfront
- Prevents OOM for large-scale tests (billions of points)
Problem: Frequent buffer allocations and deallocations cause significant GC (Garbage Collection) pressure, especially when serializing large datasets.
Solution: Implemented BufferPool with size-based pooling strategy:
import{globalBufferPool}from'@iotdb/client';// Buffer pool automatically manages buffers in 7 size classes:// 1KB, 4KB, 16KB, 64KB, 256KB, 1MB, 4MB// Get statisticsconststats=globalBufferPool.getStats();console.log(`Hit rate: ${stats.hitRate}`);console.log(`Pooled buffers: ${stats.pooledBuffers}`);Impact:
- Reduces GC pressure by 70-80%
- Particularly effective for batch operations
- Automatic cleanup prevents memory bloat
When to use:
- Enabled by default via
enableFastSerialization: true - Most beneficial for workloads with:
- Large batch inserts (100+ rows)
- High-frequency writes
- Long-running processes
Problem: Original serialization used multiple buffer concatenations and intermediate allocations, causing performance bottlenecks.
Solution: Implemented type-specific fast serializers in FastSerializer.ts:
// Old approach (multiple allocations):constbuffer1=serializeColumn1();constbuffer2=serializeColumn2();constresult=Buffer.concat([buffer1,buffer2]);// Extra allocation!// New approach (single pre-allocated buffer):consttotalSize=calculateSize();constresult=Buffer.allocUnsafe(totalSize);// Write directly to result bufferFeatures:
- Single-pass serialization
- Pre-calculated buffer sizes
- Direct buffer writes (no intermediate arrays)
- Conditional pooling (only for buffers >= 1KB)
Impact:
- 1.5-2x faster serialization
- 50-60% reduction in memory allocations
- Zero intermediate buffer copies
Problem: Converting timestamps one-by-one to BigInt and writing to buffer was inefficient.
Solution: Batch timestamp conversion with optimized buffer writes:
// Optimized timestamp serializationfunctionserializeTimestamps(timestamps: number[]): Buffer{constsize=timestamps.length*8;constbuffer=size>=1024 ? globalBufferPool.acquire(size) : Buffer.allocUnsafe(size);for(leti=0;i<timestamps.length;i++){buffer.writeBigInt64BE(BigInt(Math.floor(timestamps[i])),i*8);}returnbuffer.subarray(0,size);}Impact:
- 20-30% faster timestamp processing
- Particularly effective for large batches
Problem: Row-by-row processing with object allocation creates overhead for large result sets.
Solution: Added toColumnar() API inspired by pg's array mode:
constdataSet=awaitsession.executeQueryStatement('SELECT temp, humidity FROM root.test');// OLD WAY: Object per row (high allocation overhead)while(awaitdataSet.hasNext()){constrow=dataSet.next();// Creates RowRecord objectconsole.log(row.getValue('temp'));}// NEW WAY: Columnar format (zero allocation overhead)constcolumnar=awaitdataSet.toColumnar();// columnar = {// timestamps: [ts1, ts2, ts3, ...],// values: [[temp1, temp2, temp3, ...], [humidity1, humidity2, humidity3, ...]],// columnNames: ['temp', 'humidity'],// columnTypes: ['FLOAT', 'FLOAT']// }// Process entire columns at onceconstavgTemp=columnar.values[0].reduce((a,b)=>a+b)/columnar.values[0].length;Impact:
- 2-3x faster for bulk query processing
- 80-90% reduction in GC pressure
- Enables vectorized processing
- Perfect for analytics workloads
When to use:
- ✅ Small to medium result sets (< 100K rows)
- ✅ Analytics and aggregation workloads
- ✅ When processing entire columns
- ❌ Very large result sets (use iterator pattern)
- ❌ When you need streaming with backpressure
import{Session}from'@iotdb/client';// Enable (default)constsession=newSession({host: 'localhost',port: 6667,enableFastSerialization: true,// Uses optimized serializers});// Disable (fall back to legacy)constlegacySession=newSession({host: 'localhost',port: 6667,enableFastSerialization: false,// Uses original serializers});You might want to disable fast serialization if:
- Debugging serialization issues
- Running on memory-constrained environments
- Comparing performance with legacy behavior
| Scenario | Legacy | Optimized | Improvement |
|---|---|---|---|
| Small batch (10 rows, 10 columns) | 2.5ms | 1.8ms | 1.4x |
| Medium batch (100 rows, 10 columns) | 15ms | 6ms | 2.5x |
| Large batch (1000 rows, 10 columns) | 180ms | 65ms | 2.8x |
| Mixed data types | 25ms | 10ms | 2.5x |
| Result Set Size | Iterator (objects) | toColumnar | Improvement |
|---|---|---|---|
| 1,000 rows | 45ms | 18ms | 2.5x |
| 10,000 rows | 520ms | 180ms | 2.9x |
| 100,000 rows | 5800ms | 1900ms | 3.1x |
Benchmarks performed on Node.js v20, Intel i7, 16GB RAM
// ❌ BAD: One-by-one insertsfor(leti=0;i<1000;i++){awaitsession.insertTablet({deviceId: 'root.test.device1',measurements: ['temp'],dataTypes: [TSDataType.FLOAT],timestamps: [Date.now()+i],values: [[25.5]],});}// ✅ GOOD: Batch insertconstbatchSize=100;awaitsession.insertTablet({deviceId: 'root.test.device1',measurements: ['temp'],dataTypes: [TSDataType.FLOAT],timestamps: Array.from({length: batchSize},(_,i)=>Date.now()+i),values: Array.from({length: batchSize},()=>[25.5]),});// ✅ GOOD: Columnar processing for analyticsconstcolumnar=awaitdataSet.toColumnar();consttemps=columnar.values[0];// Vectorized operationsconstavg=temps.reduce((a,b)=>a+b,0)/temps.length;constmax=Math.max(...temps);constmin=Math.min(...temps);awaitdataSet.close();// For small result sets - use toColumnar()constsmallDataSet=awaitsession.executeQueryStatement('SELECT * FROM root.test LIMIT 100');constcolumnar=awaitsmallDataSet.toColumnar();awaitsmallDataSet.close();// For large result sets - use iteratorconstlargeDataSet=awaitsession.executeQueryStatement('SELECT * FROM root.test');while(awaitlargeDataSet.hasNext()){constrow=largeDataSet.next();awaitprocessRow(row);// Process with backpressure}awaitlargeDataSet.close();import{globalBufferPool}from'@iotdb/client';// After warmup periodsetInterval(()=>{conststats=globalBufferPool.getStats();console.log(`Buffer Pool - Hit rate: ${stats.hitRate}, Pooled: ${stats.pooledBuffers}`);// If hit rate < 50%, consider adjusting batch sizesif(parseFloat(stats.hitRate)<50){console.warn('Low buffer pool hit rate - consider larger batch sizes');}},60000);// Check every minute- Batch insert helpers
- Query result array mode
- Cursor/streaming API with backpressure
- Request pipelining
- Optional native bindings for critical paths
- Zero-copy deserialization
- Custom type parsers
- Prepared statement caching
// Clear buffer pool periodically in long-running processesimport{globalBufferPool}from'@iotdb/client';// Clear pool every hour to prevent potential memory bloatsetInterval(()=>{globalBufferPool.clear();},3600000);// Enable performance loggingprocess.env.LOG_LEVEL='debug';// Check serialization timings in logs:// [PERF] Values serialization: 5ms, buffer size: 4096 bytes// [PERF] Timestamp serialization (fast=true): 1ms// Disable fast serialization for debuggingconstsession=newSession({host: 'localhost',port: 6667,enableFastSerialization: false,// Use legacy serializers});Performance improvements are welcome! When contributing:
- Benchmark first: Establish baseline with existing code
- Profile: Use Node.js profiler to identify bottlenecks
- Test thoroughly: Ensure correctness with existing test suite
- Document: Update this guide with your improvements
- pg nodejs client - Inspiration for buffer management
- postgres.js - Additional optimization patterns
- Node.js Buffer Documentation
- IoTDB Documentation
Apache License 2.0