# Cookbook
Advanced recipes combining multiple features. Use these patterns to solve real problems in your app.
## API Rate Limiting
### Burst Protection with Sustained Limits
Allow short bursts but enforce sustained usage limits:
```rust
use tiny_counter::{EventStore, TimeUnit};
let store = EventStore::new();
// Allow 10/min bursts, but only 100/hour sustained
let result = store
.limit()
.at_most("api_call", 10, TimeUnit::Minutes)
.at_most("api_call", 100, TimeUnit::Hours)
.check_and_record("api_call");
match result {
Ok(()) => make_api_call(),
Err(e) => {
let wait = e.retry_after.unwrap();
return_error!("Rate limited. Retry in {}s", wait.num_seconds());
}
}
```
This prevents both burst abuse (10 calls in one second) and sustained overuse (1000 calls over an hour by spacing them 6 seconds apart).
### Tiered Access by User Activity
Increase limits for active users:
```rust
// Free tier: 10/hour
// Pro tier requires 100 logins this month
let user_tier = if store.query("user:login").last_months(1).sum().unwrap_or(0) >= 100 {
100 // Pro user gets 100/hour
} else {
10 // Free tier gets 10/hour
};
store
.limit()
.at_most("user:api_call", user_tier, TimeUnit::Hours)
.check_and_record("user:api_call")?;
```
### Progressive Backoff
Increase cooldown based on recent failures:
```rust
let recent_failures = store.query("login:failed").last_minutes(15).sum().unwrap_or(0);
let cooldown = match recent_failures {
0..=2 => Duration::seconds(0), // No cooldown
3..=5 => Duration::seconds(30), // 30s after 3 failures
6..=10 => Duration::minutes(5), // 5m after 6 failures
_ => Duration::minutes(30), // 30m after 10+ failures
};
if cooldown > Duration::zero() {
store
.limit()
.cooldown("login:attempt", cooldown)
.check_and_record("login:attempt")?;
}
```
### Transactional API Calls
Reserve slots before making expensive calls:
```rust
// Reserve slot before work
let reservation = store
.limit()
.at_most("api_call", 10, TimeUnit::Minutes)
.reserve("api_call")?;
// Make API call
match expensive_api_call().await {
Ok(result) => {
reservation.commit(); // Count this call
Ok(result)
}
Err(e) => {
// Auto-cancels on drop if not committed
Err(e)
}
}
```
Prevents counting failed or cancelled requests.
## User Analytics
### Daily Active Users (DAU)
Count unique active days:
```rust
let dau = store.query("user:login").last_days(1).count_nonzero().unwrap_or(0);
let wau = store.query("user:login").last_days(7).count_nonzero().unwrap_or(0);
let mau = store.query("user:login").last_days(30).count_nonzero().unwrap_or(0);
println!("DAU: {}, WAU: {}, MAU: {}", dau, wau, mau);
```
Note: This counts "days with activity" not "unique users". For per-user tracking, use separate event IDs:
```rust
// Per-user tracking
store.record(&format!("user:{}:login", user_id));
// Query per user
let user_dau = store
.query(&format!("user:{}:login", user_id))
.last_days(1)
.count_nonzero()
.unwrap_or(0);
```
### Engagement Scoring
Score users by engagement patterns:
```rust
let app_launches = store.query(&format!("user:{}:launch", user_id))
.last_days(30).sum().unwrap_or(0);
let feature_uses = store.query(&format!("user:{}:feature_use", user_id))
.last_days(30).sum().unwrap_or(0);
let active_days = store.query(&format!("user:{}:launch", user_id))
.last_days(30).count_nonzero().unwrap_or(0);
let score = (app_launches * 1) + (feature_uses * 5) + (active_days * 10);
let segment = match score {
0..=50 => "dormant",
51..=200 => "casual",
201..=500 => "regular",
_ => "power_user",
};
```
### Conversion Funnels
Track multi-step conversions:
```rust
// Record funnel steps
store.record(&format!("user:{}:landing", user_id));
store.record(&format!("user:{}:signup_started", user_id));
store.record(&format!("user:{}:signup_completed", user_id));
// Calculate conversion rates
let landings = store.query(&format!("user:{}:landing", user_id))
.last_days(7).sum().unwrap_or(1);
let signups = store.query(&format!("user:{}:signup_completed", user_id))
.last_days(7).sum().unwrap_or(0);
let conversion_rate = signups as f64 / landings as f64;
println!("Conversion: {:.2}%", conversion_rate * 100.0);
```
### Cohort Retention
Track user retention by signup cohort:
```rust
// Day 1: User signs up
store.record(&format!("cohort:2025-01:signup", user_id));
// Day 7: User returns
if store.query(&format!("cohort:2025-01:signup", user_id)).ever().sum().unwrap_or(0) > 0 {
store.record(&format!("cohort:2025-01:day7", user_id));
}
// Calculate retention
let signups = store.query("cohort:2025-01:signup").ever().count_nonzero().unwrap_or(1);
let day7_returns = store.query("cohort:2025-01:day7").ever().count_nonzero().unwrap_or(0);
let retention = day7_returns as f64 / signups as f64;
println!("Day 7 retention: {:.2}%", retention * 100.0);
```
## Multi-Device Sync
### Offline-First Mobile App
Record events offline, sync when connected:
```rust
// Offline recording
store.record("user:purchase");
store.record("user:page_view");
// Check for pending changes
if store.is_dirty() && is_online() {
// Upload local changes
let local_data = store.export_dirty()?;
api.upload_events(user_id, local_data).await?;
// Download remote changes
let remote_data = api.fetch_events(user_id).await?;
store.merge_all(remote_data)?;
// Clear dirty flag after successful sync
store.reset_dirty();
}
```
### Conflict-Free Sync
Merge works with concurrent updates across devices:
```rust
// Device 1 records 5 events
device1.record_count("page_view", 5);
let dev1_data = device1.export_all()?;
// Device 2 records 3 events
device2.record_count("page_view", 3);
let dev2_data = device2.export_all()?;
// Server merges both (5 + 3 = 8)
server.merge_all(dev1_data)?;
server.merge_all(dev2_data)?;
assert_eq!(server.query("page_view").ever().sum(), Some(8));
// Both devices sync and see total
device1.merge_all(server.export_all()?)?;
device2.merge_all(server.export_all()?)?;
assert_eq!(device1.query("page_view").ever().sum(), Some(8));
assert_eq!(device2.query("page_view").ever().sum(), Some(8));
```
### Partial Sync
Sync only specific events:
```rust
// Export only analytics events
let analytics_events = store.export_all()?
.into_iter()
.filter(|(id, _)| id.starts_with("analytics:"))
.collect();
api.upload_analytics(analytics_events).await?;
```
## Resource Management
### Connection Pool Tracking
Track open/closed connections:
```rust
// Connection opened
store.record("db:connection:open");
// Connection closed
store.record("db:connection:closed");
// Current open connections
let open = store.query("db:connection:open").ever().sum().unwrap_or(0);
let closed = store.query("db:connection:closed").ever().sum().unwrap_or(0);
let current = (open as i64 - closed as i64).max(0);
println!("Open connections: {}", current);
// Or use delta query
let current = store
.query_delta("db:connection:open", "db:connection:closed")
.ever()
.sum();
println!("Open connections: {}", current.max(0));
```
### Capacity Limits
Enforce capacity based on current state:
```rust
let open = store.query_delta("db:open", "db:closed").ever().sum();
if open >= 100 {
return Err("Connection pool exhausted");
}
store.record("db:open");
// ... use connection ...
store.record("db:closed");
```
### Auto-Reset at Midnight
Balance connections at day boundary:
```rust
use chrono::Utc;
// Check if last reset was yesterday
let last_reset = load_last_reset_time();
let now = Utc::now();
if now.date() != last_reset.date() {
// New day - reset connection counts
store.balance_delta("db:open", "db:closed")?;
save_last_reset_time(now);
}
```
## Feature Flags
### Progressive Rollout
Enable features for active users first:
```rust
let launches = store
.query(&format!("user:{}:launch", user_id))
.last_days(30)
.sum()
.unwrap_or(0);
let new_feature_enabled = launches >= 10; // Active users only
if new_feature_enabled {
store.record(&format!("user:{}:new_feature_used", user_id));
}
```
### A/B Testing
Track feature variants and measure outcomes:
```rust
// Assign user to variant
let variant = match user_id % 2 {
0 => "control",
_ => "treatment",
};
store.record(&format!("ab_test:variant:{}", variant));
// Track conversions per variant
store.record(&format!("ab_test:{}:conversion", variant));
// Calculate results
let control_conversions = store
.query("ab_test:control:conversion")
.last_days(7)
.sum()
.unwrap_or(0);
let treatment_conversions = store
.query("ab_test:treatment:conversion")
.last_days(7)
.sum()
.unwrap_or(0);
let control_users = store
.query("ab_test:variant:control")
.last_days(7)
.sum()
.unwrap_or(1);
let treatment_users = store
.query("ab_test:variant:treatment")
.last_days(7)
.sum()
.unwrap_or(1);
let control_rate = control_conversions as f64 / control_users as f64;
let treatment_rate = treatment_conversions as f64 / treatment_users as f64;
let lift = ((treatment_rate - control_rate) / control_rate) * 100.0;
println!("Control: {:.2}%, Treatment: {:.2}%, Lift: {:.2}%",
control_rate * 100.0, treatment_rate * 100.0, lift);
```
## Security Patterns
### Brute Force Protection
Exponential backoff with time windows:
```rust
let attempts = store.query("login:failed").last_hours(1).sum().unwrap_or(0);
store
.limit()
.at_most("login:attempt", 5, TimeUnit::Minutes)
.at_most("login:attempt", 20, TimeUnit::Hours)
.cooldown("login:failed", Duration::seconds(2u64.pow(attempts.min(10) as u32)))
.check_and_record("login:attempt")?;
match authenticate(username, password) {
Ok(user) => {
store.record("login:success");
Ok(user)
}
Err(e) => {
store.record("login:failed");
Err(e)
}
}
```
### Anomaly Detection
Detect unusual activity patterns:
```rust
// Baseline: average activity over 30 days
let baseline = store
.query("user:api_calls")
.last_days(30)
.average_nonzero()
.unwrap_or(10.0);
// Current: activity in last hour
let current = store
.query("user:api_calls")
.last_hours(1)
.sum()
.unwrap_or(0) as f64;
// Alert if current > 5x baseline
if current > baseline * 5.0 {
store.record("security:anomaly_detected");
send_alert("Unusual activity detected");
}
```
### Session Management
Track concurrent sessions with auto-cleanup:
```rust
// Session start
store.record(&format!("user:{}:session:start", user_id));
// Active sessions (started in last 24 hours)
let active_sessions = store
.query(&format!("user:{}:session:start", user_id))
.last_hours(24)
.sum()
.unwrap_or(0);
// Limit concurrent sessions
if active_sessions >= 3 {
return Err("Too many active sessions. Close other sessions.");
}
```
## Testing Patterns
### Time-Based Test Scenarios
Test time-dependent behavior deterministically:
```rust
use tiny_counter::TestClock;
#[test]
fn test_weekly_limit() {
let clock = TestClock::build_for_testing();
let store = EventStore::builder()
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
// Day 1: Use 5 calls
store.record_count("api", 5);
assert!(store.limit().at_most("api", 10, TimeUnit::Weeks).allowed("api"));
// Day 7: Use 4 more calls (total 9)
clock.advance(Duration::days(6));
store.record_count("api", 4);
assert!(store.limit().at_most("api", 10, TimeUnit::Weeks).allowed("api"));
// Try 11th call - should fail
assert!(!store.limit().at_most("api", 10, TimeUnit::Weeks).allowed("api"));
// Day 8: First call drops off, have room again
clock.advance(Duration::days(1));
assert!(store.limit().at_most("api", 10, TimeUnit::Weeks).allowed("api"));
}
```
### Concurrent Access Testing
Test thread safety:
```rust
use std::sync::Arc;
use std::thread;
#[test]
fn test_concurrent_rate_limiting() {
let store = Arc::new(EventStore::new());
// Spawn 100 threads trying to reserve 10 slots
let handles: Vec<_> = (0..100)
.map(|_| {
let store = store.clone();
thread::spawn(move || {
store
.limit()
.at_most("api", 10, TimeUnit::Hours)
.reserve("api")
})
})
.collect();
let results: Vec<_> = handles
.into_iter()
.map(|h| h.join().unwrap())
.collect();
// Exactly 10 should succeed
let successes = results.iter().filter(|r| r.is_ok()).count();
assert_eq!(successes, 10);
}
```
## Performance Optimization
### Batch Recording
Reduce lock contention by batching:
```rust
// Instead of N separate records
for event in events {
store.record(&event.name); // N locks
}
// Batch count the events
let mut counts: HashMap<String, u64> = HashMap::new();
for event in events {
*counts.entry(event.name.clone()).or_insert(0) += 1;
}
for (event_name, count) in counts {
store.record_count(&event_name, count); // M locks (M < N)
}
```
### Selective Persistence
Persist only high-value events:
```rust
// Record everything in memory
store.record("low_value_event");
store.record("critical_event");
// Persist only critical events
if event_is_critical {
store.persist()?;
}
// Or persist on schedule
if last_persist.elapsed() > Duration::minutes(5) {
store.persist()?;
}
```
### Memory-Constrained Environments
Configure minimal tracking:
```rust
let store = EventStore::builder()
.track_minutes(60) // Only track 1 hour
.track_hours(24) // Only track 1 day
.build()?;
// Each event uses ~1KB instead of ~2KB
```
## Custom Patterns
### Sliding Window Average
Calculate rolling averages:
```rust
// Last 7 days activity
let last_week = store.query("api_call").last_days(7).sum().unwrap_or(0);
let avg_per_day = last_week as f64 / 7.0;
// Compare to previous 7 days
let prev_week = store.query("api_call").days(7..14).sum().unwrap_or(0);
let prev_avg = prev_week as f64 / 7.0;
let growth = ((avg_per_day - prev_avg) / prev_avg) * 100.0;
println!("Week-over-week growth: {:.1}%", growth);
```
### Threshold Alerts
Trigger actions when thresholds crossed:
```rust
let current_hour = store.query("errors").last_hours(1).sum().unwrap_or(0);
if current_hour > 100 {
store.record("alert:error_threshold_exceeded");
send_alert("Error rate exceeded 100/hour");
}
// Don't spam alerts
let recent_alerts = store.query("alert:error_threshold_exceeded")
.last_hours(1)
.sum()
.unwrap_or(0);
if recent_alerts == 0 {
send_alert("Error spike detected");
}
```
### Event Correlation
Find patterns across events:
```rust
// Track event sequences
let checkouts = store.query("checkout").last_days(7).sum().unwrap_or(0);
let cart_updates = store.query("cart_update").last_days(7).sum().unwrap_or(0);
let abandoned = cart_updates.saturating_sub(checkouts);
println!("Cart abandonment: {} of {} carts", abandoned, cart_updates);
// Conversion window analysis
let recent_carts = store.query("cart_update").last_hours(1).sum().unwrap_or(0);
let recent_checkouts = store.query("checkout").last_hours(1).sum().unwrap_or(0);
if recent_carts > 0 && recent_checkouts == 0 {
store.record("pattern:high_cart_low_checkout");
}
```
## Uniform Buckets for Backend Analytics
Disable calendar alignment for consistent bucket sizes in statistical analysis. **Do not use for user-facing queries.**
### When to Use Uniform Buckets
Use uniform buckets when you need:
- Consistent 30-day periods for comparing months
- Statistical analysis across uniform time windows
- Backend metrics where human time alignment doesn't matter
```toml
[dependencies]
tiny-counter = { version = "0.1", default-features = false, features = ["storage-fs", "serde-bincode"] }
```
### Why Not for User-Facing Queries
Uniform buckets have sharp edges:
**Weeks start on Jan 1st**, not Monday:
- If Jan 1st 2025 is Wednesday, Week 1 = Wed-Tue, Week 2 = Wed-Tue
- Users expect Monday-Sunday weeks
**Months are 30-day periods**, not calendar months:
- Month 1 = Jan 1-30, Month 2 = Jan 31 - Mar 1
- "Last month" means nothing to users
**Days use UTC midnight**, not local time:
```rust
// User in PST (UTC-8) queries "today" at 3pm local time
let today = store.query("app_launch").last_days(1).sum();
// Gets: midnight UTC yesterday to midnight UTC today
// Which is: 4pm yesterday (local) to 4pm today (local)
// Half yesterday, half today - wrong!
```
### Example: Statistical Analysis
Uniform buckets work well for backend metrics:
```rust
// Compare consistent 30-day windows
let this_period = store.query("api_calls").last_months(1).sum().unwrap_or(0);
let last_period = store.query("api_calls").months(1..2).sum().unwrap_or(0);
let growth = ((this_period as f64 - last_period as f64) / last_period as f64) * 100.0;
println!("30-day growth: {:.1}%", growth);
// Each "month" is exactly 30 days - consistent for comparison
```
Use calendar alignment (default) for user-facing queries like "today", "this week", "this month".
## Configuration Migration
Change tracking configuration after deployment. The store converts existing data automatically when loaded from storage.
### Adding More Buckets
Add more buckets to see finer detail in recent history:
```rust
// Old configuration: 30 minutes
let old_store = EventStore::builder()
.track_minutes(30)
.with_storage(storage)
.build()?;
// Record events...
old_store.record("api_call");
old_store.persist()?;
// New configuration: 60 minutes
let new_store = EventStore::builder()
.track_minutes(60)
.with_storage(storage)
.build()?;
// Data loads and converts automatically
// Old events appear in new buckets at estimated times
let count = new_store.query("api_call").last_minutes(60).sum();
```
All events transfer to the new configuration. Events recorded 25 minutes ago appear in bucket 25 of the new 60-bucket array.
### Adding New Time Units
Add time units for longer-term tracking:
```rust
// Old: minutes only
let old_store = EventStore::builder()
.track_minutes(60)
.with_storage(storage)
.build()?;
// New: minutes + hours
let new_store = EventStore::builder()
.track_minutes(60)
.track_hours(24)
.with_storage(storage)
.build()?;
// All minute buckets transfer
// Hour buckets populate with same events
```
Events appear in both time units. An event recorded 30 minutes ago appears in minute bucket 30 and hour bucket 0.
### Removing Time Units
Remove time units to reduce memory usage:
```rust
// Old: minutes + hours + days
let old_store = EventStore::builder()
.track_minutes(60)
.track_hours(24)
.track_days(7)
.with_storage(storage)
.build()?;
// New: hours + days only
let new_store = EventStore::builder()
.track_hours(24)
.track_days(7)
.with_storage(storage)
.build()?;
// Events transfer to remaining time units
// Minute-level detail converts to hour buckets
```
Events stay in coarser time units. Precision within the hour is lost—you know events happened this hour but not which minute.
### Reducing Bucket Count
Shrink tracking window to free memory:
```rust
// Old: track 90 days
let old_store = EventStore::builder()
.track_days(90)
.with_storage(storage)
.build()?;
// New: track 30 days
let new_store = EventStore::builder()
.track_days(30)
.with_storage(storage)
.build()?;
// Events from last 30 days transfer
// Events older than 30 days drop
```
**Events older than the new window disappear.** If you had 60 days of data and switch to 30 days, events from days 31-60 vanish permanently.
### What Happens During Conversion
The store converts data when loading from storage. Each event's buckets transfer to the new configuration:
1. Read event from storage
2. Compare stored configuration to current configuration
3. If different, convert bucket arrays
4. Transfer events to new buckets at estimated times
Timestamp estimation uses bucket midpoints. An event in bucket 5 of a 60-minute array happened approximately 55 minutes ago (5 × 60 seconds + 30 seconds).
Events in gaps between time units (like 45-minute tracking + 24-hour tracking) have less precise timestamps. The converter detects these by comparing expected counts in overlapping buckets and estimates their time using the gap midpoint.
### Precision Loss Patterns
**Lose precision when:**
Reducing buckets:
```rust
// 120 minutes → 60 minutes
// Events 61-120 minutes ago drop
```
Creating gaps:
```rust
// 60 minutes + 24 hours → 45 minutes + 24 hours
// Events 46-60 minutes ago lose per-minute precision
```
Removing fine-grained units:
```rust
// Minutes + hours → hours only
// Events lose per-minute precision
```
**Keep precision when:**
Adding buckets:
```rust
// 60 minutes → 120 minutes
// All events transfer, more detail for 61-120 minutes ago
```
Filling gaps:
```rust
// 45 minutes + 24 hours → 60 minutes + 24 hours
// Gap events (46-60 min ago) gain minute precision
```
Adding time units:
```rust
// Days only → days + weeks
// All daily data transfers, weeks populate from same data
```
### Migration Strategy
Test configuration changes before deploying:
```rust
#[test]
fn test_config_migration() {
let storage = MemoryStorage::new();
// Old config: 30 days
let old_store = EventStore::builder()
.track_days(30)
.with_storage(storage.clone())
.build()?;
old_store.record("event");
old_store.persist()?;
drop(old_store);
// New config: 60 days
let new_store = EventStore::builder()
.track_days(60)
.with_storage(storage)
.build()?;
// Verify event transferred
assert_eq!(new_store.query("event").last_days(60).sum(), Some(1));
}
```
Calculate memory impact:
```rust
// Old: 60 minutes + 24 hours = 84 buckets
// New: 60 minutes + 24 hours + 30 days = 114 buckets
// Increase: 36% more memory per event
```
Avoid shrinking windows in production. If you must shrink, export historical data first:
```rust
// Before shrinking from 90 days to 30 days
let historical = store.export_all()?;
save_to_archive(historical)?;
// Now safe to deploy with smaller window
```