diff --git a/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health/ActiveNodeGateEndpointFilter.cs b/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health/ActiveNodeGateEndpointFilter.cs
new file mode 100644
index 0000000..0590798
--- /dev/null
+++ b/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health/ActiveNodeGateEndpointFilter.cs
@@ -0,0 +1,62 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ZB.MOM.WW.Health;
+
+///
+/// Endpoint filter that gates a route to the active node. Resolves
+/// from request services; when it is registered and reports a standby
+/// ( is false ) the request is short-circuited with
+/// HTTP 503 and a Retry-After header. When no gate is registered (non-clustered host / tests)
+/// the request is served, preserving prior behaviour.
+///
+public sealed class ActiveNodeGateEndpointFilter : IEndpointFilter
+{
+ /// Default Retry-After value (seconds) advertised on a standby 503 response.
+ private const int RetryAfterSeconds = 5;
+
+ ///
+ /// Returns 503 (with Retry-After ) when the resolved reports
+ /// a standby node; otherwise delegates to the next filter or endpoint handler.
+ ///
+ /// The endpoint filter invocation context.
+ /// The next filter or endpoint handler in the pipeline.
+ public async ValueTask InvokeAsync(
+ EndpointFilterInvocationContext context,
+ EndpointFilterDelegate next)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+ ArgumentNullException.ThrowIfNull(next);
+
+ var httpContext = context.HttpContext;
+ var gate = httpContext.RequestServices.GetService();
+
+ if (gate is { IsActiveNode: false })
+ {
+ httpContext.Response.Headers.RetryAfter = RetryAfterSeconds.ToString();
+ return Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
+ }
+
+ return await next(context);
+ }
+}
+
+///
+/// Route convention that gates endpoint(s) to the active node, returning 503 on standby nodes.
+///
+public static class ActiveNodeGateExtensions
+{
+ ///
+ /// Applies to the decorated endpoint(s): the route is
+ /// served only when the DI-resolved reports the node active, and
+ /// returns 503 with a Retry-After header when the node is a standby.
+ ///
+ /// The endpoint convention builder to decorate.
+ /// The same for chaining.
+ public static IEndpointConventionBuilder RequireActiveNode(this IEndpointConventionBuilder builder)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ return builder.AddEndpointFilter(new ActiveNodeGateEndpointFilter());
+ }
+}
diff --git a/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health/IActiveNodeGate.cs b/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health/IActiveNodeGate.cs
new file mode 100644
index 0000000..cd74623
--- /dev/null
+++ b/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health/IActiveNodeGate.cs
@@ -0,0 +1,20 @@
+namespace ZB.MOM.WW.Health;
+
+///
+/// Single-property seam: is this node the active / leader node?
+///
+///
+/// Attach to endpoints or route groups via
+/// . A standby node must not serve the
+/// gated routes, so the filter returns HTTP 503 when is false .
+/// The implementation is supplied by the consumer — the ZB.MOM.WW.Health.Akka package ships
+/// AkkaActiveNodeGate for clustered nodes; non-Akka hosts provide their own.
+///
+public interface IActiveNodeGate
+{
+ ///
+ /// true when this node is the active node and may serve gated routes;
+ /// false on a standby node.
+ ///
+ bool IsActiveNode { get; }
+}
diff --git a/ZB.MOM.WW.Health/tests/ZB.MOM.WW.Health.Tests/ActiveNodeGateTests.cs b/ZB.MOM.WW.Health/tests/ZB.MOM.WW.Health.Tests/ActiveNodeGateTests.cs
new file mode 100644
index 0000000..e89db46
--- /dev/null
+++ b/ZB.MOM.WW.Health/tests/ZB.MOM.WW.Health.Tests/ActiveNodeGateTests.cs
@@ -0,0 +1,70 @@
+using System.Net;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using ZB.MOM.WW.Health;
+
+namespace ZB.MOM.WW.Health.Tests;
+
+///
+/// Verifies : a decorated endpoint serves
+/// normally (200) when the resolved reports the node active, and
+/// returns 503 with a Retry-After header when the node is a standby.
+///
+public sealed class ActiveNodeGateTests
+{
+ private sealed class FakeActiveNodeGate : IActiveNodeGate
+ {
+ public bool IsActiveNode { get; set; }
+ }
+
+ private static async Task CallAsync(bool isActive)
+ {
+ var builder = WebApplication.CreateBuilder();
+ builder.WebHost.UseTestServer();
+ builder.Services.AddSingleton(new FakeActiveNodeGate { IsActiveNode = isActive });
+
+ await using var app = builder.Build();
+ app.MapGet("/x", () => "ok").RequireActiveNode();
+ await app.StartAsync();
+
+ var client = app.GetTestClient();
+ return await client.GetAsync("/x");
+ }
+
+ [Fact]
+ public async Task ActiveNode_Returns200()
+ {
+ var response = await CallAsync(isActive: true);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("ok", await response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task StandbyNode_Returns503_WithRetryAfterHeader()
+ {
+ var response = await CallAsync(isActive: false);
+
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
+ Assert.True(
+ response.Headers.Contains("Retry-After"),
+ "Standby response must carry a Retry-After header.");
+ }
+
+ [Fact]
+ public async Task NoGateRegistered_AllowsRequest()
+ {
+ // When no IActiveNodeGate is registered (non-clustered host / tests), the endpoint is served.
+ var builder = WebApplication.CreateBuilder();
+ builder.WebHost.UseTestServer();
+
+ await using var app = builder.Build();
+ app.MapGet("/x", () => "ok").RequireActiveNode();
+ await app.StartAsync();
+
+ var response = await app.GetTestClient().GetAsync("/x");
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+}