Skip to content

RuntimeError: deque mutated during iteration in Scope.fork() on Python 3.14 free-threaded #5465

@yilei

Description

@yilei

How do you use Sentry?

Sentry Saas (sentry.io)

Version

2.52.0

Steps to Reproduce

Use ThreadingIntegration(propagate_scope=True) in the following example code :

import sys
import threading

import sentry_sdk
from sentry_sdk.integrations.threading import ThreadingIntegration


def main():
    print(f"Python {sys.version}")
    print(f"sentry-sdk {sentry_sdk.VERSION}")
    print(f"GIL enabled: {sys._is_gil_enabled()}")

    sentry_sdk.init(
        dsn="https://examplePublicKey@o0.ingest.sentry.io/0",
        integrations=[ThreadingIntegration(propagate_scope=True)],
        traces_sample_rate=0,
    )

    # Grab the main thread's isolation scope. The race requires multiple
    # threads to operate on the same Scope object — one mutating
    # _breadcrumbs, another copying it via fork().
    shared_scope = sentry_sdk.get_isolation_scope()

    stop = threading.Event()

    def mutate_breadcrumbs():
        """Appends breadcrumbs to the shared scope's _breadcrumbs deque."""
        i = 0
        while not stop.is_set():
            shared_scope.add_breadcrumb(
                {"category": "test", "message": f"crumb-{i}", "level": "info"},
                hint={},
            )
            i += 1

    def fork_shared_scope():
        """Forks the shared scope, triggering copy.copy(deque)."""
        while not stop.is_set():
            shared_scope.fork()

    print()

    # Use a barrier so all threads start racing simultaneously.
    num_workers = 4
    barrier = threading.Barrier(num_workers + 1)

    def run_with_barrier(fn):
        barrier.wait()
        fn()

    threads: list[threading.Thread] = []
    for _ in range(2):
        t = threading.Thread(
            target=run_with_barrier, args=(mutate_breadcrumbs,), daemon=True
        )
        t.start()
        threads.append(t)
    for _ in range(2):
        t = threading.Thread(
            target=run_with_barrier, args=(fork_shared_scope,), daemon=True
        )
        t.start()
        threads.append(t)

    # Release all threads at once
    barrier.wait()

    try:
        stop.wait(timeout=5)
    except KeyboardInterrupt:
        pass
    finally:
        stop.set()
        for t in threads:
            t.join(timeout=5)


if __name__ == "__main__":
    main()

Expected Result

The program should finish without errors in Python 3.14 free-threaded mode.

Actual Result

❯ python sentry_free_threaded_repro.py
Python 3.14.3 free-threading build (main, Feb 13 2026, 18:56:10) [GCC 11.4.0]
sentry-sdk 2.52.0
GIL enabled: False

Exception in thread Thread-3 (run_with_barrier):
Exception in thread Thread-4 (run_with_barrier):
Traceback (most recent call last):
Traceback (most recent call last):
  File "lib/python3.14t/threading.py", line 1082, in _bootstrap_inner
    self._context.run(self.run)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^
  File "lib/python3.14t/site-packages/sentry_sdk/integrations/threading.py", line 140, in run
    return _run_old_run_func()
  File "lib/python3.14t/site-packages/sentry_sdk/integrations/threading.py", line 135, in _run_old_run_func
    reraise(*_capture_exception())
    ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "lib/python3.14t/site-packages/sentry_sdk/utils.py", line 1784, in reraise
    raise value
  File "lib/python3.14t/site-packages/sentry_sdk/integrations/threading.py", line 133, in _run_old_run_func
    return old_run_func(self, *a[1:], **kw)
  File "lib/python3.14t/threading.py", line 1024, in run
    self._target(*self._args, **self._kwargs)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "sentry_free_threaded_repro.py", line 49, in run_with_barrier
    fn()
    ~~^^
  File "sentry_free_threaded_repro.py", line 39, in fork_shared_scope
    shared_scope.fork()
    ~~~~~~~~~~~~~~~~~^^
  File "lib/python3.14t/site-packages/sentry_sdk/scope.py", line 511, in fork
    forked_scope = copy(self)
  File "lib/python3.14t/copy.py", line 82, in copy
    return copier(x)
  File "lib/python3.14t/site-packages/sentry_sdk/scope.py", line 288, in __copy__
    rv._breadcrumbs = copy(self._breadcrumbs)
                      ~~~~^^^^^^^^^^^^^^^^^^^
  File "lib/python3.14t/copy.py", line 82, in copy
    return copier(x)
RuntimeError: deque mutated during iteration
  File "lib/python3.14t/threading.py", line 1082, in _bootstrap_inner
    self._context.run(self.run)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^
  File "lib/python3.14t/site-packages/sentry_sdk/integrations/threading.py", line 140, in run
    return _run_old_run_func()
  File "lib/python3.14t/site-packages/sentry_sdk/integrations/threading.py", line 135, in _run_old_run_func
    reraise(*_capture_exception())
    ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "lib/python3.14t/site-packages/sentry_sdk/utils.py", line 1784, in reraise
    raise value
  File "lib/python3.14t/site-packages/sentry_sdk/integrations/threading.py", line 133, in _run_old_run_func
    return old_run_func(self, *a[1:], **kw)
  File "lib/python3.14t/threading.py", line 1024, in run
    self._target(*self._args, **self._kwargs)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "sentry_free_threaded_repro.py", line 49, in run_with_barrier
    fn()
    ~~^^
  File "sentry_free_threaded_repro.py", line 39, in fork_shared_scope
    shared_scope.fork()
    ~~~~~~~~~~~~~~~~~^^
  File "lib/python3.14t/site-packages/sentry_sdk/scope.py", line 511, in fork
    forked_scope = copy(self)
  File "lib/python3.14t/copy.py", line 82, in copy
    return copier(x)
  File "lib/python3.14t/site-packages/sentry_sdk/scope.py", line 288, in __copy__
    rv._breadcrumbs = copy(self._breadcrumbs)
                      ~~~~^^^^^^^^^^^^^^^^^^^
  File "lib/python3.14t/copy.py", line 82, in copy
    return copier(x)
RuntimeError: deque mutated during iteration

As a workaround, we should be able to use a lock to protect calls to Scope's mutating methods. But it would be ideal if we could make the Scope class thread safe. What do you think?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions