Actora Logo
engineering

Scaling Analytics with Rust: Processing Millions of Events Per Second

How we rebuilt our analytics infrastructure in Rust to handle 10M+ events per second while reducing costs by 70%.

Actora Engineering Team

Actora Engineering Team

Oct 25, 20257 min
Scaling Analytics with Rust: Processing Millions of Events Per Second

Scaling Analytics with Rust: Processing Millions of Events Per Second

Six months ago, our Node.js analytics pipeline was buckling under pressure. Today, our Rust-powered infrastructure processes over 10 million events per second with room to spare. Here's how we did it.

The Breaking Point

As Actora grew, so did the volume of on-chain and off-chain events we needed to track. Every quest completion, token transaction, and user interaction generated analytics data that needed to be processed in real-time.

By Q2 2024, we were facing critical bottlenecks:

  • High Latency: P99 response times exceeded 5 seconds
  • Memory Leaks: Node.js processes consuming 8GB+ RAM
  • Infrastructure Costs: $45K/month and climbing
  • Scaling Limits: Vertical scaling no longer effective
  • Data Loss: Events dropped during traffic spikes

Something had to change. Fast.

Why Rust?

After evaluating Go, C++, and Rust, we chose Rust for several compelling reasons:

1. Zero-Cost Abstractions

Write high-level code that compiles to blazing-fast machine instructions without runtime overhead.

// Elegant syntax, optimal performance
let total: u64 = events
    .par_iter()
    .filter(|e| e.event_type == EventType::QuestComplete)
    .map(|e| e.value)
    .sum();

2. Memory Safety Without Garbage Collection

No GC pauses means consistent, predictable latency — critical for real-time analytics.

3. Fearless Concurrency

Rust's ownership system prevents data races at compile time, making parallel processing safe and efficient.

4. Ecosystem Maturity

Libraries like Tokio, Serde, and Rayon provide production-ready async runtime and serialization.

Architecture Overview

Our new analytics pipeline consists of three core components:

Event Ingestion Layer

Technology: Actix-web + Tokio

  • Handles incoming HTTP/WebSocket events
  • Validates and enriches data
  • Routes to processing queues
  • Throughput: 150K requests/second per instance

Processing Engine

Technology: Custom Rust pipeline with Rayon

  • Parallel event processing
  • Real-time aggregation
  • Deduplication and filtering
  • Latency: P99 < 50ms

Storage Layer

Technology: ClickHouse + Redis

  • Time-series analytics database
  • Hot cache for recent queries
  • Automatic data rollups
  • Query Performance: Sub-100ms for complex aggregations

The Migration Journey

Phase 1: Proof of Concept (2 weeks)

We started with the most critical bottleneck: event aggregation.

Before (Node.js):

// Blocking, single-threaded
const aggregated = events.reduce((acc, event) => {
  acc[event.type] = (acc[event.type] || 0) + 1;
  return acc;
}, {});

After (Rust):

// Parallel, zero-copy
use rayon::prelude::*;
use dashmap::DashMap;

let aggregated: DashMap<EventType, u64> = DashMap::new();
events.par_iter().for_each(|event| {
    aggregated.entry(event.event_type)
        .and_modify(|count| *count += 1)
        .or_insert(1);
});

Result: 40x performance improvement on the same hardware.

Phase 2: Incremental Rollout (6 weeks)

Rather than a risky big-bang rewrite, we rolled out Rust services gradually:

Week 1-2: Event ingestion endpoints Week 3-4: Real-time aggregation pipelines
Week 5-6: Historical query optimization

We ran both systems in parallel, comparing outputs to ensure correctness.

Phase 3: Full Production (4 weeks)

Once confidence was high, we:

  1. Migrated all analytics traffic to Rust
  2. Decommissioned legacy Node.js services
  3. Optimized infrastructure costs
  4. Implemented advanced monitoring

Technical Deep Dive

Efficient Memory Management

One of Rust's superpowers is zero-copy deserialization with libraries like serde:

use serde::{Deserialize, Serialize};
use bytes::Bytes;

#[derive(Deserialize, Serialize)]
struct AnalyticsEvent {
    user_id: u64,
    event_type: EventType,
    timestamp: i64,
    metadata: HashMap<String, Value>,
}

// Zero-copy deserialization from bytes
fn parse_event(data: &[u8]) -> Result<AnalyticsEvent> {
    bincode::deserialize(data)
}

This approach eliminates intermediate allocations, reducing memory pressure by 85%.

Lock-Free Data Structures

For high-concurrency scenarios, we use lock-free data structures:

use crossbeam::queue::SegQueue;
use std::sync::Arc;

// Lock-free queue for event buffering
let event_queue: Arc<SegQueue<Event>> = Arc::new(SegQueue::new());

// Multiple producers, no locks needed
let queue = event_queue.clone();
tokio::spawn(async move {
    queue.push(event);
});

Async/Await with Tokio

Tokio's runtime provides efficient async I/O for handling thousands of concurrent connections:

use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> Result<()> {
    let listener = TcpListener::bind("0.0.0.0:8080").await?;

    loop {
        let (mut socket, _) = listener.accept().await?;

        tokio::spawn(async move {
            let mut buffer = vec![0; 1024];
            socket.read(&mut buffer).await.unwrap();
            process_event(&buffer).await;
        });
    }
}

Compile-Time Optimizations

Rust's compiler provides aggressive optimizations when building for release:

[profile.release]
opt-level = 3              # Maximum optimization
lto = "fat"                # Link-time optimization
codegen-units = 1          # Better optimization
panic = "abort"            # Smaller binary size
strip = true               # Remove debug symbols

These settings reduced our binary size by 60% and improved performance by an additional 15%.

Performance Benchmarks

Here's how our Rust implementation compares to the previous Node.js system:

Throughput

  • Node.js: 120K events/second
  • Rust: 10M+ events/second
  • Improvement: 83x

Latency (P99)

  • Node.js: 5.2 seconds
  • Rust: 47 milliseconds
  • Improvement: 110x faster

Memory Usage

  • Node.js: 8.2 GB per instance
  • Rust: 480 MB per instance
  • Improvement: 94% reduction

CPU Utilization

  • Node.js: 85% average
  • Rust: 32% average
  • Improvement: Better hardware utilization

Infrastructure Cost

  • Node.js: $45K/month
  • Rust: $13K/month
  • Savings: 71% reduction

Real-World Impact

The migration to Rust delivered transformative results:

"Since deploying the Rust analytics pipeline, we've eliminated all data loss incidents and can now provide real-time insights that were previously impossible."

— Sarah Chen, Head of Data Engineering

For Our Users

  • Instant Dashboards: Analytics load in under 100ms
  • Real-Time Updates: Live data streaming without delays
  • Complex Queries: Multi-dimensional analysis in seconds
  • Reliability: 99.99% uptime SLA

For Our Team

  • Reduced Oncall: 80% fewer production incidents
  • Predictable Costs: Fixed infrastructure spend
  • Developer Velocity: Faster feature development
  • Confidence: Compiler catches bugs before production

Challenges We Faced

The migration wasn't without difficulties:

1. Learning Curve

Challenge: Team unfamiliar with Rust's ownership model
Solution: Dedicated 2-week training, pair programming, internal Rust guild

2. Ecosystem Gaps

Challenge: Some Node.js libraries had no Rust equivalent
Solution: Built custom solutions, contributed to open source

3. FFI Integration

Challenge: Calling legacy C libraries from Rust
Solution: Used bindgen for automatic binding generation

4. Debugging Complexity

Challenge: Async stack traces harder to debug
Solution: Invested in observability (tracing, metrics, logs)

Best Practices We Learned

After six months in production, here are our recommendations:

Start Small

Don't rewrite everything at once. Identify critical bottlenecks and start there.

Embrace the Type System

Let Rust's compiler guide you. If it compiles, it likely works correctly.

Profile Before Optimizing

Use cargo flamegraph and perf to identify actual bottlenecks.

# Generate flamegraph
cargo flamegraph --bin analytics-server

# Profile with perf
perf record -g ./target/release/analytics-server
perf report

Leverage Cargo Features

Use feature flags for conditional compilation:

[features]
default = ["production"]
production = ["optimizations"]
development = ["debug-logging"]

[dependencies]
tracing = { version = "0.1", optional = true, features = ["debug-logging"] }

Write Comprehensive Tests

Rust's testing framework makes it easy:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_event_aggregation() {
        let events = generate_test_events(10000);
        let result = aggregate_events(&events);
        assert_eq!(result.len(), 100);
    }

    #[tokio::test]
    async fn test_async_processing() {
        let event = create_test_event();
        let result = process_event(event).await;
        assert!(result.is_ok());
    }
}

Tools & Libraries We Use

Our Rust stack includes these essential crates:

Web Framework

  • actix-web: High-performance HTTP server
  • tokio: Async runtime

Serialization

  • serde: Serialization framework
  • bincode: Binary encoding
  • serde_json: JSON support

Concurrency

  • rayon: Data parallelism
  • crossbeam: Lock-free structures
  • dashmap: Concurrent HashMap

Database

  • clickhouse: ClickHouse client
  • redis: Redis client
  • sqlx: Compile-time checked SQL

Observability

  • tracing: Structured logging
  • metrics: Metrics collection
  • opentelemetry: Distributed tracing

Common Pitfalls to Avoid

1. Over-Engineering

Don't prematurely optimize. Rust is already fast — write clear code first.

2. Ignoring Lifetimes

Understand lifetimes early. They're Rust's secret weapon for memory safety.

3. Blocking in Async

Never use std::thread::sleep in async code:

// ❌ Wrong
async fn bad_delay() {
    std::thread::sleep(Duration::from_secs(1));
}

// ✅ Correct
async fn good_delay() {
    tokio::time::sleep(Duration::from_secs(1)).await;
}

4. Excessive Cloning

Use references and borrowing instead of cloning:

// ❌ Inefficient
fn process_data(data: Vec<String>) {
    for item in data.clone() {
        println!("{}", item);
    }
}

// ✅ Efficient
fn process_data(data: &[String]) {
    for item in data {
        println!("{}", item);
    }
}

When to Use Rust

Rust excels in these scenarios:

Perfect For

✅ High-performance services
✅ Real-time data processing
✅ System-level programming
✅ Resource-constrained environments
✅ Mission-critical infrastructure

Consider Alternatives

⚠️ Rapid prototyping
⚠️ Small internal tools
⚠️ Team without Rust expertise
⚠️ Simple CRUD APIs
⚠️ Projects with tight deadlines

The Road Ahead

We're continuing to invest in Rust across our infrastructure:

Q4 2024: Migrate blockchain indexer to Rust
Q1 2025: Real-time recommendation engine
Q2 2025: WebAssembly for client-side analytics
Q3 2025: Custom query language compiler

Getting Started with Rust

Ready to explore Rust for your analytics needs?

Learn Rust

Try Our Stack

We've open-sourced parts of our analytics infrastructure:

Join the Community


Interested in working on high-performance Rust systems? We're hiring senior engineers to help scale our infrastructure. View open positions.

Questions? Email us at engineering@actora.com or join our Discord community to discuss Rust, performance optimization, and analytics architecture.

What's Next?

In our next engineering post, we'll explore Building Real-Time Dashboards with WebAssembly — how we're bringing near-native performance to the browser.

Stay tuned! 🦀

Actora Engineering Team

Written by

Actora Engineering Team

Ready to Get Started?

Join thousands of Web3 enthusiasts building the future of decentralized applications