# Free-Threading URL: /docs/about/free-threading/ Section: about Tags: free-threading, performance, architecture -------------------------------------------------------------------------------- Free-Threading Bengal is designed for Python 3.14t (free-threaded Python). On free-threaded builds, page rendering achieves true parallelism — no GIL contention. The same code runs on standard Python; it just uses sequential rendering until you switch interpreters. Run It uv python install 3.14t uv run --python=3.14t bengal build Bengal auto-detects free-threading at runtime and uses ThreadPoolExecutor when available. Key Patterns Runtime Detection Bengal checks sys._is_gil_enabled() to decide whether threads provide real parallelism: def is_free_threaded() -> bool: if hasattr(sys, "_is_gil_enabled"): try: return not sys._is_gil_enabled() except (AttributeError, TypeError): pass # Fallback: sysconfig for Py_GIL_DISABLED ... When True, Bengal spins up a ThreadPoolExecutor for page rendering. Immutable Snapshots (Lock-Free) After content discovery, Bengal freezes the entire site into immutable dataclasses — PageSnapshot, SectionSnapshot, SiteSnapshot. During rendering, workers only read from snapshots. No locks in the hot path. @dataclass(frozen=True, slots=True) class PageSnapshot: title: str href: str source_path: Path parsed_html: str content_hash: str section: SectionSnapshot | None = None next_page: PageSnapshot | None = None prev_page: PageSnapshot | None = None ... Context Propagation ThreadPoolExecutor.submit() does not inherit ContextVar values. Bengal uses contextvars.copy_context().run so each worker gets the parent's context: ctx = contextvars.copy_context() future_to_page = { executor.submit(ctx.run, process_page_with_pipeline, page): page for page in batch } Thread-Local Pipelines Each worker gets its own rendering pipeline via threading.local(), avoiding shared mutable state: def get_or_create_pipeline(current_gen, create_pipeline_fn): needs_new = ( not hasattr(thread_local, "pipeline") or getattr(thread_local, "pipeline_generation", -1) != current_gen ) if needs_new: thread_local.pipeline = create_pipeline_fn() thread_local.pipeline_generation = current_gen return thread_local.pipeline Performance Workers Time (1K pages) Speedup 1 ~9.0 s 1.0x 4 ~3.0 s 3.0x 8 ~2.5 s 3.6x On free-threaded Python, ~1.5–2x faster than GIL builds at the same worker count. Incremental builds: 35–80 ms for a single-page change on an 800-page site. Thread-Safe Internals (0.2.7+) Several internal components were hardened for free-threading in v0.2.7: EffectTracer Lock Serialization The build tracer records timing, cache hits, and fingerprint data. Without the GIL, concurrent defaultdict access can observe partially-constructed entries. All EffectTracer mutations (record, record_batch, update_fingerprint, flush_pending_fingerprints, clear) and reads now acquire a lock: class EffectTracer: def __init__(self): self._lock = threading.Lock() self._effects: dict[str, list] = defaultdict(list) def record(self, phase, event): with self._lock: self._effects[phase].append(event) Thread-Safe LRU Cache functools.lru_cache relies on the GIL for internal dict coherency. Bengal's get_bengal_dir() — called from multiple threads during rendering — now uses LRUCache from bengal.utils.primitives.lru_cache, which protects access with an RLock: # Before (GIL-dependent): @lru_cache(maxsize=1) def get_bengal_dir(): ... # After (free-threading safe): _cache = LRUCache(maxsize=1) def get_bengal_dir(): return _cache.get_or_compute("bengal_dir", _find_bengal_dir) Plugin Registry Freeze The plugin system uses a builder→immutable pattern. PluginRegistry collects extensions during startup, then freeze() produces a FrozenPluginRegistry (frozen dataclass with tuple fields) — no locks needed during rendering because the data is immutable. Code References Pattern File GIL detection bengal/utils/concurrency/gil.py Parallel rendering, thread-local pipeline bengal/orchestration/render/parallel.py Context propagation to workers bengal/utils/concurrency/context_propagation.py Immutable snapshots bengal/snapshots/types.py Rendering (asset ContextVar) bengal/orchestration/build/rendering.py EffectTracer (lock-serialized) bengal/effects/tracer.py Thread-safe LRUCache bengal/utils/primitives/lru_cache.py Plugin registry (freeze pattern) bengal/plugins/registry.py See Also Large Site Optimization — Free-threading notes and scaling Performance Overview — Incremental and parallel strategies Bengal source -------------------------------------------------------------------------------- Metadata: - Author: lbliii - Word Count: 494 - Reading Time: 2 minutes