stream-velocity
stream-velocity is a small, dependency-free Python library that answers one question in real time: "how many events happened in the last few minutes, and how much did they total?" — per account, card, or any other entity.
It exists because small fraud and risk teams often need this exact check but can't justify a Redis cluster or a $10k/month streaming platform. stream-velocity gives them a fast, in-process alternative that uses a fixed amount of memory no matter how much data flows through it.
Installation
pip install -e .
A public PyPI release is in progress. Until then, install from source as shown above.
Quick Start
from stream_velocity import VelocityTracker
# Track a 5-minute window, split into 5-second buckets
tracker = VelocityTracker(window_seconds=300, bucket_seconds=5)
# Record incoming events for an account
tracker.record_event(entity_id="acc_990142", amount=150.00)
tracker.record_event(entity_id="acc_990142", amount=45.50)
tracker.record_event(entity_id="acc_990142", amount=210.00)
# Read the current rolling totals
velocity = tracker.get_velocity("acc_990142")
print(velocity.count) # 3
print(velocity.sum_amount) # 405.5
API Reference
VelocityTracker
The main entry point. Tracks rolling velocity across many entities.
VelocityTracker(
window_seconds=300,
bucket_seconds=5,
idle_timeout=None,
cleanup_interval=60.0,
)
| Parameter | Type | Default | Description |
|---|---|---|---|
window_seconds |
int |
300 |
Length of the rolling window, in seconds. |
bucket_seconds |
int |
5 |
Width of each bucket, in seconds. window_seconds must be evenly divisible by bucket_seconds. |
idle_timeout |
float or None |
None |
Drop an entity if it records no events for this many seconds. None disables eviction. Should be at least window_seconds. |
cleanup_interval |
float |
60.0 |
How often (seconds) the idle sweep runs automatically. |
Methods
record_event(entity_id, amount=0.0, timestamp=None)→None— record one event.timestampdefaults to the current time; pass an explicit value for replay or testing.
get_velocity(entity_id, timestamp=None)→VelocityResult— current rolling totals for an entity. Returns zeros if the entity is unknown or has been evicted.cleanup(now=None)→None— immediately drop entities idle longer thanidle_timeout. The sweep also runs automatically everycleanup_intervalseconds.len(tracker)→int— number of entities currently tracked.
VelocityResult
A frozen (immutable) dataclass returned by get_velocity.
| Field | Type | Description |
|---|---|---|
entity_id |
str |
The entity the result is for. |
window_seconds |
int |
The window length used. |
count |
int |
Number of events in the window. |
sum_amount |
float |
Sum of amount values in the window. |
BucketedRingBuffer (lower-level)
The underlying single-entity counter. Prefer VelocityTracker unless you need to manage one entity manually.
buffer = BucketedRingBuffer(window_seconds=300, bucket_seconds=5)
buffer.record(timestamp=1_000_000.0, amount=10.0)
metrics = buffer.get_metrics(current_timestamp=1_000_010.0) # {"window_seconds": 300, "count": 1, "sum_amount": 10.0}
Features
- No dependencies — pure Python 3.9+, no database, no C extensions.
- Always fast — updates and lookups take the same time whether you process 10 or 10 million events (this is what "O(1)" means).
- Fixed memory — counters are created once and reused forever, so memory use stays flat no matter how many events arrive.
- Bounded tracking — set
idle_timeoutto drop idle entities automatically, so millions of distinct accounts don't grow memory forever. - Low latency — reads stay under 5 ms while processing 100,000+ events.
Limitations
- Single process — the tracker lives in memory and is not shared across processes or machines. Run one instance per worker, or pair it with an external store for distributed setups.
- Not persistent — state is lost on restart; there is no disk or external storage.
- Not thread-safe — use one tracker per thread or worker, or add external locking if you share a tracker across threads.
How It Works
A rolling window is split into a fixed number of buckets. For a 5-minute window with 5-second buckets, that's 60 buckets.
- Finding the bucket — an event's timestamp maps to a bucket with one division:
slot = (timestamp // bucket_seconds) % num_buckets. - Reusing buckets — as time passes, expired buckets are wiped clean and reused, so the total number of buckets never changes.
- Running totals — two totals (count and amount) are kept up to date on every write, so reading the answer means just returning the totals — no scanning.
events ──► slot = (timestamp // 5) % 60
│
▼
┌───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │...│ 57│ 58│ 59│ ◄── fixed ring of buckets, reused forever
└───┴───┴───┴───┴───┴───┴───┘
Only counts and sums are stored — no raw transaction details.
Benchmarks
Latency
python3 tests/benchmark_latency.py
Representative output (Apple M-series MacBook Air, Python 3.10). Absolute timings vary by machine; the < 5.0 ms SLA is enforced in CI on every push.
--- Running stream-velocity benchmark: 100,000 events ---
Total write time for 100,000 events: 61.88 ms
Average write latency per event: 0.62 µs
Read query latency: 0.0418 ms
Aggregation result: 100000 events, $1,000,000.00
SLA Target Passed: Query execution < 5.0 ms
Memory
python3 tests/benchmark_memory.py
The library creates its counters once and reuses them, so memory stays flat as events pile up. A typical 5-minute window uses only a few kilobytes — far below the 100 MB ceiling a lightweight fraud service can run within.
Docker
A minimal Alpine image installs the library and runs the speed benchmark:
docker build -t stream-velocity .
docker run --rm stream-velocity
Development
Run the test suite and benchmarks:
python3 -m unittest discover -s tests
python3 tests/benchmark_latency.py
python3 tests/benchmark_memory.py
License
MIT. See LICENSE.
Metadata
Release files for stream-velocity 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| stream_velocity-0.1.0.tar.gz | 9.0 kB | Without provenance | Details for stream_velocity-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| stream_velocity-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for stream_velocity-0.1.0-py3-none-any.whl |
Total release size: 16.8 kB
Release files / stream_velocity-0.1.0.tar.gz
| Download URL | stream_velocity-0.1.0.tar.gz |
|---|---|
| Size | 9.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fcd253b360c0a0e7b3e0da476514dc057405413e3832ca2f5711dbe8b57f5a51
|
|
BLAKE2b-256 checksum How to use checksums |
545b82b9e91e123f9ab113e4ba0d8e93f9f0732090cbce607c5120d9b2a66d9d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.4
|
Release files / stream_velocity-0.1.0-py3-none-any.whl
| Download URL | stream_velocity-0.1.0-py3-none-any.whl |
|---|---|
| Size | 7.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f3c3bff5510d5466b80573e034543ffececf0bf128c9915882a92215510be150
|
|
BLAKE2b-256 checksum How to use checksums |
b84cf6e156550cf239ac083cc2f6a496a30d7675eb1c87931d5649e348dcf6e7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.4
|
Security for stream-velocity 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.