Record app usage to drive on-device decisions in mobile and desktop apps.
Count events as they happen. Query aggregates later when you know what questions matter. Details fade automatically—only frequency and recency remain.
Answer questions about events per time:
- How many events in the previous N time units?
- How many time units had one or more events?
- When did an event last occur, or first occur?
For example, recording just app launches answers:
- Has the user used the app in 15 of the last 28 days?
- Has the app launched today? Was it yesterday?
- How often does the user visit settings?
- When did the user last launch the app?
- Has usage increased this week vs last week?
Drive app behavior:
- Show tooltip? (user saved 3 passwords this week → suggest Sync setup)
- Run recovery migration? (user saw error recently)
- Rate limit API? (spread usage across sessions for good citizenship)
Event data:
- Stays on device for privacy
- No exact timestamps stored—only counts per time period
- Details automatically fade as buckets rotate
- Uses 1KB per event (100 events = 100KB)
- Thread safe
Product decisions care about frequency and recency, not individual actions. Aggregate patterns matter; specific details fade over time.
use tiny_counter::EventStore;
let store = EventStore::new();
store.record("app_launch");
store.record("app_launch");
let launches = store.query("app_launch").last_days(7).sum().unwrap_or(0);
assert_eq!(launches, 2);Good for:
- On-device usage analytics
- Feature adoption metrics
- Rate limiting APIs and user actions
- System monitoring (error rates, throughput)
Not for:
- Audit logs (events fall off after tracking window)
- Exact timestamps (only counts per time bucket)
- Unbounded history (fixed-size storage)
[dependencies]
tiny-counter = "0.1"
# With optional features
tiny-counter = { version = "0.1", features = ["storage-sqlite", "serde-json", "tokio"] }
# For uniform 30-day months instead of calendar months
tiny-counter = { version = "0.1", default-features = false, features = ["storage-fs", "serde-bincode"] }Rotating buckets: Events drop into time buckets. Bucket 0 is "today" (since midnight) or "this hour" (since :00). Bucket 1 is "yesterday" or "last hour." Time advances → buckets rotate → old data falls off.
Multiple time units: One record() updates all time units (minutes, hours, days, months). Query any scale without reprocessing.
Fixed memory: Each event type uses ~1KB (default: 256 buckets × 4 bytes). 200 events = 200KB.
Tradeoff: Trade precision for memory. You get "10 events in last hour" but not exact timestamps. Events older than the tracking window drop silently.
Default config tracks 256 total buckets:
- 60 Minutes (last hour)
- 72 Hours (last 3 days)
- 56 Days (last ~8 weeks)
- 52 Weeks (last year)
- 12 Months (last year)
- 4 Years
Customize with builder:
use tiny_counter::EventStore;
let store = EventStore::builder()
.track_hours(24)
.track_days(28)
.track_weeks(26)
.track_years(2)
.build()
.unwrap();Configuration changes are handled automatically—change bucket counts anytime and existing data adapts on load.
// Record events
store.record("app_launch");
store.record_count("api_call", 5);
// Query time windows
let last_hour = store.query("api_call").last_hours(1).sum().unwrap_or(0);
let last_week = store.query("app_launch").last_days(7).sum().unwrap_or(0);
// Count active periods
let active_days = store.query("app_launch").last_days(28).count_nonzero().unwrap_or(0);
// When did event last occur?
if let Some(duration) = store.query("error:sync").last_seen() {
println!("Last error {} minutes ago", duration.num_minutes());
}use tiny_counter::{EventStore, storage::Sqlite};
let store = EventStore::builder()
.with_storage(Sqlite::open("events.db")?)
.build()?;
store.record("page_view");
store.persist()?; // Save to diskuse tiny_counter::TimeUnit;
// Enforce multiple limits
match store.limit()
.at_most("api_call", 10, TimeUnit::Minutes)
.at_most("api_call", 100, TimeUnit::Hours)
.check_and_record("api_call")
{
Ok(_) => println!("Request allowed"),
Err(e) => println!("Rate limited: {}", e),
}Prevent race conditions with atomic check-and-reserve:
let reservation = store.limit()
.at_most("payment", 1, TimeUnit::Minutes)
.reserve("payment")?;
match process_payment() {
Ok(_) => reservation.commit(), // Count it
Err(_) => reservation.cancel(), // Release slot
}// Conversion rate
let conversion = store.query_ratio("purchases", "visits").last_days(7);
// Net change (inventory, balance, connections)
let balance = store.query_delta("deposits", "withdrawals").ever().sum();// Export dirty counters from device 1
let device1_data = device1_store.export_dirty()?;
// Merge into device 2
device2_store.merge_all(device1_data)?;Storage backends:
storage-fs- File-per-event (default)storage-sqlite- SQLite database (all events in one DB)MemoryStorage- No persistence (testing)
Serialization formats:
serde-bincode- Compact binary (default)serde-json- Human-readable JSON
Time bucket behavior:
calendar(default) - Days/weeks/months rotate at local midnight- Disable with
default-features = falsefor uniform 30-day months and 24-hour days
Optional features:
tokio- Auto-persist with background task
Mix and match any storage with any format.
Constraint types:
at_most- Maximum N per window (typical rate limit)at_least- Minimum N required (prerequisite check)cooldown- Minimum time between eventswithin- Event must have occurred recentlyduring/outside_of- Schedule-based (business hours, weekdays)
Combine constraints:
use std::time::Duration;
use tiny_counter::Schedule;
store.limit()
.at_most("api", 100, TimeUnit::Hours)
.at_least("login", 1, TimeUnit::Days) // Must be logged in
.cooldown("reset", Duration::minutes(5)) // Wait between resets
.during(Schedule::hours(9, 17)) // Business hours only
.check_and_record("api")?;- REFERENCE.md - Complete API guide with progressive disclosure
- COOKBOOK.md - Advanced patterns combining features
- DESIGN.md - Architecture for contributors
- Examples - Runnable code:
- Core:
basic,type_safety,persistence - Use cases:
rate_limiting,analytics,multi_device,resource_tracking,security,concurrent - Features:
async_autopersist(tokio)
- Core:
MIT OR Apache-2.0