The complete traffic control system for Flutter & Dart. Stop UI glitches, API spam, and race conditions with production-safe debounce, throttle, and rate limiting.
Stop using manual Timers. They cause memory leaks and crashes. Switch to the production-grade event rate limiting library for Flutter & Dart.
Enterprise-ready library unifying debounce, throttle, rate limiting, and async concurrency control into a single, battle-tested package with 360+ tests and 95% coverage.
Like ABS brakes for your app β prevents crashes, stops memory leaks, handles edge cases automatically.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β flutter_debounce_throttle β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Debounce β Throttle β Rate Limit β Async Queue β Batch β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Flutter UI β Dart Backend β CLI β Serverpod β Dart Frog β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Universal | Runs everywhere β Mobile, Web, Desktop, Server |
| Safety First | No crashes, no memory leaks, lifecycle-aware |
| Zero Friction | Simple API, no boilerplate, zero dependencies |
| Problem | Impact | Solution |
|---|---|---|
| Phantom Clicks | User taps "Buy" 10x β 10 orders β refund nightmare | ThrottledInkWell blocks duplicates |
| Battery Drain | Search fires every keystroke β drains battery, burns data | Debouncer waits for typing pause |
| UI Jank | Scroll events fire 60x/sec β laggy animations | HighFrequencyThrottler at 16ms |
| Race Conditions | Old search results override new ones | ConcurrencyMode.replace cancels stale |
| Problem | Impact | Solution |
|---|---|---|
| Cost Explosion | Calling OpenAI/Maps API every request β $$$$ bill | RateLimiter controls outbound calls |
| Database Overload | Writing logs one-by-one β DB locks up | BatchThrottler batches 100 writes β 1 |
| DDoS Vulnerability | No rate limiting β server goes down | RateLimiter with Token Bucket |
Understanding the core difference with duration: 300ms:
Executes immediately, then locks for the duration. Subsequent events are ignored during the lock.
Events: (Click1) (Click2) (Click3) (Click4)
Time: |β 0ms βββββββ 100ms ββββ 200ms ββββ 300ms ββββ 400ms ββ|
βΌ β²
Execution: [EXECUTE] Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β· [LOCKED/DROP] Β·Β·Β·Β·Β·Β·Β· [EXECUTE]
ββββββββ 300ms cooldown βββββββ
Use for: Payment buttons, save buttons, scroll events
Waits for a pause in events for the duration before executing.
Events: (Type 'A') (Type 'B') (Type 'C') [User stops typing]
Time: |β 0ms ββββ 100ms ββββ 200ms ββββββββββββββ 500ms ββββββ|
βΌ βΌ βΌ β²
Execution: [WAIT] Β·Β·Β·Β·Β· [RESET] Β·Β·Β·Β·Β· [RESET] Β·Β·Β·Β·Β·Β·Β·Β· [EXECUTE 'ABC']
ββββββββ 300ms wait βββββββ
Use for: Search autocomplete, form validation, window resize
How overlapping async tasks are handled (example: two 500ms API calls):
If busy, new tasks are ignored entirely.
Task 1: [ββββββββ 500ms API Call ββββββββ] β
Completes
Task 2: β Try to start
[DROPPED β]
Result: Only Task 1 runs. Task 2 is ignored.
Use for: Payment processing, file uploads
The new task immediately cancels the running task.
Task 1: [ββββββββ 500ms API Call ββX Cancelled
Task 2: β New task starts
[ββββββββ 500ms API Call ββββββββ] β
Completes
Result: Task 1 cancelled. Only Task 2's result is used.
Use for: Search autocomplete, switching tabs, real-time filters
Tasks wait in line for their turn.
Task 1: [ββββββββ 500ms ββββββββ] β
Task 2: β Queued
[Waiting...] [ββββββββ 500ms ββββββββ] β
Result: Task 1 runs, then Task 2 runs immediately after.
Use for: Chat messages, notification queue, ordered operations
Only keeps the current running task and one latest queued task.
Task 1: [ββββββββ 500ms ββββββββ] β
Task 2: β Queued
Task 3: β Replaces Task 2 in queue
[Waiting...] [ββββββββ 500ms ββββββββ] β
Result: Task 1 runs, Task 2 is dropped, Task 3 runs after Task 1.
Use for: Auto-save, data sync, real-time updates
"What should I use for...?"
| Environment | Use Case | Solution | Why It's Better |
|---|---|---|---|
| Flutter UI | Button Click | ThrottledBuilder |
Auto loading state, auto dispose |
| Flutter UI | Search Input | DebouncedTextController |
One line, integrates with TextField |
| State Mgmt | Provider/Bloc/GetX | EventLimiterMixin |
No manual Timer management |
| Streams | Socket/Sensor data | StreamDebounceListener |
Auto-cancel subscription |
| Hooks | Functional widgets | useDebouncedCallback |
No nested widgets, clean code |
| Server | Batch DB writes | BatchThrottler |
100x fewer DB calls |
| Server | Rate limit API | RateLimiter |
Token Bucket algorithm |
| Capability | This Library | easy_debounce | rxdart | Manual Timer |
|---|---|---|---|---|
| Debounce & Throttle | β | β | β | |
| Memory Safe (Auto-dispose) | β | β | β Leaky | |
| Async & Future Support | β | β | β | β |
| Concurrency Control (4 modes) | β | β | β | |
| Rate Limiter (Token Bucket) | β | β | β | β |
| Server-side (Pure Dart) | β | β | β | β |
| Flutter Widgets | β | β | β | β |
| State Management Mixin | β | β | β | β |
| Dependencies | 0 | 0 | Many | 0 |
One library. All use cases. Zero compromises.
// β οΈ This pattern can leak memory:
class InfiniteScrollController with EventLimiterMixin {
void onPostLike(String postId) {
debounce('like_$postId', () => api.like(postId)); // New limiter per post!
}
}
// User scrolls through 1000+ posts β 1000+ limiters β OOM crashv2.3.0+ automatically cleans up unused limiters:
- Limiters unused for 10+ minutes are auto-removed
- Cleanup triggers when limiter count exceeds 100
- No configuration needed - works out of the box!
// β
This is now safe by default:
class SafeController with EventLimiterMixin {
void onPostLike(String postId) {
debounce('like_$postId', () => api.like(postId));
// Old limiters auto-cleanup after 10 minutes of inactivity
}
}void main() {
// Customize TTL and threshold
DebounceThrottleConfig.init(
limiterAutoCleanupTTL: Duration(minutes: 5), // Faster cleanup
limiterAutoCleanupThreshold: 50, // More aggressive
);
runApp(MyApp());
}Learn more: Best Practices - Memory Management
Just need a throttled button? One line:
ThrottledInkWell(onTap: () => pay(), child: Text('Pay'))Just need debounced search? One line:
TextField(onChanged: (s) => debouncer(() => search(s)))That's it. No setup. No dispose. Works immediately.
Anti-Spam Button (prevents double-tap)
ThrottledInkWell(
duration: 500.ms,
onTap: () => processPayment(),
child: Text('Pay \$99'),
)Debounced Search (waits for typing pause)
final debouncer = Debouncer(duration: 300.ms);
TextField(
onChanged: (text) => debouncer(() => search(text)),
)Async with Loading State
AsyncThrottledBuilder(
builder: (context, throttle) => ElevatedButton(
onPressed: throttle(() async => await submitForm()),
child: Text('Submit'),
),
)Cancel Stale Requests (search autocomplete)
final controller = ConcurrentAsyncThrottler(mode: ConcurrencyMode.replace);
void onSearch(String query) {
controller(() async {
final results = await api.search(query); // Old requests auto-cancelled
updateUI(results);
});
}Server-Side Batching (100x fewer DB writes)
final batcher = BatchThrottler(
duration: 2.seconds,
maxBatchSize: 50,
onBatchExecute: (logs) => database.insertBatch(logs),
);
batcher(() => logEntry); // 1000 calls β 20 batchesToken Bucket Rate Limiting (API cost control)
final limiter = RateLimiter(maxTokens: 100, refillRate: 10);
if (!limiter.tryAcquire()) {
return Response.tooManyRequests();
}Distributed Rate Limiting (Multi-server rate limits with Redis)
// Setup Redis store
final redis = await RedisConnection().connect('localhost', 6379);
final store = RedisRateLimiterStore(redis: redis, keyPrefix: 'api:');
// Rate limit across all server instances
final limiter = DistributedRateLimiter(
key: 'user-${userId}',
store: store,
maxTokens: 1000,
refillRate: 100, // 100 requests/sec
refillInterval: Duration(seconds: 1),
);
if (!await limiter.tryAcquire()) {
return Response.tooManyRequests();
}Throttled Gesture Detector (Universal gesture throttling)
// Throttle ALL gesture types automatically
ThrottledGestureDetector(
discreteDuration: 500.ms, // For taps, long press
continuousDuration: 16.ms, // For pan, scale (60fps)
onTap: () => handleTap(),
onLongPress: () => showMenu(),
onPanUpdate: (details) => updatePosition(details.delta),
onScaleUpdate: (details) => zoom(details.scale),
child: MyWidget(),
)Finally, a drop-in replacement for GestureDetector with built-in throttling! No more manual wrapper logic.
Key Features:
- β Full GestureDetector API - All 40+ callbacks supported
- β Smart Throttling - Discrete events (tap, long press) and continuous events (pan, scale) use different throttle strategies
- β 60fps Smooth - Continuous gestures throttled at 16ms by default for silky animations
- β
Zero Config - Just replace
GestureDetectorwithThrottledGestureDetector
Example:
// Before: Manual throttling, complex wrapper logic
GestureDetector(
onTap: () => _throttler.call(() => handleTap()),
onPanUpdate: (details) => _panThrottler.call(() => updatePosition(details)),
child: MyWidget(),
)
// After: One widget, automatic throttling
ThrottledGestureDetector(
onTap: () => handleTap(),
onPanUpdate: (details) => updatePosition(details.delta),
child: MyWidget(),
)Rate limiting that works across your entire backend cluster. Perfect for microservices and serverless.
Key Features:
- β Distributed - Share rate limits across multiple server instances
- β Redis/Memcached - Built-in support with reference implementations
- β
Custom Stores - Implement
AsyncRateLimiterStorefor any backend - β Production Ready - Atomic operations, fail-safe design
Server-Side Example (Dart Frog / Shelf):
// Setup once
final store = RedisRateLimiterStore(
redis: await RedisConnection().connect('redis-server', 6379),
keyPrefix: 'ratelimit:',
ttl: Duration(hours: 1),
);
// Middleware
Handler rateLimitMiddleware(Handler handler) {
return (context) async {
final userId = context.read<User>().id;
final limiter = DistributedRateLimiter(
key: 'user:$userId',
store: store,
maxTokens: 100, // Burst capacity
refillRate: 10, // 10 requests/sec sustained
refillInterval: Duration(seconds: 1),
);
if (!await limiter.tryAcquire()) {
return Response(
statusCode: 429,
headers: {
'Retry-After': (await limiter.timeUntilNextToken).inSeconds.toString(),
},
);
}
return handler(context);
};
}Custom Storage Implementation:
// Implement for any backend (PostgreSQL, MongoDB, etc)
class PostgresStore implements AsyncRateLimiterStore {
final Database db;
@override
Future<RateLimiterState> fetchState(String key) async {
final row = await db.query('SELECT tokens, last_refill FROM rate_limits WHERE key = ?', [key]);
return row.isEmpty
? RateLimiterState(tokens: 0, lastRefillMicroseconds: 0)
: RateLimiterState.fromList([row['tokens'], row['last_refill']]);
}
@override
Future<void> saveState(String key, RateLimiterState state) async {
await db.execute(
'INSERT INTO rate_limits (key, tokens, last_refill) VALUES (?, ?, ?) '
'ON CONFLICT (key) DO UPDATE SET tokens = ?, last_refill = ?',
[key, state.tokens, state.lastRefillMicroseconds, state.tokens, state.lastRefillMicroseconds],
);
}
}Migration takes 2 minutes. You get memory safety for free.
// Before (easy_debounce) - manual cancel, possible memory leak
EasyDebounce.debounce('search', Duration(ms: 300), () => search(q));
// After - auto-dispose, lifecycle-aware
final debouncer = Debouncer(duration: 300.ms);
debouncer(() => search(q));See full Migration Guide β
# Flutter App
dependencies:
flutter_debounce_throttle: ^2.4.0
# Flutter + Hooks
dependencies:
flutter_debounce_throttle_hooks: ^2.4.0
# Pure Dart (Server, CLI)
dependencies:
dart_debounce_throttle: ^2.4.0| Guarantee | How |
|---|---|
| Stability | 360+ tests, 95% coverage |
| Type Safety | No dynamic, full generic support |
| Lifecycle Safe | Auto-checks mounted, auto-cancel on dispose |
| Memory Safe | Zero leaks (verified with LeakTracker) |
| Zero Dependencies | Only meta package in core |
| FAQ | Common questions answered |
| API Reference | Complete API documentation |
| Best Practices | Patterns & recommendations |
| Migration Guide | From easy_debounce, rxdart |
| Examples | Interactive demos |
| Package | Platform | Use Case |
|---|---|---|
flutter_debounce_throttle |
Flutter | Widgets, Mixin |
flutter_debounce_throttle_hooks |
Flutter + Hooks | useDebouncer, useThrottler |
dart_debounce_throttle |
Pure Dart | Server, CLI, anywhere |
We're committed to long-term maintenance and improvement.
| Version | Status | Features |
|---|---|---|
| v1.0 | β Released | Core debounce/throttle, widgets, mixin |
| v1.1 | β Released | RateLimiter, extensions, leading/trailing edge, batch limits |
| v2.0 | β Released | Package rename to dart_debounce_throttle, improved documentation |
| v2.2 | β Released | Error handling (onError callbacks), TTL auto-cleanup, performance optimization |
| v2.3 | β Released | Memory leak prevention, auto-cleanup for EventLimiterMixin |
| v2.4 | β Released | ThrottledGestureDetector, DistributedRateLimiter with Redis/Memcached support |
| v2.5 | π Planned | Retry policies, circuit breaker pattern |
| v3.x | π Roadmap | Web Workers support, isolate-safe controllers |
Have a feature request? Open an issue
300+ tests Β· Zero dependencies Β· Type-safe Β· Production-ready
Made with craftsmanship by Brewkits