Stop using manual Timers. They cause memory leaks and crashes. Switch to the standard traffic control system for Flutter & Dart.
Production-ready library unifying debounce, throttle, rate limiting, and async concurrency control into a single, battle-tested package. Like ABS brakes for your app — smooth, safe, and automatic.
┌─────────────────────────────────────────────────────────────────────┐
│ 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.
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();
}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.0.0
# Flutter + Hooks
dependencies:
flutter_debounce_throttle_hooks: ^2.0.0
# Pure Dart (Server, CLI)
dependencies:
dart_debounce_throttle: ^2.0.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.1 | 🔜 Planned | Retry policies, circuit breaker pattern |
| v2.x | 📋 Roadmap | Web Workers support, isolate-safe controllers |
Have a feature request? Open an issue
360+ tests · Zero dependencies · Type-safe · Production-ready
Made with craftsmanship by Brewkits