website/docs/cookbook/multiprocessing.md
import {CodeExample} from '@site/src/components/crocodocs';
In this cookbook recipe, you'll learn how to use Python's built-in
multiprocessing module —
including concurrent.futures.ProcessPoolExecutor —
from a Flet app, for true CPU parallelism across processes.
For I/O-bound work, or work that just needs to stay off the UI thread, prefer
async or threads — they are lighter and work on every platform.
Reach for multiprocessing when you need multiple CPU cores doing Python work
at the same time (number crunching, batch processing, ML inference, etc.), or
when you need process isolation for work that may fail or need to be stopped.
:::important[Platform and Flet version support]
multiprocessing works in Flet desktop apps during development (flet run) and
in packaged desktop apps built with flet build {macos,windows,linux}
or flet debug {macos,windows,linux} when using Flet v0.86.0 or newer.
It is not supported on iOS and Android (mobile operating systems don't
allow apps to spawn arbitrary child processes) or in the browser. On those
platforms, prefer threads or asyncio instead.
:::
In a desktop app packaged with flet build, there is no separate python executable —
the interpreter is embedded inside your app's binary. When multiprocessing spawns a
worker, it re-executes that binary with a CPython helper command line; the binary
recognizes that shape and services it as a plain, windowless Python interpreter.
This also covers multiprocessing's helper processes (the resource tracker and the forkserver).
These are standard
Python multiprocessing guidelines
— but in a packaged Flet app they are mandatory, not just good style.
Start your app only under the if __name__ == "__main__": guard. For example:
import flet as ft
def main(page: ft.Page):
...
if __name__ == "__main__":
ft.run(main)
With the spawn and forkserver start methods, worker/helper processes need
to safely import your main module. spawn is the default on macOS and Windows;
forkserver is the default on Linux starting with Python 3.14. Without the
guard, a child process can try to start your whole app again.
Worker targets, arguments, and return values must be picklable so Python can send them between processes. In practice:
main() or inside a
button handlerpage, database connections, open files, lambdas,
or nested functionsGood:
def sort_chunk(chunk):
return sorted(chunk)
Avoid:
def main(page: ft.Page):
def sort_chunk(chunk):
return sorted(chunk)
The nested version is not reliably picklable because worker processes need to import the function by name from a module.
Worker processes run in a separate interpreter with no connection to your app's
page. Pass data back through multiprocessing.Queue,
Pipe, or
pool futures,
and update the UI from the main process.
sys.executable in a packaged app points at your app's binary, not a
python executable. That is intentional — don't override it with
multiprocessing.set_executable().multiprocessing.freeze_support() in Flet apps.
Calling it inside the if __name__ == "__main__": block is harmless, but Flet
does not rely on PyInstaller-style frozen-executable bootstrapping.print() output is not connected to your app's console log; use a
Queue or file-based logging if you need worker diagnostics.fork start method:
your app's process runs the Flutter engine with many active threads, and forking it is unsafe.
Prefer the platform default (usually forkserver or spawn), or request one explicitly with
multiprocessing.set_start_method()
or multiprocessing.get_context().Sort chunks of data across all CPU cores and stream progress to the page:
<CodeExample path="cookbook/multiprocessing/parallel_sort.py" language="python" />Note how the long-running orchestration is moved off the UI event handler with
page.run_thread, while the CPU-heavy work runs in the
process pool. The worker function may live in your main module (as above) or in
a separate importable module — both work.
To show fine-grained progress from inside a single long-running job, pass a
multiprocessing.Queue
to the worker and drain it on a background thread. The worker put()s progress
values and a None sentinel when it is done:
Starting a process is not free — especially in a packaged app, where each child
loads your app binary's libraries first. For repeated jobs, start one worker
that stays alive and serves requests over a
Pipe,
paying the startup cost once. Expensive setup (loading a model, opening a
dataset) can then also happen once, in the worker:
The worker is started with daemon=True, so it is terminated automatically
when your app exits.
Threads cannot be forcefully stopped from the outside — a worker Process can,
at any time, with terminate().
This makes processes the right tool for jobs you may need to abort, such as
long calls into external libraries:
A background thread join()s the worker and reports how it ended — normally
(exit code 0) or via cancellation (a negative exit code).
:::info Python's multiprocessing guidelines recommend avoiding process termination or doing it only for processes which never use any shared resources. :::