Future vs Stream for Continuous Polling: Which Keeps Memory Usage in Check?
0 reputation · 07 Jun 2020, 02:38 UTC
0 reputation · 07 Jun 2020, 02:38 UTC
Implement a background service that polls a REST endpoint every minute and processes the response in a Flutter app.
The service runs for hours, so memory consumption and subscription lifecycle must be managed carefully. The implementation should be easy to debug and maintain.
Should the polling logic use a single Future per request, or a Stream that emits each poll result?
StreamController impact memory when emitting one event per minute over long periods?Stream to prevent leaks in a long‑running background service?Future-based approach simplify debugging compared to a Stream-based approach in this scenario?For a single background consumer polling a REST endpoint every minute, use a sequential Future loop. Await each response before scheduling the next poll. A Stream is the better fit only when multiple independent consumers must react to the same result.
Confirmed: Neither Future nor Stream leaks memory by itself. Heap growth over hours comes from uncanceled subscriptions, an unclosed StreamController, retained response objects, listeners capturing large contexts, or buffered errors.
Likely: A linear async/await chain is easier to debug because stack traces follow the await path. A long-lived Stream spreads lifecycle, controller state, and error handling across emissions, making hour-long sessions harder to trace.
Use a cancellable loop rather than Timer.periodic with an async callback. This prevents overlapping HTTP responses if a poll takes longer than one minute.
bool _running = true;
Future<void> _pollLoop() async {
while (_running) {
try {
final res = await _pollEndpoint();
_process(res); // release res after processing
} catch (e, st) {
_logError(e, st);
}
if (!_running) break;
await Future.delayed(const Duration(minutes: 1));
}
}
void dispose() {
_running = false;
// cancel any pending HTTP work if possible
}
If you choose a Stream, close it deterministically:
final _controller = StreamController<PollResult>.broadcast();
StreamSubscription? _sub;
void start() {
_sub = _controller.stream.listen(_handle);
}
void dispose() {
_sub?.cancel();
_controller.close(); // after listeners are gone
}
Timer or set the loop flag to false in dispose.StreamController, cancel all subscriptions before calling close(); do not add events after close.Future or response in a list or map. Process and drop each response.BuildContext, large JSON, images, and listener closures after each poll so the GC can reclaim them.Confirmed: iOS and Android do not guarantee continuous background execution for hours. The OS may suspend or terminate the isolate. If the service must run while the app is backgrounded, use platform background fetch or a work-manager plugin; the in-app loop only covers foreground execution.
Run the service in Flutter DevTools memory view for an extended period. After garbage collection, the heap should not step upward with each poll. Log poll start, completion, error, and dispose to confirm no accumulation. This article assumes current stable Flutter; verify background plugin behavior against your target OS versions.
The one detail that would change this recommendation is whether more than one consumer needs each poll result. If yes, switch to a broadcast Stream with the lifecycle above; otherwise the sequential Future loop remains the simpler, lower-risk choice.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.