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 ]
The Problems Encountered:
- 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.
- Memory Allocation Churn: Dynamically allocating dozens of temporary lists per request under 5,000 req/s triggered aggressive Python garbage collection cycles.
-
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);
}
}
}
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...
}
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>,
}
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:
- Nonces are inserted into
current_bucket. - When
window_duration(e.g., 60 seconds) expires,previous_bucketis replaced withcurrent_bucketviastd::mem::replace, and a new, cleancurrent_bucketis initialized. - Checking a nonce requires checking
current_bucket.contains()orprevious_bucket.contains(). - 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
}
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(())
}
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)
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_17andmusllinux_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
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
- 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.
-
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. -
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
- 📦 PyPI: synapse-shield | synapse-core-rs
- 💻 GitHub Repository: 0xStoic-bit/Synapse_Shield
- 📝 License: MIT
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)