DEV Community

Cover image for How I Rewrote Synapse Shield in Rust: Achieving Sub-Millisecond Kinematic Biometrics & 15 Multi-Platform Native Wheels with Maturin
Mustafa Güngör
Mustafa Güngör

Posted on Originally published at github.com

How I Rewrote Synapse Shield in Rust: Achieving Sub-Millisecond Kinematic Biometrics & 15 Multi-Platform Native Wheels with Maturin

When building an in-process Web Application Firewall (WAF) and bot mitigation system, your margin for latency error is practically zero.

Traditional CAPTCHAs degrade user experience, and cloud-based WAFs (like Cloudflare or AWS WAF) often inject 100ms to 250ms of network round-trip overhead. With Synapse Shield, my objective was clear: zero user friction (no puzzles, no clicks) and zero external network hops.

Everything had to happen inside the application process within microseconds.

In earlier versions (v0.6.x – v0.7.x), the core engine was written entirely in Python. While Python excels at rapid prototyping, orchestrating async web requests, and serving ML inference, it hit a brick wall when handling high-throughput kinematic feature extraction and concurrent state verification under botnet load.

Here is the technical deep-dive into how I re-engineered Synapse Shield’s core into Rust (synapse-core-rs), dropped feature extraction latency from 123 µs to 22 µs, replaced SQLite disk bottlenecks with an atomic two-bucket in-memory nonce cache, and automated the cross-compilation of 15 multi-platform native wheels using Maturin and GitHub Actions.


1. The Bottlenecks: Why Pure Python Reached Its Limit

To distinguish synthetic mouse and keyboard movements (generated by Puppeteer, Playwright, or Selenium) from biological humans, Synapse Shield extracts 24 kinematic and biometric features over time-series telemetry:

  • Third-order derivatives (Jerk: $d^3x/dt^3$): Detecting neuromuscular micro-tremors (8–12 Hz) that algorithmic bots either omit or fake with naive Gaussian jitter.
  • Fitts’ Law Terminal Deceleration: Biological motor control naturally decelerates in the final 25% of a ballistic trajectory as visual feedback guides the cursor to the target.
  • Euclidean Straightness & Curvature: Ratio of displacement to path length ($\frac{D}{\sum \Delta s}$).
  • Power Spectral Density & Spectral Entropy: Discrete Fast Fourier Transforms (RFFT) on velocity vectors.
       [ Client Telemetry (x, y, t) ]
                      │
                      ▼
   ┌─────────────────────────────────────┐
   │        Kinematic Differentiation    │
   │  Velocity (dx/dt) -> Accel -> Jerk  │
   ├─────────────────────────────────────┤
   │     Spectral Analysis (RFFT/PSD)    │
   ├─────────────────────────────────────┤
   │   Two-Bucket Nonce Replay Check     │
   └─────────────────────────────────────┘
                      │
                      ▼
             [ Bot / Human Score ]
Enter fullscreen mode Exit fullscreen mode

The Problems Encountered:

  1. GIL and Loop Overhead: Iterating over 150–300 trajectory points, calculating trigonometric arc distances, and running continuous numerical differentiations in Python loops incurred significant interpreter overhead.
  2. Memory Allocation Churn: Dynamically allocating dozens of temporary lists per request under 5,000 req/s triggered aggressive Python garbage collection cycles.
  3. SQLite Disk Locks under DDoS: To prevent replay attacks, nonces were originally written to an atomic SQLite table with WAL mode. Under concurrent spikes, SQLite file lock contention (sqlite3.OperationalError: database is locked) degraded throughput and increased p99 latency to over 15ms.

The solution was obvious: Extract the CPU-intensive numerical math and the high-concurrency state management into a compiled, memory-safe Rust extension.


2. Re-engineering Kinematics in Rust

In Rust (crates/synapse_core_rs/src/kinematics.rs), we process raw telemetry without heap allocations wherever possible, passing continuous slices and pre-allocating vectors with Vec::with_capacity.

Calculating Neuromuscular Jerk ($d^3x/dt^3$)

Synthetic curves (like Bézier or linear interpolation) look smooth to the naked eye, but in the 3rd derivative, their jerk profile collapses to zero or produces unnatural step functions.

Here is how we calculate discrete accelerations and absolute jerk:

// Accelerations & Jerk extraction in Rust
if velocities.len() >= 2 {
    let mut accelerations = Vec::with_capacity(velocities.len() - 1);
    for i in 1..velocities.len() {
        accelerations.push((velocities[i] - velocities[i - 1]) / dts[i]);
    }

    let acc_count = accelerations.len() as f64;
    let avg_acc: f64 = accelerations.iter().sum::<f64>() / acc_count;
    feat.avg_acceleration = avg_acc;
    feat.acceleration_var = accelerations.iter()
        .map(|&a| (a - avg_acc).powi(2))
        .sum::<f64>() / acc_count;

    if accelerations.len() >= 2 {
        let mut jerks = Vec::with_capacity(accelerations.len() - 1);
        for i in 1..accelerations.len() {
            jerks.push((accelerations[i] - accelerations[i - 1]) / dts[i + 1]);
        }

        if !jerks.is_empty() {
            let jerk_sum: f64 = jerks.iter().map(|&j| j.abs()).sum();
            feat.avg_jerk = jerk_sum / (jerks.len() as f64);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Discrete Spectral Analysis (RFFT) Without Heavy External BLAS

Rather than pulling in massive C-libraries (like FFTW) which complicate cross-compilation, we implemented a lightweight Discrete Real Fourier Transform optimized for small sequences ($N \le 300$ points):

pub fn compute_spectral_features(velocities: &[f64], avg_vel: f64, feat: &mut ExtractedFeatures) {
    let n = velocities.len();
    let num_freqs = n / 2 + 1;
    let mut psd = Vec::with_capacity(num_freqs);
    let v_centered: Vec<f64> = velocities.iter().map(|&v| v - avg_vel).collect();

    for k in 0..num_freqs {
        let mut re = 0.0;
        let mut im = 0.0;
        let angle_factor = 2.0 * std::f64::consts::PI * (k as f64) / (n as f64);
        for (t, &val) in v_centered.iter().enumerate() {
            let angle = angle_factor * (t as f64);
            re += val * angle.cos();
            im -= val * angle.sin();
        }
        let power = (re * re + im * im) / (n as f64);
        psd.push(power);
    }
    // Compute Spectral Purity & Spectral Entropy...
}
Enter fullscreen mode Exit fullscreen mode

3. Sub-Microsecond Nonce Cache: The Two-Bucket Architecture

To prevent token replay attacks without touching the disk or invoking SQLite locks, we engineered a Two-Bucket In-Memory State Engine wrapped in an RwLock:

pub struct StateEngine {
    current_bucket: HashSet<String>,
    previous_bucket: HashSet<String>,
    last_rotation: Instant,
    window_duration: Duration,
    ip_bans: HashMap<String, u64>,
}
Enter fullscreen mode Exit fullscreen mode

Why Two Buckets?

If you store nonces in a single hash set with individual TTLs, you must either run a background sweep thread (which causes locking pauses) or store timestamps with every entry (increasing memory overhead).

With Two Buckets:

  1. Nonces are inserted into current_bucket.
  2. When window_duration (e.g., 60 seconds) expires, previous_bucket is replaced with current_bucket via std::mem::replace, and a new, clean current_bucket is initialized.
  3. Checking a nonce requires checking current_bucket.contains() or previous_bucket.contains().
  4. Time Complexity: $O(1)$ lookups and $O(1)$ rotation, zero disk I/O, sub-microsecond latency ($< 0.8\ \mu s$).
pub fn consume_nonce(&mut self, nonce: &str) -> bool {
    self.maybe_rotate();

    // Check if seen in either active window
    if self.current_bucket.contains(nonce) || self.previous_bucket.contains(nonce) {
        return false; // Replay attack detected!
    }

    self.current_bucket.insert(nonce.to_string());
    true
}
Enter fullscreen mode Exit fullscreen mode

4. Bridging Rust to Python via PyO3

Using PyO3, exposing our Rust engine to Python required zero boilerplate.

use pyo3::prelude::*;
use pyo3::types::{PyDict, PyAny};

#[pyfunction]
fn extract_features_rs<'py>(py: Python<'py>, telemetry: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyDict>> {
    let raw = parse_python_telemetry(telemetry);
    let feat = compute_kinematics(&raw);
    features_to_pydict(py, &feat)
}

#[pymodule]
fn synapse_core_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(is_rust_core_active, m)?)?;
    m.add_function(wrap_pyfunction!(extract_features_rs, m)?)?;
    m.add_function(wrap_pyfunction!(consume_nonce_rs, m)?)?;
    m.add_function(wrap_pyfunction!(is_ip_banned_rs, m)?)?;
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Seamless Python Fallback

In Python, we gracefully detect whether the native binary is present. If someone installs on an unsupported embedded architecture, it smoothly falls back to pure Python:

try:
    import synapse_core_rs as _core
    RUST_CORE_AVAILABLE = True
except ImportError:
    _core = None
    RUST_CORE_AVAILABLE = False

def extract_features(telemetry: dict) -> dict:
    if RUST_CORE_AVAILABLE:
        return _core.extract_features_rs(telemetry)
    return _python_fallback_extract_features(telemetry)
Enter fullscreen mode Exit fullscreen mode

5. Shipping 15 Multi-Platform Native Wheels with Maturin

The biggest hurdle with compiled C/Rust extensions in Python is user distribution. If a user does pip install your-package and their machine triggers cargo build without a Rust toolchain installed, the installation fails.

We solved this by using Maturin paired with GitHub Actions matrix builds to generate pre-compiled binary wheels for:

  • Linux: manylinux_2_17 and musllinux_1_1 (x86_64, aarch64) across CPython 3.9, 3.10, 3.11, 3.12, and 3.13.
  • macOS: universal2 (supporting both Apple Silicon M-series and Intel x86_64).
  • Windows: MSVC x86_64.

The GitHub Actions Workflow Snippet:

name: Build Native Wheels
on:
  push:
    tags: ['v*']

jobs:
  build_wheels:
    name: Wheel on ${{ matrix.os }} (${{ matrix.target }})
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
          - os: ubuntu-latest
            target: aarch64-unknown-linux-gnu
          - os: macos-latest
            target: universal2-apple-darwin
          - os: windows-latest
            target: x86_64-pc-windows-msvc

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - name: Build Wheels with Maturin
        uses: PyO3/maturin-action@v1
        with:
          target: ${{ matrix.target }}
          args: --release --out dist -m crates/synapse_core_rs/Cargo.toml
          manylinux: auto
Enter fullscreen mode Exit fullscreen mode

When users run pip install synapse-shield, pip automatically pulls the pre-built .whl for their exact OS and Python ABI. Zero C++ compilers, zero Rust toolchains required on the client machine.


6. Benchmarks: 10,000-Request Stress Test

To validate the architecture, we ran an adversarial benchmark simulating a high-rate botnet assault (10,000 continuous requests across 50 worker threads).

Metric Pure Python (v0.7.x) Rust Core (synapse-core-rs v0.8.2) Improvement
Feature Extraction Latency 123.4 µs 21.8 µs ~5.6x faster (82% reduction)
Replay Nonce Verification 1,240.0 µs (SQLite) 0.7 µs (Two-Bucket) ~1,700x faster
Max Throughput (RPS) 2,450 req/s 11,200 req/s 4.5x higher capacity
P99 Defense Latency 18.2 ms 0.84 ms Sub-millisecond guaranteed
Dropped / Locked Requests 0.42% (SQLite lock) 0.00% 100% Reliability

Under saturated load, the pure Python version choked on SQLite locks and GIL context switching. The Rust native engine kept CPU utilization flat and processed all 10,000 requests without a single dropped packet.


Key Lessons for Systems Engineers

  1. Don't rewrite everything—isolate the hot paths: Python is fantastic for FastAPI middleware, configuration parsing, and routing. Rust is unmatched for high-frequency differentiation, cryptographic hashing, and atomic concurrency. PyO3 gives you the best of both worlds.
  2. Disk I/O has no place in microsecond security pipelines: Moving replay attack prevention from SQLite into an in-memory double-buffering scheme (Two-Bucket) provided the single largest latency win.
  3. Maturin makes shipping Rust wheels delightful: Building cross-platform binary wheels used to require terrifying Docker setups. With maturin-action, distributing native extensions to PyPI is now as straightforward as publishing pure Python packages.

Links & Source Code

If you are working on bot mitigation, biometric signal processing, or writing high-performance Python extensions in Rust, feel free to star the repo or leave your thoughts below!

Top comments (0)