diff --git a/CHANGELOG.md b/CHANGELOG.md
index eacb06c1..e8849b32 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
---
+## v26.09.02 (2026-09-10)
+
+### Fixed
+
+- **`ApplicationContext.start()` is idempotent.** The context already tracked `_started` — set at the
+ end of `start()`, cleared at the end of `stop()` — but nothing ever read it, so a second `start()`
+ re-ran the whole pipeline rather than returning. Auto-configurations registered again,
+ `@configuration` classes were processed again, and a second fully-initialised set of singletons was
+ created and **started** beside the first: a second Kafka consumer joining the same group and stealing
+ partitions from the first, a second scheduler firing every `@scheduled` task twice, a second
+ connection pool. Nothing owned the duplicates, so `stop()` disposed one set and leaked the other.
+
+ A double start is easy to reach — an ASGI server that runs the lifespan twice, a reload, a test
+ harness sharing one module-level application across files — and it failed silently, which is the
+ worst property a lifecycle bug can have. Every adapter in the codebase already guards itself this way
+ (`KafkaEventBus.start()` opens with `if self._started: return`); the context now follows its own
+ convention. `stop()` still clears the flag, so a stopped context restarts and rebuilds normally.
+
+---
+
## v26.09.01 (2026-09-09)
Found by building a real service on `26.07.01`. Two defects, both of the same shape: a capability the
diff --git a/README.md b/README.md
index be188bce..400655d2 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
-
+
diff --git a/pyproject.toml b/pyproject.toml
index 5dc9e7b5..bfd6ec6d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ name = "pyfly"
# CalVer YY.MM.PATCH — package metadata uses PEP 440 normalized form (26.5.4);
# git tag, GitHub release and human-readable display use leading-zero form
# (v26.05.04) to match the Java/.NET/Go siblings.
-version = "26.9.1"
+version = "26.9.2"
description = "The official Python implementation of the Firefly Framework — DI, CQRS, EDA, hexagonal architecture, and more."
readme = "README.md"
license = "Apache-2.0"
diff --git a/src/pyfly/__init__.py b/src/pyfly/__init__.py
index 94b20eb2..bc421451 100644
--- a/src/pyfly/__init__.py
+++ b/src/pyfly/__init__.py
@@ -13,4 +13,4 @@
# limitations under the License.
"""PyFly — Enterprise Python Framework."""
-__version__ = "26.09.01"
+__version__ = "26.09.02"
diff --git a/src/pyfly/context/application_context.py b/src/pyfly/context/application_context.py
index 402271a8..952fe23d 100644
--- a/src/pyfly/context/application_context.py
+++ b/src/pyfly/context/application_context.py
@@ -161,7 +161,28 @@ def bean_count(self) -> int:
# ------------------------------------------------------------------
async def start(self) -> None:
- """Start the context: resolve @configuration beans, call lifecycle hooks, publish events."""
+ """Start the context: resolve @configuration beans, call lifecycle hooks, publish events.
+
+ Idempotent. A context that is already started returns immediately, because re-running the
+ pipeline does not refresh the context — it BUILDS A SECOND ONE beside it. Auto-configurations
+ register again, ``@configuration`` classes are processed again, and a second fully-initialised
+ set of singletons is created and started: a second Kafka consumer joins the same group and
+ steals partitions from the first, a second scheduler fires every ``@scheduled`` task twice, a
+ second connection pool opens. Nothing owns the duplicates, so :meth:`stop` disposes one set and
+ leaks the other.
+
+ A double start is easy to reach — an ASGI server that runs the lifespan twice, a reload, a test
+ harness sharing one module-level application across files — and it fails silently, which is the
+ worst property a lifecycle bug can have. Every adapter in this codebase already guards itself
+ the same way (``KafkaEventBus.start()`` opens with ``if self._started: return``); the context
+ now follows its own convention.
+
+ :meth:`stop` clears the flag, so a stopped context can be started again and rebuilds normally.
+ """
+ if self._started:
+ logger.debug("context_start_ignored", extra={"reason": "already started"})
+ return
+
try:
await self._do_start()
except BeanCreationException:
diff --git a/tests/context/test_application_context.py b/tests/context/test_application_context.py
index 15c98ec3..82405506 100644
--- a/tests/context/test_application_context.py
+++ b/tests/context/test_application_context.py
@@ -673,3 +673,77 @@ class NeverService:
await ctx.start()
with pytest.raises(NoSuchBeanError):
ctx.get_bean(NeverService)
+
+
+# ---------------------------------------------------------------------------
+# start() is idempotent
+#
+# The context already tracked `_started` — it was set True at the end of start()
+# and False at the end of stop() — but nothing ever READ it, so a second start()
+# re-ran the entire pipeline: auto-configurations re-registered, @configuration
+# classes re-processed, and a second, fully-initialised set of singletons created
+# beside the first.
+#
+# That is not a test-only concern. The second set is STARTED: a second Kafka
+# consumer joins the same group and steals partitions from the first, a second
+# scheduler fires every @scheduled task twice, a second connection pool opens.
+# Nothing owns the duplicates, so stop() disposes one set and leaks the other.
+# A double start is easy to reach — an ASGI server that runs the lifespan twice,
+# a reload, a test harness that shares one module-level app across files.
+#
+# PyFly's own adapters already guard this way (`KafkaEventBus.start()` opens with
+# `if self._started: return`); the context now follows its own convention.
+# ---------------------------------------------------------------------------
+
+
+@configuration
+class _CountingConfiguration:
+ instances: list[object] = []
+
+ @bean
+ def counted(self) -> "_Counted":
+ made = _Counted()
+ _CountingConfiguration.instances.append(made)
+ return made
+
+
+class _Counted:
+ pass
+
+
+@pytest.mark.asyncio
+async def test_starting_twice_does_not_build_a_second_set_of_singletons() -> None:
+ _CountingConfiguration.instances.clear()
+
+ context = ApplicationContext(Config({}))
+ context.register_bean(_CountingConfiguration)
+
+ await context.start()
+ first = context.get_bean(_Counted)
+ assert len(_CountingConfiguration.instances) == 1
+
+ await context.start() # the second start must do nothing
+
+ assert len(_CountingConfiguration.instances) == 1, (
+ "start() ran the bean pipeline again and built a second singleton"
+ )
+ assert context.get_bean(_Counted) is first, "the container handed back a different instance"
+
+ await context.stop()
+
+
+@pytest.mark.asyncio
+async def test_a_stopped_context_can_be_started_again() -> None:
+ """Idempotence must not turn into a one-shot: stop() clears the flag."""
+ _CountingConfiguration.instances.clear()
+
+ context = ApplicationContext(Config({}))
+ context.register_bean(_CountingConfiguration)
+
+ await context.start()
+ await context.stop()
+ await context.start()
+
+ assert len(_CountingConfiguration.instances) == 2, "a restarted context must rebuild its singletons"
+
+ await context.stop()