diff --git a/Directory.Packages.props b/Directory.Packages.props
index d35ce2ad62..5394958e90 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -45,6 +45,7 @@
+
diff --git a/src/Marten.ScaleTesting/Commands/SeedCommand.cs b/src/Marten.ScaleTesting/Commands/SeedCommand.cs
new file mode 100644
index 0000000000..0041e62501
--- /dev/null
+++ b/src/Marten.ScaleTesting/Commands/SeedCommand.cs
@@ -0,0 +1,47 @@
+using JasperFx;
+using JasperFx.CommandLine;
+using Marten.ScaleTesting.Seeding;
+using Spectre.Console;
+
+namespace Marten.ScaleTesting.Commands;
+
+[Description("Seed N tenants × M events each under conjoined multi-tenancy. Idempotent — rerun is a no-op if the target counts are already met.")]
+public sealed class SeedCommand: JasperFxAsyncCommand
+{
+ public override async Task Execute(SeedInput input)
+ {
+ using var host = input.BuildHost();
+ var store = host.DocumentStore();
+
+ if (input.WipeFlag)
+ {
+ AnsiConsole.MarkupLine("[red]--wipe specified — destroying all data before seeding.[/]");
+ await store.Advanced.Clean.CompletelyRemoveAllAsync().ConfigureAwait(false);
+ }
+
+ await store.Storage.ApplyAllConfiguredChangesToDatabaseAsync().ConfigureAwait(false);
+
+ var options = input.ToSeedOptions();
+ var seeder = new EventSeeder(store, options);
+ var report = await seeder.RunAsync(CancellationToken.None).ConfigureAwait(false);
+
+ if (report.AlreadySeeded)
+ {
+ AnsiConsole.MarkupLine("[yellow]Nothing to do — the target tenants already have enough events. Pass --wipe to start over.[/]");
+ return true;
+ }
+
+ AnsiConsole.MarkupLine($"[green]Seed complete.[/]");
+ var table = new Table().AddColumn("Metric").AddColumn(new TableColumn("Value").RightAligned());
+ table.AddRow("Batches written", report.Batches.ToString("N0"));
+ table.AddRow("Events written", report.Events.ToString("N0"));
+ table.AddRow("Elapsed", $"{report.Elapsed.TotalSeconds:N1}s");
+ table.AddRow("Throughput (events/sec)", (report.Events / Math.Max(0.001, report.Elapsed.TotalSeconds)).ToString("N0"));
+ AnsiConsole.Write(table);
+
+ var stats = await store.Advanced.FetchEventStoreStatistics().ConfigureAwait(false);
+ AnsiConsole.MarkupLine($"[blue]Event store now holds {stats.EventCount:N0} events across {stats.StreamCount:N0} streams.[/]");
+
+ return true;
+ }
+}
diff --git a/src/Marten.ScaleTesting/Commands/SeedInput.cs b/src/Marten.ScaleTesting/Commands/SeedInput.cs
new file mode 100644
index 0000000000..cad3a51af6
--- /dev/null
+++ b/src/Marten.ScaleTesting/Commands/SeedInput.cs
@@ -0,0 +1,33 @@
+using JasperFx;
+using JasperFx.CommandLine;
+using Marten.ScaleTesting.Seeding;
+
+namespace Marten.ScaleTesting.Commands;
+
+public sealed class SeedInput: NetCoreInput
+{
+ [Description("Number of tenants to seed under conjoined multi-tenancy. Default: 50.")]
+ public int TenantsFlag { get; set; } = 50;
+
+ [Description("Events per tenant. Default: 400,000 (×50 tenants ≈ 20M events).")]
+ public int EventsPerTenantFlag { get; set; } = 400_000;
+
+ [Description("Number of hash partition buckets for the conjoined tenancy. Default: 8.")]
+ public int BucketsFlag { get; set; } = 8;
+
+ [Description("Parallel writer task count. Default: 8.")]
+ public int WritersFlag { get; set; } = 8;
+
+ [Description("Root seed for deterministic stream generation. Default: 42.")]
+ public int SeedFlag { get; set; } = 42;
+
+ [Description("Wipe the event store schema before seeding. Default: false (idempotent rerun).")]
+ public bool WipeFlag { get; set; }
+
+ public SeedOptions ToSeedOptions() => new(
+ TenantCount: TenantsFlag,
+ EventsPerTenant: EventsPerTenantFlag,
+ HashBuckets: BucketsFlag,
+ WriterTasks: WritersFlag,
+ Seed: SeedFlag);
+}
diff --git a/src/Marten.ScaleTesting/ConnectionSource.cs b/src/Marten.ScaleTesting/ConnectionSource.cs
new file mode 100644
index 0000000000..6baf492bbe
--- /dev/null
+++ b/src/Marten.ScaleTesting/ConnectionSource.cs
@@ -0,0 +1,16 @@
+namespace Marten.ScaleTesting;
+
+///
+/// Mirrors DaemonTests.TeleHealth.ConnectionSource but copy-pasted so the
+/// dev-tool stays self-contained (no ProjectReference back into
+/// DaemonTests). Override at runtime via the marten_testing_database
+/// environment variable, same convention as the test harness.
+///
+internal static class ConnectionSource
+{
+ public const string DefaultConnectionString =
+ "Host=localhost;Port=5432;Database=marten_testing;Username=postgres;password=postgres";
+
+ public static string ConnectionString =>
+ Environment.GetEnvironmentVariable("marten_testing_database") ?? DefaultConnectionString;
+}
diff --git a/src/Marten.ScaleTesting/Domain/Appointments.cs b/src/Marten.ScaleTesting/Domain/Appointments.cs
new file mode 100644
index 0000000000..128b021e08
--- /dev/null
+++ b/src/Marten.ScaleTesting/Domain/Appointments.cs
@@ -0,0 +1,93 @@
+// Lifted from src/DaemonTests/TeleHealth/Appointments.cs (#4666 Phase A).
+// Copy-paste, not ProjectReference — the ScaleTesting harness owns its domain
+// fork so we can extend without disturbing the DaemonTests fixtures.
+using JasperFx.Events;
+using Marten.Events.Aggregation;
+
+namespace Marten.ScaleTesting.Domain;
+
+public record AppointmentRequested(Guid PatientId, string StateCode, string SpecialtyCode);
+public record AppointmentRouted(Guid BoardId, string ReasonCode);
+public record AppointmentExternalIdentifierAssigned(Guid AppointmentId, Guid ExternalId);
+public record ProviderAssigned(Guid ProviderId);
+public record AppointmentStarted;
+public record AppointmentCompleted;
+public record AppointmentEstimated(DateTimeOffset Time);
+public record AppointmentCancelled;
+
+public enum AppointmentStatus
+{
+ Requested,
+ Scheduled,
+ Started,
+ Completed
+}
+
+public class Appointment
+{
+ public Guid Id { get; set; }
+ public long Version { get; set; }
+ public DateTimeOffset Created { get; set; }
+ public string SpecialtyCode { get; set; } = string.Empty;
+ public Licensing? Requirement { get; set; }
+ public AppointmentStatus Status { get; set; }
+ public Guid? ProviderId { get; set; }
+ public DateTimeOffset? EstimatedTime { get; set; }
+ public Guid? BoardId { get; set; }
+ public Guid PatientId { get; set; }
+ public DateTimeOffset? Started { get; set; }
+ public DateTimeOffset? Completed { get; set; }
+}
+
+public partial class AppointmentProjection: SingleStreamProjection
+{
+ public AppointmentProjection()
+ {
+ Options.CacheLimitPerTenant = 1000;
+ }
+
+ public override Appointment? Evolve(Appointment? snapshot, Guid id, IEvent e)
+ {
+ switch (e.Data)
+ {
+ case AppointmentRequested requested:
+ snapshot = new Appointment
+ {
+ Status = AppointmentStatus.Requested,
+ Requirement = new Licensing(requested.SpecialtyCode, requested.StateCode),
+ PatientId = requested.PatientId,
+ Created = e.Timestamp,
+ SpecialtyCode = requested.SpecialtyCode
+ };
+ break;
+
+ case AppointmentRouted routed:
+ snapshot!.BoardId = routed.BoardId;
+ break;
+
+ case ProviderAssigned assigned:
+ snapshot!.ProviderId = assigned.ProviderId;
+ break;
+
+ case AppointmentEstimated estimated:
+ snapshot!.Status = AppointmentStatus.Scheduled;
+ snapshot.EstimatedTime = estimated.Time;
+ break;
+
+ case AppointmentStarted:
+ snapshot!.Status = AppointmentStatus.Started;
+ snapshot.Started = e.Timestamp;
+ break;
+
+ case AppointmentCompleted:
+ snapshot!.Status = AppointmentStatus.Completed;
+ snapshot.Completed = e.Timestamp;
+ break;
+
+ case AppointmentCancelled:
+ return null;
+ }
+
+ return snapshot;
+ }
+}
diff --git a/src/Marten.ScaleTesting/Domain/Boards.cs b/src/Marten.ScaleTesting/Domain/Boards.cs
new file mode 100644
index 0000000000..864a040531
--- /dev/null
+++ b/src/Marten.ScaleTesting/Domain/Boards.cs
@@ -0,0 +1,67 @@
+// Lifted from src/DaemonTests/TeleHealth/Boards.cs (#4666 Phase A).
+using JasperFx.Core;
+
+namespace Marten.ScaleTesting.Domain;
+
+internal interface BoardStateEvent;
+
+public record BoardOpened(string Name, DateOnly Date, DateTimeOffset Opened, string[] StateCodes, string[] SpecialtyCodes): BoardStateEvent;
+public record BoardFinished(DateTimeOffset Timestamp): BoardStateEvent;
+public record BoardClosed(DateTimeOffset Timestamp, string Reason): BoardStateEvent;
+public record ShiftAdded(Guid ShiftId);
+public record AlertRaised(string AlertCode);
+public record AlertCleared(string AlertCode);
+public record ShiftDropped(Guid ShiftId);
+
+public class Board
+{
+ public Board()
+ {
+ }
+
+ public Board(BoardOpened opened)
+ {
+ Name = opened.Name;
+ Activated = opened.Opened;
+ Date = opened.Date;
+
+ SpecialtyCodes = opened.SpecialtyCodes;
+ StateCodes = opened.StateCodes;
+ }
+
+ public void Apply(BoardFinished finished) => Finished = finished.Timestamp;
+
+ public void Apply(BoardClosed closed)
+ {
+ Closed = closed.Timestamp;
+ CloseReason = closed.Reason;
+ }
+
+ public void Apply(ShiftAdded added) => ActiveShifts.Fill(added.ShiftId);
+
+ public void Apply(ShiftDropped dropped) => ActiveShifts.Remove(dropped.ShiftId);
+
+ public void Apply(AlertRaised alert)
+ {
+ AlertCodes = AlertCodes.Concat([alert.AlertCode]).Distinct().ToArray();
+ }
+
+ public void Apply(AlertCleared cleared)
+ {
+ AlertCodes = AlertCodes.Where(x => x != cleared.AlertCode).ToArray();
+ }
+
+ public Guid Id { get; set; }
+ public string Name { get; } = string.Empty;
+ public DateTimeOffset Activated { get; set; }
+ public DateTimeOffset? Finished { get; set; }
+ public DateOnly Date { get; set; }
+ public DateTimeOffset? Closed { get; set; }
+
+ public string[] AlertCodes { get; set; } = [];
+ public string[] StateCodes { get; set; } = [];
+ public string[] SpecialtyCodes { get; set; } = [];
+
+ public string CloseReason { get; private set; } = string.Empty;
+ public List ActiveShifts { get; set; } = new();
+}
diff --git a/src/Marten.ScaleTesting/Domain/ProviderShift.cs b/src/Marten.ScaleTesting/Domain/ProviderShift.cs
new file mode 100644
index 0000000000..0559b1fb49
--- /dev/null
+++ b/src/Marten.ScaleTesting/Domain/ProviderShift.cs
@@ -0,0 +1,91 @@
+// Lifted from src/DaemonTests/TeleHealth/ProviderShift.cs (#4666 Phase A).
+using JasperFx.Events;
+using JasperFx.Events.Grouping;
+using Marten.Events.Aggregation;
+
+namespace Marten.ScaleTesting.Domain;
+
+public class ProviderShift(Guid boardId, Provider provider)
+{
+ public Guid Id { get; set; }
+ public long Version { get; set; }
+ public Guid BoardId { get; private set; } = boardId;
+ public Guid ProviderId => Provider.Id;
+ public ProviderStatus Status { get; set; } = ProviderStatus.Paused;
+ public string Name { get; init; } = string.Empty;
+ public Guid? AppointmentId { get; set; }
+
+ public Provider Provider { get; set; } = provider;
+}
+
+public enum ProviderStatus
+{
+ Ready,
+ Assigned,
+ Charting,
+ Paused
+}
+
+public record ProviderScheduled(Guid ProviderId, DateTimeOffset ExpectedStart);
+public record AppointmentAssigned(Guid AppointmentId);
+public record ProviderJoined(Guid BoardId, Guid ProviderId);
+public record EnhancedProviderJoined(Guid BoardId, Provider Provider);
+public record ProviderReady;
+public record ProviderPaused;
+public record ProviderSignedOff;
+public record ChartingFinished;
+public record ChartingStarted;
+
+public partial class ProviderShiftProjection: SingleStreamProjection
+{
+ public ProviderShiftProjection()
+ {
+ Options.CacheLimitPerTenant = 1000;
+ }
+
+ public override async Task EnrichEventsAsync(SliceGroup group, IQuerySession querySession, CancellationToken cancellation)
+ {
+ await group
+ .EnrichWith()
+ .ForEvent()
+ .ForEntityId(x => x.ProviderId)
+ .EnrichAsync((slice, e, provider) =>
+ {
+ slice.ReplaceEvent(e, new EnhancedProviderJoined(e.Data.BoardId, provider));
+ });
+ }
+
+ public override ProviderShift? Evolve(ProviderShift? snapshot, Guid id, IEvent e)
+ {
+ switch (e.Data)
+ {
+ case EnhancedProviderJoined joined:
+ snapshot = new ProviderShift(joined.BoardId, joined.Provider)
+ {
+ Provider = joined.Provider,
+ Status = ProviderStatus.Ready
+ };
+ break;
+
+ case ProviderReady:
+ snapshot!.Status = ProviderStatus.Ready;
+ break;
+
+ case AppointmentAssigned assigned:
+ snapshot!.Status = ProviderStatus.Assigned;
+ snapshot.AppointmentId = assigned.AppointmentId;
+ break;
+
+ case ProviderPaused:
+ snapshot!.Status = ProviderStatus.Paused;
+ snapshot.AppointmentId = null;
+ break;
+
+ case ChartingStarted:
+ snapshot!.Status = ProviderStatus.Charting;
+ break;
+ }
+
+ return snapshot;
+ }
+}
diff --git a/src/Marten.ScaleTesting/Domain/ReferenceData.cs b/src/Marten.ScaleTesting/Domain/ReferenceData.cs
new file mode 100644
index 0000000000..d87f4cbe51
--- /dev/null
+++ b/src/Marten.ScaleTesting/Domain/ReferenceData.cs
@@ -0,0 +1,46 @@
+// Lifted from src/DaemonTests/TeleHealth/{Patient,Providers,RoutingReason,Specialty}.cs (#4666 Phase A).
+using Marten.Schema;
+
+namespace Marten.ScaleTesting.Domain;
+
+public class Patient
+{
+ public Guid Id { get; set; }
+ public string FirstName { get; set; } = string.Empty;
+ public string LastName { get; set; } = string.Empty;
+}
+
+public record Licensing(string SpecialtyCode, string StateCode);
+
+public enum ProviderRole
+{
+ Physician,
+ PhysicianAssistant,
+ Nurse
+}
+
+public class Provider
+{
+ public Guid Id { get; set; }
+ public string FirstName { get; set; } = string.Empty;
+ public string LastName { get; set; } = string.Empty;
+ public ProviderRole Role { get; set; }
+
+ public List Licensing { get; set; } = [];
+}
+
+public class RoutingReason
+{
+ public Guid Id { get; set; }
+ public string Code { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+ public bool IsActive { get; set; }
+ public int Severity { get; set; }
+}
+
+public class Specialty
+{
+ [Identity]
+ public string Code { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+}
diff --git a/src/Marten.ScaleTesting/Marten.ScaleTesting.csproj b/src/Marten.ScaleTesting/Marten.ScaleTesting.csproj
new file mode 100644
index 0000000000..924579fa26
--- /dev/null
+++ b/src/Marten.ScaleTesting/Marten.ScaleTesting.csproj
@@ -0,0 +1,27 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ Marten.ScaleTesting
+
+ false
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Marten.ScaleTesting/Program.cs b/src/Marten.ScaleTesting/Program.cs
new file mode 100644
index 0000000000..6fb483f844
--- /dev/null
+++ b/src/Marten.ScaleTesting/Program.cs
@@ -0,0 +1,51 @@
+using JasperFx;
+using JasperFx.Events;
+using Marten;
+using Marten.Events;
+using Marten.ScaleTesting;
+using Marten.Storage;
+using Microsoft.Extensions.Hosting;
+
+// #4666 Phase A — host bootstrap. Mirrors the conjoined-multi-tenancy config
+// from src/DaemonTests/Composites/multi_stage_projections.cs:246-254 but
+// keeps the bucket count + tenancy style parametrisable per subcommand.
+//
+// Projections are registered at host build time so the Async daemon (Phase B)
+// has them on hand; Phase A doesn't start the daemon, only seeds events
+// against the same schema shape the rebuild will use.
+
+var builder = Host.CreateDefaultBuilder(args);
+builder.ConfigureServices(services =>
+{
+ services.AddMarten(opts =>
+ {
+ opts.Connection(ConnectionSource.ConnectionString);
+ opts.DisableNpgsqlLogging = true;
+
+ // Quick append is the realistic shape for the high-throughput seed.
+ opts.Events.AppendMode = EventAppendMode.Quick;
+
+ // Conjoined tenancy with hash partitioning — bucket count is fixed
+ // at 8 here so the schema is stable across seed/rebuild runs. The
+ // CLI seed subcommand still threads its own bucket value but the
+ // host-level config wins for partition DDL.
+ opts.Events.TenancyStyle = TenancyStyle.Conjoined;
+ opts.Policies.AllDocumentsAreMultiTenantedWithPartitioning(x =>
+ {
+ x.ByHash(Enumerable.Range(1, 8).Select(i => $"b_{i}").ToArray());
+ });
+ opts.Advanced.DefaultTenantUsageEnabled = false;
+
+ // Phase A intentionally does NOT register the snapshot projections —
+ // the seeder only writes raw events. Registering Snapshot would
+ // require the JasperFx.Events.SourceGenerator to emit the dispatcher
+ // for each aggregate's partial class, which is fine when we wire up
+ // the composite projection in Phase B but is dead weight for a
+ // pure-seeding run. Without registration, StartStream on the
+ // seeder just tags the stream with the aggregate type name; that
+ // doesn't trigger any projection machinery and Phase B's rebuild
+ // builds the snapshots from the seeded events.
+ });
+});
+
+return await builder.RunJasperFxCommands(args).ConfigureAwait(false);
diff --git a/src/Marten.ScaleTesting/README.md b/src/Marten.ScaleTesting/README.md
new file mode 100644
index 0000000000..0f693360df
--- /dev/null
+++ b/src/Marten.ScaleTesting/README.md
@@ -0,0 +1,49 @@
+# Marten.ScaleTesting
+
+Long-running load test harness for Marten 9's async daemon projection rebuilds
+([#4666](https://github.com/JasperFx/marten/issues/4666)). Interactive / dev-box
+only — **not** packaged, **not** wired into CI.
+
+Drives the daemon-thread-safety synthesis fixes (#4657 → #4658 → #4667 phases)
+against realistic conjoined-multi-tenant event interleaving at the 20M+ event
+scale, beyond what unit-test fixtures cover.
+
+## Usage
+
+```bash
+# Seed N tenants × M events per tenant under conjoined multi-tenancy
+dotnet run --project src/Marten.ScaleTesting -- seed --tenants 50 --events-per-tenant 400000 --buckets 8 --seed 42
+
+# Rebuild the TeleHealth composite projection against the seeded data
+# (Phase B — not yet implemented)
+dotnet run --project src/Marten.ScaleTesting -- rebuild --projection composite --report metrics.json
+
+# Validate aggregates against a single-shard baseline (Phase C — not yet implemented)
+dotnet run --project src/Marten.ScaleTesting -- validate --baseline baseline.json
+```
+
+Connect string defaults to the standard Marten test connection
+(`Host=localhost;Port=5432;Database=marten_testing;Username=postgres;password=postgres`).
+Override with the `marten_testing_database` env var.
+
+## Project layout
+
+| Directory | Contents |
+|---|---|
+| `Domain/` | TeleHealth events / aggregates / reference data **lifted** (copied) from `src/DaemonTests/TeleHealth/`. Self-contained so we can extend without touching test fixtures. |
+| `Seeding/` | Producer-consumer event seeder: per-stream `IEnumerable` generators, weighted-random k-way merge, `Channel` pipeline. |
+| `Commands/` | JasperFx.CommandLine subcommands. Modelled on `src/EventAppenderPerfTester/`. |
+
+## Phases
+
+* **Phase A (this PR):** project scaffold + lifted Telehealth domain + event seeder + `seed` subcommand. Idempotent via `mt_events` row count check.
+* **Phase B:** 4+2+2 composite topology (stage 1: single-stream snapshots + `AppointmentMetricsProjection`; stage 2: `AppointmentDetailsProjection` + `BoardSummaryProjection`; stage 3: NEW `ProviderUtilizationProjection` + `TenantDailyRollupProjection`) + `rebuild` subcommand on the single-pass `CompositeReplayExecutor` path.
+* **Phase C:** `validate` (single-shard baseline diff) + `stress` (chain `seed` + `rebuild` + `validate`) + JSON metrics sink.
+* **Phase D:** use it. Drive the daemon-thread-safety fixes against the harness — each fix should hold the crash gate AND not regress rebuild time.
+
+## Non-goals
+
+* Not a microbenchmark — `src/MartenBenchmarks/` covers per-method timings.
+* Not a NuGet package — internal tool only.
+* Not wired into CI.
+* Not sharded-PG or distributed.
diff --git a/src/Marten.ScaleTesting/Seeding/EventBatch.cs b/src/Marten.ScaleTesting/Seeding/EventBatch.cs
new file mode 100644
index 0000000000..817c4a0b7f
--- /dev/null
+++ b/src/Marten.ScaleTesting/Seeding/EventBatch.cs
@@ -0,0 +1,26 @@
+namespace Marten.ScaleTesting.Seeding;
+
+///
+/// One stream's complete event sequence for a single tenant. Each batch is
+/// the complete contents of a single StartStream<TAggregate> +
+/// SaveChangesAsync call — no inter-batch coordination is required,
+/// so the writer pool can fan out fully in parallel without colliding on the
+/// per-stream version sequence.
+///
+///
+/// Cross-stream interleaving at the mt_events table level still
+/// happens via the producer's draw order: it picks the next stream to emit
+/// using a weighted random across stream types per tenant, then round-robins
+/// across tenants. Writers commit roughly in producer order, so the events
+/// table ends up with the same interleaving shape a real workload exhibits.
+///
+///
+/// Conjoined tenant id; never null.
+/// Stream id; stable so a given stream id always maps to one StartStream call.
+/// Aggregate the stream rolls up to (Appointment / Board / ProviderShift).
+/// The complete event sequence for the stream, in append order.
+internal sealed record EventBatch(
+ string TenantId,
+ Guid StreamId,
+ Type AggregateType,
+ IReadOnlyList