DEV Community

Cover image for The Most Comprehensive Event Rate Limiting Solution for Flutter & Dart Ecosystem
Viet, Nguyen Tuan
Viet, Nguyen Tuan

Posted on

The Most Comprehensive Event Rate Limiting Solution for Flutter & Dart Ecosystem

The Problem You're Facing

Are you implementing debounce/throttle manually with Timer?

class _SearchState extends State<SearchPage> {
  Timer? _debounceTimer;

  void onSearchChanged(String query) {
    _debounceTimer?.cancel();
    _debounceTimer = Timer(Duration(milliseconds: 300), () {
      _callSearchAPI(query);
    });
  }

  @override
  void dispose() {
    _debounceTimer?.cancel(); // Forget this = memory leak!
    super.dispose();
  }
}
Enter fullscreen mode Exit fullscreen mode

3 critical issues:

  1. Memory leak if you forget to dispose
  2. Race condition - old requests return after new ones
  3. Code duplication every time you need debounce

Solution: flutter_debounce_throttle

An all-in-one library with 360+ tests and 95% coverage that solves everything:

✅ Automatic lifecycle management

class _NewWayState extends State<NewWay> {
  final debouncer = Debouncer(duration: 300.ms);

  void search(String q) {
    debouncer(() => api.search(q)); // That's it!
  }
  // No need to dispose anything!
}
Enter fullscreen mode Exit fullscreen mode

✅ Proper async handling

final controller = ConcurrentAsyncThrottler(
  mode: ConcurrencyMode.replace, // Auto cancel old requests
);

void onSearch(String query) {
  controller(() async {
    final results = await api.search(query);
    setState(() => searchResults = results);
  });
}
Enter fullscreen mode Exit fullscreen mode

✅ Ready-to-use widgets

// Anti-spam button (1 line!)
ThrottledInkWell(
  onTap: () => processPayment(),
  child: Text('Pay \$99'),
)

// Search with loading state
DebouncedQueryBuilder<List<User>>(
  onQuery: (text) async => await searchUsers(text),
  builder: (context, search, isLoading) => TextField(onChanged: search),
)
Enter fullscreen mode Exit fullscreen mode

Quick Comparison

Feature This lib easy_debounce rxdart Manual Timer
Auto-dispose ⚠️
Async support
Race condition handling ⚠️
Rate Limiter
Flutter Widgets
Server-side (Pure Dart)
Dependencies 0 0 Many 0

Real-World Use Cases

📱 Prevent double-click

ThrottledInkWell(
  duration: 2.seconds,
  onTap: () => processPayment(),
  child: Text('Buy \$99'),
)
// User taps 10 times → only 1 is processed
Enter fullscreen mode Exit fullscreen mode

🔍 Search autocomplete

final debouncer = Debouncer(duration: 300.ms);
TextField(onChanged: (text) => debouncer(() => search(text)))
// Only call API when user stops typing for 300ms
Enter fullscreen mode Exit fullscreen mode

🖥️ Batch DB writes (Backend)

final batcher = BatchThrottler(
  duration: 2.seconds,
  maxBatchSize: 100,
  onBatchExecute: (logs) => db.insertBatch(logs),
);
batcher(() => logEntry);
// 1000 calls → 10 batches → 90% less DB load
Enter fullscreen mode Exit fullscreen mode

💰 Rate limit API

final limiter = RateLimiter(maxTokens: 1000, refillRate: 100);
if (!limiter.tryAcquire()) return 'Rate limit exceeded';
await callOpenAI(prompt);
Enter fullscreen mode Exit fullscreen mode

Advanced Features

🏢 Distributed Rate Limiting (v2.4+)

Rate limiting across multiple server instances with Redis:

final limiter = DistributedRateLimiter(
  key: 'user:$userId',
  store: RedisRateLimiterStore(redis: redis),
  maxTokens: 100,
  refillRate: 10,
);
if (!await limiter.tryAcquire()) return Response(statusCode: 429);
Enter fullscreen mode Exit fullscreen mode

🧹 Auto-cleanup (v2.3+)

Automatically cleanup unused limiters to prevent memory leaks with dynamic IDs:

class PostController with EventLimiterMixin {
  void onLike(String postId) {
    debounce('like_$postId', () => api.like(postId));
  }
}
// Auto-remove unused limiters after 10 minutes
// User scrolls through 1000 posts - no OOM crash!
Enter fullscreen mode Exit fullscreen mode

🎮 ThrottledGestureDetector

Drop-in replacement for GestureDetector with automatic throttling:

ThrottledGestureDetector(
  onTap: () => handleTap(),
  onPanUpdate: (details) => updatePosition(details.delta),
  child: MyWidget(),
)
// Supports 40+ gesture callbacks with auto throttling!
Enter fullscreen mode Exit fullscreen mode

Throttle vs Debounce?

Throttle: Execute immediately, lock for 300ms, ignore events during lock

  • Use: Button clicks, scroll events

Debounce: Wait for user to stop for 300ms before executing

  • Use: Search input, form validation

4 Concurrency Modes:

  • drop: Ignore new task if busy
  • replace: Cancel old task, run new one (perfect for search)
  • enqueue: Queue tasks
  • keepLatest: Keep only running task + latest task

Mixin for State Management

Works with Provider, GetX, Bloc, Riverpod...

class SearchController extends GetxController with EventLimiterMixin {
  void search(String query) {
    debounceAsync('search', () async {
      final results = await api.search(query);
      update(results);
    }, mode: ConcurrencyMode.replace);
  }

  @override
  void onClose() {
    cancelAll(); // Auto cleanup
    super.onClose();
  }
}
Enter fullscreen mode Exit fullscreen mode

Production Quality

  • 360+ tests with 95% coverage
  • Zero dependencies (only meta)
  • Type-safe (full generic support)
  • Memory-safe (verified with LeakTracker)
  • Cross-platform (Mobile, Web, Desktop, Server)

Installation

# Flutter App
dependencies:
  flutter_debounce_throttle: ^2.4.2

# Flutter + Hooks
dependencies:
  flutter_debounce_throttle_hooks: ^2.4.2

# Pure Dart (Server/CLI)
dependencies:
  dart_debounce_throttle: ^2.4.2
Enter fullscreen mode Exit fullscreen mode

Conclusion

If you're using:

  • ❌ Manual Timer → Memory leak risk
  • easy_debounce → Missing async support
  • rxdart only for debounce → Overkill dependencies

Then flutter_debounce_throttle is the solution:

  • ✅ Memory-safe by default
  • ✅ Production-tested (360+ tests)
  • ✅ Universal (Flutter + Dart backend)
  • ✅ Zero dependencies

Links:

Technical review of flutter_debounce_throttle v2.4.2

Top comments (0)