diff --git a/tests/ZB.MOM.WW.ScadaBridge.PerformanceTests/Streaming/SiteStreamThroughputTests.cs b/tests/ZB.MOM.WW.ScadaBridge.PerformanceTests/Streaming/SiteStreamThroughputTests.cs
new file mode 100644
index 00000000..07993754
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.PerformanceTests/Streaming/SiteStreamThroughputTests.cs
@@ -0,0 +1,102 @@
+using System.Diagnostics;
+using Akka.Actor;
+using Microsoft.Extensions.Logging.Abstractions;
+using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
+using ZB.MOM.WW.ScadaBridge.SiteRuntime;
+using ZB.MOM.WW.ScadaBridge.SiteRuntime.Streaming;
+
+namespace ZB.MOM.WW.ScadaBridge.PerformanceTests.Streaming;
+
+///
+/// Throughput measurement for the site-wide attribute/alarm stream
+/// (: Source.ActorRef → BroadcastHub
+/// with per-subscriber buffering, DropHead overflow). Design envelope: this
+/// stream carries every attribute value change on a site; it must comfortably
+/// sustain tens of thousands of events/sec. Thresholds are deliberately
+/// conservative (CI-machine safe) — this test exists to catch order-of-magnitude
+/// regressions, not to benchmark.
+///
+///
+///
+/// This is the first of the two highest-value real measurements arch-review 08 §2.3
+/// called for. It follows the existing HotPathLatencyTests style:
+/// -based, [Trait("Category", "Performance")],
+/// conservative thresholds so CI stays green on slow machines.
+///
+///
+/// It spins up a real (non-clustered) rather than using
+/// Akka.TestKit — the PerformanceTests project references SiteRuntime (which brings
+/// in Akka + Akka.Streams transitively) but not Akka.TestKit.Xunit2, so no csproj
+/// change is required.
+///
+///
+public class SiteStreamThroughputTests
+{
+ private sealed class CountingActor : ReceiveActor
+ {
+ public static long Received;
+
+ public CountingActor()
+ {
+ // The manager Tells the raw ISiteStreamEvent record (AttributeValueChanged)
+ // straight to the subscriber — no wrapper — so we match on the record.
+ Receive(_ => Interlocked.Increment(ref Received));
+ ReceiveAny(_ => { });
+ }
+ }
+
+ [Trait("Category", "Performance")]
+ [Fact]
+ public async Task SiteStream_SustainsAtLeast10kEventsPerSecond_ToOneSubscriber()
+ {
+ const int eventCount = 100_000;
+ CountingActor.Received = 0;
+
+ var system = ActorSystem.Create("SiteStreamThroughputTests");
+ try
+ {
+ // Buffer sized to the whole burst so DropHead should not fire.
+ var manager = new SiteStreamManager(
+ new SiteRuntimeOptions { StreamBufferSize = eventCount },
+ NullLogger.Instance);
+ manager.Initialize(system);
+
+ var counter = system.ActorOf(Props.Create());
+ manager.Subscribe("PerfInstance", counter);
+
+ var sw = Stopwatch.StartNew();
+ for (var i = 0; i < eventCount; i++)
+ {
+ manager.PublishAttributeValueChanged(new AttributeValueChanged(
+ "PerfInstance", "Attr", "Attr", i, "Good", DateTimeOffset.UtcNow));
+ }
+
+ // Drain: wait until the counter goes quiet (no growth across a 200ms window).
+ long last = -1;
+ var deadline = DateTime.UtcNow.AddSeconds(30);
+ while (DateTime.UtcNow < deadline)
+ {
+ var now = Interlocked.Read(ref CountingActor.Received);
+ if (now == last && now > 0) break;
+ last = now;
+ await Task.Delay(200);
+ }
+ sw.Stop();
+
+ var received = Interlocked.Read(ref CountingActor.Received);
+ var eventsPerSecond = received / sw.Elapsed.TotalSeconds;
+
+ // DropHead means under extreme pressure some events may drop; with the
+ // buffer sized to the burst, deliver ratio should be ~1.0.
+ Assert.True(received >= eventCount * 0.9,
+ $"Expected >=90% delivery, got {received}/{eventCount}");
+ Assert.True(eventsPerSecond >= 10_000,
+ $"Expected >=10k events/s sustained, got {eventsPerSecond:F0}/s over {sw.Elapsed.TotalSeconds:F1}s " +
+ $"(delivered {received}/{eventCount})");
+ }
+ finally
+ {
+ await system.Terminate();
+ }
+ }
+}