What this usually means
The FutureBuilder's future argument is being recreated on every build, causing it to re-execute the asynchronous function. This happens when the future is created inline inside the build method or when setState is called somewhere in the ancestor tree on every frame (e.g., in a build method or via a listener that fires continuously). The FutureBuilder itself is not broken—it's reacting to a new Future object each time it builds.
The first ten minutes — establish facts before touching code.
- 1Enable the Flutter inspector's 'Rebuild Count' overlay to see widget rebuild frequencies.
- 2Add a print('build') at the top of the build method containing the FutureBuilder to confirm infinite rebuilds.
- 3Check if the future is created inline: `future: someAsyncFunction()` inside build – move it out.
- 4Look for any setState calls in the ancestor widget's build method or in event handlers that fire continuously.
- 5Inspect the parent widget's didUpdateWidget to see if props change on every frame.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchThe widget's build method – future definition location
- searchParent widget's didUpdateWidget and build methods
- searchAny ChangeNotifier or Stream that might be firing continuously
- searchFlutter DevTools timeline for frame rebuilds and widget rebuild count
- searchNetwork tab in DevTools to see repeated API calls
- searchState class initState/dispose – ensure future is created once
Practical causes, not theory. These are the things you will actually find.
- warningFuture created inline in build method: `FutureBuilder(future: fetchData())`
- warningsetState called inside build method (directly or via listener)
- warningParent widget rebuilds due to InheritedWidget or MediaQuery changes on every frame
- warningStream or broadcast stream used as future argument that never completes
- warningAnimation controller or ticker that updates state on every frame
Concrete fix directions. Pick the one that matches your root cause.
- buildStore the Future in a class field (initState, didChangeDependencies, or constructor) and reuse it.
- buildUse Future.microtask or compute to defer heavy work if needed, but still assign once.
- buildIf using BLoC or Provider, use AsyncSnapshot's connectionState to handle loading/error in a StreamBuilder instead.
- buildFor streams, use StreamBuilder with a broadcast stream that doesn't complete—but ensure you handle closing it.
- buildWrap the FutureBuilder in a RepaintBoundary or const constructor to limit rebuilds.
A fix you cannot prove is a guess. Close the loop.
- verifiedAdd a breakpoint or print in the async function; it should only fire once on initial load.
- verifiedCheck Flutter DevTools widget rebuild count: should be 1 or 2 (initial + data).
- verifiedMonitor network calls: only one API request per page load.
- verifiedRemove the FutureBuilder and verify that the parent rebuilds stop (if the issue is in the parent).
- verifiedTest on a slow connection: the UI should not freeze or show repeated loading spinners.
Things that make this bug worse or harder to find.
- warningCreating the future inside build even if you think it's cheap – it's still a new object.
- warningUsing setState in a listener that fires on every frame (e.g., scroll listener).
- warningAssuming StreamBuilder has the same issue – it doesn't, but don't misuse it.
- warningForgetting to cancel the future in dispose if using a Timer or delayed execution.
- warningWrapping the entire app in a Builder that calls setState on rebuild.
The Infinite API Call that Tanked Production
Timeline
- 09:15User reports app freezes after navigating to profile page.
- 09:20I replicate: profile page loads, then spinner appears repeatedly.
- 09:25Check DevTools: widget rebuild count for ProfileCard is over 1000 in 10 seconds.
- 09:30Network tab: GET /user/profile fires every 100ms.
- 09:35Inspect ProfileCard build method: `future: fetchProfile()` inside build.
- 09:37Check parent: ProfilePage uses a StreamProvider that emits new state on every connection change.
- 09:40Fix: move future to initState; also stabilize parent by filtering stream emissions.
- 09:45Deploy hotfix. Rebuild count drops to 2. No more freezes.
I got a Slack ping from a user: 'App freezes when I tap on my profile.' Classic. I opened the app, navigated to the profile, and saw the loading spinner flash repeatedly. The UI was completely unresponsive after a few seconds. I pulled up DevTools and the widget rebuild count was off the charts—over 1000 rebuilds for a simple card widget.
I checked the network tab next. The GET /user/profile endpoint was being called every 100ms. The backend team was probably wondering why we were DDoS-ing them. I opened the ProfileCard widget and saw the culprit immediately: `future: fetchProfile()` was written directly in the build method. Every rebuild created a new Future, so the FutureBuilder re-executed the async function. But why was it rebuilding so often?
I looked at the parent, ProfilePage. It used a StreamProvider that emitted a new value every time the connectivity state changed. The phone was switching between WiFi and cellular, causing the stream to emit repeatedly. That rebuild the parent, which rebuilt the child, which recreated the future. I moved the future to initState and added a `distinct()` filter on the stream. Rebuild count went from 1000 to 2. The fix took 10 minutes, but finding the root cause took 30. The lesson: always store futures, and stabilize your state providers.
Root cause
Future created inline in build method combined with a parent widget that rebuilt on every connectivity change.
The fix
Moved future creation to initState and applied `distinct()` on the stream provider to prevent unnecessary rebuilds.
The lesson
Never create a Future inside a build method. Always assign it once in initState or a constructor. Also, stabilize your state providers to avoid cascading rebuilds.
FutureBuilder works by comparing the previous future object to the new one using ==. If they differ, it cancels the old future (if still pending) and starts the new one. When you inline `future: fetchData()`, Dart creates a new closure on every build, so the reference changes every time. This triggers a restart of the async operation.
Even if you use `const` constructor for the parent widget, if the future is created inline, the Future object is new each time because the closure captures the current context. The fix is to store the Future in a state variable and pass it directly.
Open DevTools and go to the 'Rebuild Counts' tab. It shows a color-coded overlay: green for few rebuilds, red for many. Click on a widget to see its exact rebuild count. For FutureBuilder, if the count exceeds the number of expected async state changes (typically 2: loading + data/error), something is wrong.
You can also use the 'Timeline' tab to see frame build times. If you see frames taking >16ms and the widget tree rebuilding repeatedly, that's a strong indicator. Pair this with a print statement in the async function to confirm it's being called multiple times.
StreamBuilder does not have the same issue because it doesn't cancel and restart on every rebuild—it listens to the same stream unless the stream object changes. However, if you recreate the stream on every build, you'll get the same infinite loop. The key difference is that FutureBuilder explicitly checks for a new Future object, while StreamBuilder reuses the stream subscription as long as the stream identity is the same.
Practical advice: use FutureBuilder for one-shot async work (API calls), and StreamBuilder for continuous data (Firestore, WebSockets). Always store the stream in a variable to avoid recreation.
Even if you correctly store the future, a parent widget that rebuilds on every frame will cause the FutureBuilder to rebuild too. The FutureBuilder itself won't restart the future, but it will rebuild its builder function, which can be expensive and cause UI jank. Common culprits: MediaQuery, LayoutBuilder, AnimatedBuilder, or any widget that calls setState in build.
To isolate, wrap the FutureBuilder in a RepaintBoundary or use a const constructor for the parent. If the parent is a stateful widget, check didUpdateWidget for unnecessary setState calls. Use `shouldRepaint` or `const` to minimize rebuilds.
Frequently asked questions
Does using `const` on the FutureBuilder help?
No. The FutureBuilder itself is not const if you pass a non-const future. Even if you mark it const, the future argument is evaluated at runtime, so the widget rebuilds. The fix is to ensure the future reference doesn't change, not to make the widget const.
Can I use `Future.delayed` to debounce?
Not as a fix for infinite rebuilds—that's a hack. The proper fix is to prevent the future from being recreated. Using Future.delayed might reduce frequency but still creates new futures, and the old ones are cancelled, leading to race conditions.
Why does StreamBuilder not have this problem?
StreamBuilder compares stream objects by identity, not by equality. If the same stream instance is passed, it doesn't restart. FutureBuilder uses `==` which for closures is reference equality, but inline functions create new references. So StreamBuilder is more forgiving, but still, avoid recreating streams.
What if I need to refetch data? How do I trigger a new future?
Use a separate state variable (like an int counter) that you increment in initState or via a button. Pass the counter as a key to the FutureBuilder or use a method that returns a new future based on the counter. The key ensures the widget is rebuilt with a new future intentionally.
Is it safe to use a FutureBuilder inside a ListView.builder?
Yes, but you must ensure each item has a stable future. If the list rebuilds often (e.g., due to scroll), you'll create many futures. Instead, pre-fetch data and store it in a map keyed by index or id, then use a FutureBuilder that references the stored future.