diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Health/DbHealthProbeActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Health/DbHealthProbeActor.cs
new file mode 100644
index 0000000..2d9c17c
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Health/DbHealthProbeActor.cs
@@ -0,0 +1,58 @@
+using Akka.Actor;
+using Akka.Event;
+using Microsoft.EntityFrameworkCore;
+using ZB.MOM.WW.OtOpcUa.Configuration;
+
+namespace ZB.MOM.WW.OtOpcUa.Runtime.Health;
+
+///
+/// Single-flight cached health probe against the ConfigDb. Reads cached state via
+/// Ask<DbHealthStatus>; a single SELECT 1 runs at most every RefreshInterval.
+/// Consumed by both the host's /health/ready endpoint (Task 54) and
+/// RedundancyStateActor's stale calc.
+///
+public sealed class DbHealthProbeActor : ReceiveActor, IWithTimers
+{
+ public static readonly TimeSpan RefreshInterval = TimeSpan.FromSeconds(5);
+
+ public sealed class GetStatus { public static readonly GetStatus Instance = new(); private GetStatus() { } }
+ public sealed record DbHealthStatus(bool Reachable, DateTime AsOfUtc, string? LastError);
+ public sealed class Tick { public static readonly Tick Instance = new(); private Tick() { } }
+
+ private readonly IDbContextFactory _dbFactory;
+ private readonly ILoggingAdapter _log = Context.GetLogger();
+ private DbHealthStatus _last = new(false, DateTime.MinValue, "not probed yet");
+
+ public ITimerScheduler Timers { get; set; } = null!;
+
+ public static Props Props(IDbContextFactory dbFactory) =>
+ Akka.Actor.Props.Create(() => new DbHealthProbeActor(dbFactory));
+
+ public DbHealthProbeActor(IDbContextFactory dbFactory)
+ {
+ _dbFactory = dbFactory;
+ Receive(_ => Sender.Tell(_last));
+ Receive(_ => RunProbe());
+ }
+
+ protected override void PreStart()
+ {
+ RunProbe();
+ Timers.StartPeriodicTimer("probe", Tick.Instance, RefreshInterval);
+ }
+
+ private void RunProbe()
+ {
+ try
+ {
+ using var db = _dbFactory.CreateDbContext();
+ _ = db.Deployments.AsNoTracking().Take(1).ToList();
+ _last = new DbHealthStatus(true, DateTime.UtcNow, null);
+ }
+ catch (Exception ex)
+ {
+ _last = new DbHealthStatus(false, DateTime.UtcNow, ex.Message);
+ _log.Warning(ex, "DbHealthProbe: probe failed");
+ }
+ }
+}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Health/PeerOpcUaProbeActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Health/PeerOpcUaProbeActor.cs
new file mode 100644
index 0000000..7e8c157
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Health/PeerOpcUaProbeActor.cs
@@ -0,0 +1,54 @@
+using Akka.Actor;
+using Akka.Cluster.Tools.PublishSubscribe;
+using Akka.Event;
+using ZB.MOM.WW.OtOpcUa.Commons.Types;
+
+namespace ZB.MOM.WW.OtOpcUa.Runtime.Health;
+
+///
+/// Periodically pings a peer node's OPC UA endpoint (opc.tcp://peer:4840) and publishes
+/// the result on the cluster's redundancy-state input topic so the admin RedundancyStateActor
+/// can react. Real OPC UA probe call is staged for follow-up F12.
+///
+public sealed class PeerOpcUaProbeActor : ReceiveActor, IWithTimers
+{
+ // Owned by ControlPlane.Redundancy.RedundancyStateActor; duplicated here to avoid a
+ // Runtime → ControlPlane project reference. Keep both literals in lock-step.
+ public const string RedundancyStateTopic = "redundancy-state";
+
+ public static readonly TimeSpan DefaultProbeInterval = TimeSpan.FromSeconds(10);
+
+ public sealed record OpcUaProbeResult(NodeId NodeId, bool Ok);
+ public sealed class Tick { public static readonly Tick Instance = new(); private Tick() { } }
+
+ private readonly NodeId _peer;
+ private readonly TimeSpan _interval;
+ private readonly Action