Creating a Production Health Monitor for .NET MAUI Apps
๐ฉบ Creating a Production Health Monitor for .NET MAUI Apps
A mobile application can be running without actually being healthy.
The process may still be alive. The UI may still respond. The application may not have crashed. Yet critical parts of the system could already be degraded:
- The API is unreachable.
- Authentication tokens can no longer be refreshed.
- The local database is failing.
- Background synchronization has stopped.
- A queue is continuously growing.
- Secure storage is unavailable.
- Network requests are timing out.
- Memory consumption is increasing.
- A critical dependency has entered a degraded state.
- The application has accumulated unsynchronized operations.
- Startup initialization partially failed.
- A service is repeatedly throwing exceptions without crashing the process.
Traditional crash reporting won't necessarily tell us about any of these conditions.
For production applications, "the app didn't crash" is not the same thing as "the app is healthy." Backend developers are familiar with health endpoints such as:
/health
/health/live
/health/ready
ASP.NET Core applications can expose health checks that report whether databases, caches, queues, APIs, and other dependencies are operational.
Mobile applications need a somewhat different model.
A .NET MAUI application isn't a continuously running server waiting for Kubernetes to probe it every few seconds. It moves between foreground and background states, experiences intermittent connectivity, operates on battery-powered devices, and frequently depends on remote services that may temporarily disappear.
But the underlying concept is still extremely valuable.
We can build a Production Health Monitor that continuously understands the operational condition of the application and its critical dependencies.
Instead of asking only:
Did the application crash?
we can ask:
Is the application operational?
Which subsystem is degraded?
When did it start failing?
How many times has it failed?
Can the application recover automatically?
Should we expose this condition to the user?
Should diagnostics record it?
In this article, we'll design a reusable health monitoring architecture for .NET MAUI that can observe application services, aggregate their status, detect degradation, and provide useful diagnostics without turning the mobile application into an expensive background monitoring system. ๐
๐ Table of Contents
- What Does "Healthy" Mean for a Mobile App?
- Why Crash Reporting Isn't Enough
- Health vs Availability vs Connectivity
- Designing the Health Monitoring Architecture
- Defining Health States
- Creating a Health Check Result
- Designing the Health Check Contract
- Building the Health Monitor
- Aggregating Application Health
- Registering Health Checks
- Monitoring API Connectivity
- Monitoring Authentication
- Monitoring the Local Database
- Monitoring Background Synchronization
- Monitoring Persistent Queues
- Monitoring Storage
- Monitoring Memory
- Tracking Application Initialization
- Timeouts and Cancellation
- Preventing Health Checks from Becoming Expensive
- Health Check Scheduling
- Foreground and Background Awareness
- Consecutive Failure Detection
- Avoiding False Degradation
- Health State Transitions
- Self-Healing Services
- Exposing Health to the UI
- Building a Diagnostic Dashboard
- Structured Logging
- Telemetry and Metrics
- Privacy and Security
- Dependency Injection
- Testing Health Checks
- Testing Failure Scenarios
- Production Architecture
- Common Mistakes
- Best Practices
- Conclusion
1. ๐ฉบ What Does "Healthy" Mean for a Mobile App?
Application health is not a single boolean. This model is usually too simplistic:
bool IsHealthy;
Imagine an application where:
UI Healthy
Local database Healthy
Authentication Healthy
Remote API Unreachable
Synchronization Degraded
Telemetry Healthy
Is the application healthy?
The answer depends on what functionality the user needs.
If the application supports offline operation, losing the API might not make the application unusable.
Instead, the application might be:
Degraded
Another application may depend completely on the API and therefore consider the same condition:
Unhealthy
Application health should therefore be modeled as aggregated operational state rather than a binary flag.
2. ๐ฅ Why Crash Reporting Isn't Enough
Crash reporting answers an important question:
Why did the process terminate unexpectedly?
But many production failures don't terminate the process. Consider:
try
{
await synchronizationService.SyncAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "Synchronization failed.");
}
The application survives.
From a crash-reporting perspective:
Everything is fine.
Operationally:
Synchronization may have been broken for three days.
Another example:
Local queue: 4,283 pending operations
Oldest operation: 19 hours
Last successful synchronization: 22 hours ago
No crash occurred.
But something is clearly wrong.
A health monitor provides visibility into these silent failures.
3. ๐ Health vs Availability vs Connectivity
These concepts shouldn't be treated as equivalent.
Connectivity
Connectivity answers:
Does the device appear to have network access?
In MAUI, connectivity information can help determine the current network state. But:
Internet available
does not imply:
Your API is available
The device may have Wi-Fi while:
- DNS resolution fails.
- The API is down.
- TLS negotiation fails.
- A firewall blocks the service.
- The API returns
503. - Authentication fails.
Availability
Availability answers:
Can a particular dependency currently be reached?
For example:
API โ Available
Health
Health asks a broader question:
Can this subsystem currently perform its responsibility correctly?
An API might respond successfully while authentication is broken.
A database connection might open successfully while writes fail because the disk is full.
A synchronization service might technically execute while every operation is rejected.
Therefore:
Connectivity โ Availability โ Health
A production health monitor should understand these differences.
4. ๐๏ธ Designing the Health Monitoring Architecture
We'll build the system around independent health checks.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Health Monitor โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโผโโโโโโโโโ
โ โ โ
โผ โผ โผ
API Database Auth
Check Check Check
โ โ โ
โโโโโโโโโโผโโโโโโโโโ
โ
โผ
Health Aggregator
โ
โผ
Application Health
โ
โโโโโโโโโผโโโโโโโโโ
โผ โผ โผ
UI Logs Telemetry
Each health check owns knowledge of one subsystem.
The monitor coordinates execution.
The aggregator determines the overall state.
Consumers don't need to understand how every check works.
5. ๐ฆ Defining Health States
Let's start with an explicit health status.
public enum HealthStatus
{
Healthy,
Degraded,
Unhealthy,
Unknown
}
Each state has a specific meaning.
| Status | Meaning |
|---|---|
Healthy | Component is operating normally |
Degraded | Component works, but with reduced capability or elevated risk |
Unhealthy | Component cannot perform its required responsibility |
Unknown | Health has not yet been determined |
Unknown is important. At startup, before checks execute, reporting:
Healthy
would be misleading.
We simply don't know yet.
6. ๐ฆ Creating a Health Check Result
Each health check should return structured information.
public sealed record HealthCheckResult(
string Name,
HealthStatus Status,
string? Description = null,
Exception? Exception = null,
TimeSpan? Duration = null,
DateTimeOffset? CheckedAt = null);
Example:
new HealthCheckResult(
Name: "Remote API",
Status: HealthStatus.Degraded,
Description: "API latency exceeded threshold.",
Duration: TimeSpan.FromSeconds(4.2),
CheckedAt: DateTimeOffset.UtcNow);
This is significantly more useful than:
false
The system knows:
What failed?
How badly?
Why?
How long did the check take?
When was the check executed?
7. ๐งฉ Designing the Health Check Contract
Each subsystem implements the same interface.
public interface IAppHealthCheck
{
string Name { get; }
Task<HealthCheckResult> CheckAsync(
CancellationToken cancellationToken = default);
}
This gives us a plug-in-like architecture. We can add:
ApiHealthCheck
DatabaseHealthCheck
AuthenticationHealthCheck
SynchronizationHealthCheck
QueueHealthCheck
StorageHealthCheck
without changing the monitor itself.
8. ๐ฉบ Building the Health Monitor
Now define the central coordinator.
public interface IAppHealthMonitor
{
Task<ApplicationHealthSnapshot> CheckAsync(
CancellationToken cancellationToken = default);
}
The snapshot represents the complete health state at a particular point in time.
public sealed record ApplicationHealthSnapshot(
HealthStatus Status,
IReadOnlyList<HealthCheckResult> Checks,
DateTimeOffset Timestamp,
TimeSpan Duration);
The implementation can execute registered checks and aggregate their results.
public sealed class AppHealthMonitor : IAppHealthMonitor
{
private readonly IEnumerable<IAppHealthCheck> _checks;
public AppHealthMonitor(
IEnumerable<IAppHealthCheck> checks)
{
_checks = checks;
}
public async Task<ApplicationHealthSnapshot> CheckAsync(
CancellationToken cancellationToken = default)
{
var startedAt = DateTimeOffset.UtcNow;
var tasks = _checks.Select(
check => ExecuteSafelyAsync(
check,
cancellationToken));
var results = await Task.WhenAll(tasks);
var status = Aggregate(results);
return new ApplicationHealthSnapshot(
status,
results,
DateTimeOffset.UtcNow,
DateTimeOffset.UtcNow - startedAt);
}
private static async Task<HealthCheckResult> ExecuteSafelyAsync(
IAppHealthCheck check,
CancellationToken cancellationToken)
{
var startedAt = DateTimeOffset.UtcNow;
try
{
return await check.CheckAsync(
cancellationToken);
}
catch (OperationCanceledException)
when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
return new HealthCheckResult(
check.Name,
HealthStatus.Unhealthy,
"Health check failed unexpectedly.",
ex,
DateTimeOffset.UtcNow - startedAt,
DateTimeOffset.UtcNow);
}
}
private static HealthStatus Aggregate(
IReadOnlyCollection<HealthCheckResult> results)
{
if (results.Count == 0)
return HealthStatus.Unknown;
if (results.Any(
x => x.Status == HealthStatus.Unhealthy))
{
return HealthStatus.Unhealthy;
}
if (results.Any(
x => x.Status == HealthStatus.Degraded))
{
return HealthStatus.Degraded;
}
if (results.All(
x => x.Status == HealthStatus.Healthy))
{
return HealthStatus.Healthy;
}
return HealthStatus.Unknown;
}
}
We now have a basic health monitoring engine.
9. ๐งฎ Aggregating Application Health
The simplest aggregation policy is:
Any Unhealthy
โ
โผ
Application Unhealthy
Else any Degraded
โ
โผ
Application Degraded
Else all Healthy
โ
โผ
Application Healthy
But this may be too aggressive.
Imagine:
Critical API Healthy
Database Healthy
Authentication Healthy
Analytics Unhealthy
Should analytics make the entire application unhealthy?
Probably not.
We need criticality.
10. ๐ฏ Health Check Criticality
Add:
public enum HealthCheckCriticality
{
Critical,
Important,
Optional
}
Then extend the contract:
public interface IAppHealthCheck
{
string Name { get; }
HealthCheckCriticality Criticality { get; }
Task<HealthCheckResult> CheckAsync(
CancellationToken cancellationToken = default);
}
Now:
Database Critical
Authentication Critical
API Critical
Sync Important
Telemetry Optional
Analytics Optional
Aggregation can take business importance into account.
An optional analytics outage might produce:
Overall: Degraded
rather than:
Overall: Unhealthy
This distinction matters in production systems.
11. ๐ Monitoring API Connectivity
A remote API is one of the most obvious health checks. Suppose the backend exposes:
GET /health
We can create:
public sealed class ApiHealthCheck : IAppHealthCheck
{
private readonly HttpClient _httpClient;
public string Name => "Remote API";
public HealthCheckCriticality Criticality =>
HealthCheckCriticality.Critical;
public ApiHealthCheck(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<HealthCheckResult> CheckAsync(
CancellationToken cancellationToken = default)
{
var started = DateTimeOffset.UtcNow;
using var response =
await _httpClient.GetAsync(
"health",
cancellationToken);
var duration =
DateTimeOffset.UtcNow - started;
if (!response.IsSuccessStatusCode)
{
return new HealthCheckResult(
Name,
HealthStatus.Unhealthy,
$"API returned {(int)response.StatusCode}.",
Duration: duration,
CheckedAt: DateTimeOffset.UtcNow);
}
if (duration > TimeSpan.FromSeconds(3))
{
return new HealthCheckResult(
Name,
HealthStatus.Degraded,
"API response exceeded latency threshold.",
Duration: duration,
CheckedAt: DateTimeOffset.UtcNow);
}
return new HealthCheckResult(
Name,
HealthStatus.Healthy,
"API is reachable.",
Duration: duration,
CheckedAt: DateTimeOffset.UtcNow);
}
}
Now API health includes both availability and latency.
12. โฑ๏ธ Latency as a Health Signal
An API doesn't need to be completely unavailable to damage the user experience.
Consider:
| Latency | State |
|---|---|
| 180 ms | Healthy |
| 650 ms | Healthy |
| 1.8 s | Degraded |
| 7 s | Severely degraded |
| Timeout | Unhealthy |
The thresholds depend on the application.
The important point is that health can represent quality of service, not only success/failure.
13. ๐ Monitoring Authentication
Authentication health can be subtle.
A user might currently possess a valid access token, while token refresh is broken. Everything works until:
Access token expires
Then every authenticated operation suddenly fails.
A health check might inspect:
Current authentication state
Token expiration
Refresh capability
Credential availability
For example:
public sealed class AuthenticationHealthCheck
: IAppHealthCheck
{
private readonly IAuthenticationService _authentication;
public string Name => "Authentication";
public HealthCheckCriticality Criticality =>
HealthCheckCriticality.Critical;
public AuthenticationHealthCheck(
IAuthenticationService authentication)
{
_authentication = authentication;
}
public Task<HealthCheckResult> CheckAsync(
CancellationToken cancellationToken = default)
{
if (!_authentication.IsAuthenticated)
{
return Task.FromResult(
new HealthCheckResult(
Name,
HealthStatus.Healthy,
"No authenticated session is active.",
CheckedAt: DateTimeOffset.UtcNow));
}
if (!_authentication.HasUsableCredentials)
{
return Task.FromResult(
new HealthCheckResult(
Name,
HealthStatus.Unhealthy,
"Authenticated session has no usable credentials.",
CheckedAt: DateTimeOffset.UtcNow));
}
return Task.FromResult(
new HealthCheckResult(
Name,
HealthStatus.Healthy,
"Authentication state is valid.",
CheckedAt: DateTimeOffset.UtcNow));
}
}
Notice an important distinction:
Signed out
isn't automatically an unhealthy state.
It may simply be a valid application state.
Health should represent system failure, not normal business state.
14. ๐๏ธ Monitoring the Local Database
Mobile applications frequently depend on SQLite or another persistent local store. A simplistic database check might only verify:
Can I open the database?
A more meaningful check may inspect:
Database initialized?
Schema version valid?
Simple read succeeds?
Database writable?
Migration pending?
Last database failure?
Example:
public sealed class DatabaseHealthCheck
: IAppHealthCheck
{
private readonly ILocalDatabase _database;
public string Name => "Local Database";
public HealthCheckCriticality Criticality =>
HealthCheckCriticality.Critical;
public DatabaseHealthCheck(
ILocalDatabase database)
{
_database = database;
}
public async Task<HealthCheckResult> CheckAsync(
CancellationToken cancellationToken = default)
{
try
{
await _database.VerifyAsync(
cancellationToken);
return new HealthCheckResult(
Name,
HealthStatus.Healthy,
"Local database is operational.",
CheckedAt: DateTimeOffset.UtcNow);
}
catch (Exception ex)
{
return new HealthCheckResult(
Name,
HealthStatus.Unhealthy,
"Local database verification failed.",
ex,
CheckedAt: DateTimeOffset.UtcNow);
}
}
}
The verification should be lightweight. Don't run:
VACUUM
Full integrity scan
Large query
Database migration
every time the health monitor runs.
A health check should observe the system, not become one of its biggest workloads.
15. ๐ Monitoring Background Synchronization
Synchronization is an excellent example of something that can fail silently. Instead of executing a new synchronization just to determine health, inspect its operational history.
public interface ISynchronizationDiagnostics
{
DateTimeOffset? LastAttemptAt { get; }
DateTimeOffset? LastSuccessfulSyncAt { get; }
Exception? LastError { get; }
bool IsRunning { get; }
}
Then:
public sealed class SynchronizationHealthCheck
: IAppHealthCheck
{
private readonly ISynchronizationDiagnostics _diagnostics;
public string Name => "Synchronization";
public HealthCheckCriticality Criticality =>
HealthCheckCriticality.Important;
public SynchronizationHealthCheck(
ISynchronizationDiagnostics diagnostics)
{
_diagnostics = diagnostics;
}
public Task<HealthCheckResult> CheckAsync(
CancellationToken cancellationToken = default)
{
var lastSuccess =
_diagnostics.LastSuccessfulSyncAt;
if (lastSuccess is null)
{
return Task.FromResult(
new HealthCheckResult(
Name,
HealthStatus.Unknown,
"No synchronization has completed yet.",
CheckedAt: DateTimeOffset.UtcNow));
}
var age =
DateTimeOffset.UtcNow - lastSuccess.Value;
if (age > TimeSpan.FromHours(24))
{
return Task.FromResult(
new HealthCheckResult(
Name,
HealthStatus.Unhealthy,
"Synchronization has not succeeded in 24 hours.",
CheckedAt: DateTimeOffset.UtcNow));
}
if (age > TimeSpan.FromHours(2))
{
return Task.FromResult(
new HealthCheckResult(
Name,
HealthStatus.Degraded,
"Synchronization is behind schedule.",
CheckedAt: DateTimeOffset.UtcNow));
}
return Task.FromResult(
new HealthCheckResult(
Name,
HealthStatus.Healthy,
"Synchronization is current.",
CheckedAt: DateTimeOffset.UtcNow));
}
}
This is far cheaper than performing network activity during every check.
16. ๐ฌ Monitoring Persistent Queues
Offline-first applications often maintain queues such as:
Pending API operations
Pending uploads
Telemetry
Synchronization commands
Background jobs
Queue depth itself can become a health signal. Imagine:
Pending operations: 5
Oldest: 30 seconds
Probably healthy. But:
Pending operations: 7,842
Oldest: 36 hours
That's operationally significant.
Define diagnostics:
public interface IQueueDiagnostics
{
int PendingCount { get; }
DateTimeOffset? OldestItemCreatedAt { get; }
DateTimeOffset? LastSuccessfulProcessingAt { get; }
}
A queue health check can then classify the state based on:
Queue depth
Age of oldest item
Last successful processing
Repeated failures
This is much more useful than checking only whether the queue object exists.
17. ๐พ Monitoring Storage
Storage-related failures are especially dangerous because they can appear unexpectedly. Possible conditions include:
Insufficient free space
Unable to create files
Unable to update cache
Corrupted persistent state
Permission failures
But don't write large files just to test storage.
A health check should be lightweight.
For application-owned directories, a tiny probe can sometimes be appropriate:
var probePath = Path.Combine(
FileSystem.CacheDirectory,
".health-probe");
await File.WriteAllTextAsync(
probePath,
"ok",
cancellationToken);
File.Delete(probePath);
However, even this should not run every few seconds.
Health monitoring must respect the characteristics of mobile devices.
18. ๐ง Monitoring Memory
Memory monitoring is different from server environments.
A mobile application doesn't control the amount of memory the operating system is willing to provide indefinitely. Still, trends can be useful. .NET exposes:
GC.GetTotalMemory(false);
and:
GC.GetGCMemoryInfo();
A diagnostic snapshot might record:
public sealed record MemoryHealthSnapshot(
long ManagedMemoryBytes,
long HeapSizeBytes,
long FragmentedBytes,
int Gen0Collections,
int Gen1Collections,
int Gen2Collections);
But avoid simplistic rules like:
Memory > 200 MB = Unhealthy
Memory behavior varies enormously based on:
Device
Platform
Images
WebViews
Native allocations
Application workload
Memory trends are often more informative than a single threshold.
19. ๐ Tracking Application Initialization
Startup itself can be monitored. Imagine initialization consists of:
Configuration
Database
Authentication
Feature flags
Cache
Background services
Represent each stage:
public sealed record InitializationComponentState(
string Name,
bool Completed,
TimeSpan Duration,
Exception? Error = null);
Then health can distinguish:
Application startup complete
from:
Application UI displayed,
but authentication initialization failed
That distinction is extremely valuable when diagnosing production startup problems.
20. โฑ๏ธ Timeouts Are Mandatory
A health check that hangs indefinitely is worse than a failed health check. Suppose:
await _httpClient.GetAsync("health");
never completes within a useful timeframe.
The monitor itself becomes stuck.
Each check should have a bounded execution time.
One approach is to apply a timeout around individual checks.
public async Task<HealthCheckResult> ExecuteWithTimeoutAsync(
IAppHealthCheck check,
TimeSpan timeout,
CancellationToken cancellationToken)
{
using var timeoutCts =
CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken);
timeoutCts.CancelAfter(timeout);
try
{
return await check.CheckAsync(
timeoutCts.Token);
}
catch (OperationCanceledException)
when (!cancellationToken.IsCancellationRequested)
{
return new HealthCheckResult(
check.Name,
HealthStatus.Unhealthy,
$"Health check exceeded {timeout}.",
CheckedAt: DateTimeOffset.UtcNow);
}
}
Now one broken dependency cannot freeze the complete health evaluation.
21. โก Preventing Health Checks from Becoming Expensive
A health monitor can easily become counterproductive.
Imagine running every 10 seconds:
HTTP request
Database write
Disk write
Authentication refresh
DNS lookup
Queue scan
Memory snapshot
That's not monitoring.
That's a workload generator. ๐
Mobile health checks should favor:
Cached diagnostics
Existing counters
Last-success timestamps
Lightweight probes
Passive observation
over repeatedly exercising every dependency.
A good rule is:
Observe existing application behavior whenever possible. Probe actively only when necessary.
22. ๐ Health Check Scheduling
Different checks need different frequencies.
For example:
| Check | Possible Frequency |
|---|---|
| Connectivity state | Event-driven |
| API | Every few minutes / on demand |
| Authentication | On auth state changes |
| Database | Startup + after database failures |
| Sync | After synchronization attempts |
| Queue | After queue mutations |
| Storage | Occasionally |
| Memory | Diagnostic intervals |
| Initialization | During startup |
This suggests that a single global timer isn't always the best architecture.
Health information can be updated through a combination of:
Periodic checks
Events
Operational observations
Explicit refreshes
23. ๐ฑ Foreground and Background Awareness
Mobile lifecycle matters.
If the app enters the background, continuously executing active health checks may waste battery and may not even be permitted by the platform.
The monitor should understand application state.
Conceptually:
Foreground
โ
โโโ Active monitoring
โ
โผ
Background
โ
โโโ Reduce / pause monitoring
โ
โผ
Foreground
โ
โโโ Refresh important health state
A health monitor isn't a reason to fight the mobile operating system's lifecycle model.
24. ๐ Consecutive Failure Detection
One failed API health check doesn't necessarily mean the API is unhealthy.
Mobile connectivity is noisy.
A request might fail because:
Wi-Fi switched to cellular
VPN reconnected
Device moved between access points
DNS temporarily failed
Radio was waking up
Immediately transitioning:
Healthy โ Unhealthy
may create false alarms.
Instead, track consecutive failures.
public sealed class HealthFailureTracker
{
private int _consecutiveFailures;
public int ConsecutiveFailures =>
_consecutiveFailures;
public void RecordSuccess()
{
_consecutiveFailures = 0;
}
public void RecordFailure()
{
_consecutiveFailures++;
}
}
Then:
1 failure โ Healthy / warning
2 failures โ Degraded
3 failures โ Unhealthy
depending on the subsystem.
25. ๐ Avoiding Health State Flapping
Suppose an API alternates:
Healthy
Unhealthy
Healthy
Unhealthy
Healthy
every few seconds.
If the UI reacts immediately, users may see:
Connected
Offline
Connected
Offline
Connected
This is called state flapping.
We can introduce hysteresis.
For example:
Enter Unhealthy:
3 consecutive failures
Return to Healthy:
2 consecutive successes
Now state transitions require evidence rather than reacting to every isolated observation.
26. ๐ Health State Transitions
Instead of storing only the current state, track transitions.
public sealed record HealthStateTransition(
string Component,
HealthStatus Previous,
HealthStatus Current,
DateTimeOffset Timestamp);
Example:
10:15 API Healthy โ Degraded
10:17 API Degraded โ Unhealthy
10:23 API Unhealthy โ Degraded
10:24 API Degraded โ Healthy
This history is often more useful for diagnostics than the current status alone.
27. ๐งฏ Self-Healing Services
Health monitoring becomes particularly powerful when combined with recovery. Consider:
Health Check
โ
โผ
Authentication unhealthy
โ
โผ
Attempt token refresh
โ
โโโ Success โ Healthy
โ
โโโ Failure โ Require login
Or:
Sync queue stalled
โ
โผ
Restart processor
Or:
WebSocket disconnected
โ
โผ
Reconnect
But keep monitoring and recovery conceptually separate. Avoid:
public Task<HealthCheckResult> CheckAsync(...)
{
// inspect state
// delete cache
// restart services
// refresh tokens
// retry uploads
// rebuild database
}
A better model is:
Monitor
โ
โผ
Detect
โ
โผ
Publish State
โ
โผ
Recovery Policy
โ
โผ
Recover
Health checks should primarily observe.
Recovery services should act.
28. ๐ฅ๏ธ Exposing Health to the UI
Not every health issue should be shown to users. Users don't need to see:
DatabaseHealthCheck = Degraded
They need actionable information:
Some data may be temporarily out of date.
or:
You're offline. Changes will be synchronized when the connection returns.
The UI should translate technical health state into user-facing behavior.
For example:
API Unhealthy
โ
โผ
Offline banner
Sync Degraded
โ
โผ
"Pending synchronization"
Authentication Unhealthy
โ
โผ
Reauthentication flow
Health monitoring should inform UX, not expose implementation details.
29. ๐งฐ Building a Diagnostic Dashboard
For internal builds, support teams, QA, or hidden developer menus, the raw information can be extremely valuable.
Example:
Application Health
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Overall DEGRADED
Remote API HEALTHY
Latency 184 ms
Authentication HEALTHY
Token expires 42 min
Database HEALTHY
Schema v14
Synchronization DEGRADED
Last success 3h 12m ago
Upload Queue HEALTHY
Pending 2
Memory 187 MB
Last check 10:42:18
A diagnostic screen can dramatically reduce troubleshooting time.
Instead of asking:
"Is the app working?"
support can inspect concrete subsystem state.
30. ๐ก Reactive Health Updates
Consumers may need to react when health changes. Define:
public interface IHealthStatePublisher
{
event EventHandler<ApplicationHealthSnapshot>? HealthChanged;
}
Or use your application's preferred messaging/event architecture. Then:
Health Monitor
โ
โโโ UI
โโโ Diagnostics
โโโ Telemetry
โโโ Recovery Coordinator
Avoid tightly coupling every service directly to the monitor.
31. ๐ Structured Logging
Health transitions should be logged as structured events.
_logger.LogWarning(
"Health component {Component} transitioned from {PreviousStatus} to {CurrentStatus}",
component,
previousStatus,
currentStatus);
This is more useful than:
_logger.LogWarning(
"Something seems wrong.");
Useful fields might include:
Component
Status
PreviousStatus
Duration
FailureCount
Timestamp
AppVersion
Platform
32. ๐ Telemetry and Metrics
Health information becomes especially valuable when aggregated across many installations. Imagine observing:
API health failures increased from 0.5% to 31%
after backend deployment.
Or:
Database initialization failures occur primarily
on Android after application upgrade.
Or:
Synchronization degradation increased
after version 4.8.0.
Useful metrics include:
health_check_duration
health_check_failure_count
health_state_transition_count
sync_age
queue_depth
api_latency
initialization_duration
But telemetry must be designed carefully for mobile constraints.
Batch where possible.
Avoid transmitting a network event for every health observation.
33. ๐ Privacy and Security
Health diagnostics can accidentally expose sensitive data. Avoid logging:
Access tokens
Refresh tokens
User passwords
Full API responses
Personally identifiable information
Database records
Secure-storage values
Private URLs containing tokens
Prefer:
Authentication = Unhealthy
Reason = RefreshFailed
instead of:
Refresh token abc123... failed.
Operational visibility should never compromise application security.
34. โ๏ธ Dependency Injection
Register the monitor and checks:
builder.Services.AddSingleton<
IAppHealthMonitor,
AppHealthMonitor>();
builder.Services.AddSingleton<
IAppHealthCheck,
ApiHealthCheck>();
builder.Services.AddSingleton<
IAppHealthCheck,
AuthenticationHealthCheck>();
builder.Services.AddSingleton<
IAppHealthCheck,
DatabaseHealthCheck>();
builder.Services.AddSingleton<
IAppHealthCheck,
SynchronizationHealthCheck>();
Because health monitoring typically represents application-wide state, singleton lifetimes often make sense for the monitor and diagnostic state.
However, health checks should respect the lifetimes of the services they inspect.
Don't accidentally capture transient or scoped resources inside long-lived objects without considering their intended lifecycle.
35. ๐งช Testing Health Checks
Because each health check is an independent service, testing becomes straightforward. Example:
[Fact]
public async Task CheckAsync_WhenDatabaseFails_ReturnsUnhealthy()
{
var database = new FakeDatabase
{
ShouldFail = true
};
var check =
new DatabaseHealthCheck(database);
var result =
await check.CheckAsync();
Assert.Equal(
HealthStatus.Unhealthy,
result.Status);
}
Tests should cover:
Healthy dependency
Slow dependency
Unavailable dependency
Timeout
Cancellation
Unexpected exception
Recovery
36. ๐งช Testing Aggregation
Aggregation logic deserves independent tests. For example:
API Healthy
Database Healthy
Auth Healthy
Expected:
Healthy
Then:
API Degraded
Database Healthy
Auth Healthy
Expected:
Degraded
And:
API Healthy
Database Unhealthy
Auth Healthy
Expected:
Unhealthy
Criticality policies should also be tested explicitly.
37. ๐งช Testing Consecutive Failures
Health transitions are stateful. Test sequences, not only isolated values. For example:
Success
Failure
Success
should perhaps remain:
Healthy
while:
Failure
Failure
Failure
might transition:
Healthy
โ
Degraded
โ
Unhealthy
Then test recovery:
Success
Success
leading back to:
Healthy
This verifies that your anti-flapping policy actually works.
38. ๐ฌ Passive vs Active Health Checks
A useful distinction is between two types of checks.
Active checks
They actively interact with a dependency:
HTTP health request
Database probe
File write
Passive checks
They inspect information already produced by normal application behavior:
Last API failure
Last successful sync
Queue depth
Last token refresh
Initialization state
Passive checks are generally cheaper.
A mature mobile health monitoring architecture should use both.
Health Monitor
โ
โโโโโโโโโโโดโโโโโโโโโโ
โผ โผ
Active Checks Passive Signals
โ โ
API probe Last sync time
DB probe Queue depth
Storage probe Failure counters
39. ๐๏ธ Health Check Policies
As the system grows, configuration becomes useful.
public sealed class HealthCheckOptions
{
public TimeSpan DefaultTimeout { get; init; } =
TimeSpan.FromSeconds(5);
public TimeSpan RefreshInterval { get; init; } =
TimeSpan.FromMinutes(2);
public int FailuresBeforeUnhealthy { get; init; } = 3;
public int SuccessesBeforeHealthy { get; init; } = 2;
}
Then:
builder.Services.Configure<HealthCheckOptions>(
options =>
{
options.DefaultTimeout =
TimeSpan.FromSeconds(4);
options.RefreshInterval =
TimeSpan.FromMinutes(3);
options.FailuresBeforeUnhealthy = 3;
});
Hard-coded operational policy scattered across individual checks becomes difficult to maintain.
40. ๐ท๏ธ Health Check Metadata
Production systems may need additional metadata.
public sealed record HealthCheckDescriptor(
string Name,
HealthCheckCriticality Criticality,
string Category,
TimeSpan Timeout);
Categories could include:
Network
Storage
Security
Synchronization
Infrastructure
Performance
This makes diagnostic presentation easier.
41. ๐ Refreshing Health Safely
Users may manually request a diagnostic refresh.
You don't want five taps to launch five simultaneous health evaluations.
Use synchronization:
private readonly SemaphoreSlim _refreshGate =
new(1, 1);
Then:
public async Task<ApplicationHealthSnapshot> RefreshAsync(
CancellationToken cancellationToken = default)
{
await _refreshGate.WaitAsync(
cancellationToken);
try
{
return await CheckAsync(
cancellationToken);
}
finally
{
_refreshGate.Release();
}
}
Or implement a single-flight strategy where concurrent callers share the same running health evaluation.
The correct strategy depends on your application's requirements.
42. ๐ฆ Caching the Latest Snapshot
Most consumers shouldn't trigger new health checks just because they need the current status. Expose the latest known snapshot:
public interface IAppHealthMonitor
{
ApplicationHealthSnapshot? Current { get; }
Task<ApplicationHealthSnapshot> RefreshAsync(
CancellationToken cancellationToken = default);
}
Then the UI can read:
var health = healthMonitor.Current;
without causing network or disk activity. This separation is important:
Read current state
โ
Run health checks
43. ๐ฑ Lifecycle Integration
A practical strategy might be:
Application starts
โ
โผ
Run critical checks
Application enters foreground
โ
โผ
Refresh stale checks
Normal operation
โ
โผ
Update passive health signals
Dependency failure occurs
โ
โผ
Update affected health state
Application backgrounded
โ
โผ
Pause nonessential periodic checks
This respects mobile lifecycle constraints while maintaining useful health information.
44. ๐ง Health as a State Machine
Once health transitions, failure thresholds, and recovery are introduced, the system begins to resemble a state machine. For a component:
failure
Healthy โโโโโโโโโโโโโโโโบ Degraded
โฒ โ
โ โ failures continue
โ โผ
โโโโโโโโโโโโโโโโโโโ Unhealthy
recovery success
The transition policy can encode:
Healthy โ Degraded
Degraded โ Unhealthy
Unhealthy โ Degraded
Degraded โ Healthy
instead of arbitrarily assigning status after every probe.
This produces more stable operational behavior.
45. ๐งฏ Health Monitoring vs Exception Handling
Health monitoring doesn't replace exception handling.
These solve different problems.
Exception Handling
โ
โผ
What should happen when this operation fails?
Health Monitoring
โ
โผ
What is the operational condition of this subsystem?
Similarly, health monitoring doesn't replace:
Crash reporting
Logging
Telemetry
Retry policies
Circuit breakers
Connectivity detection
It coordinates signals from those systems into an operational picture.
46. ๐งฑ Health Monitoring vs Circuit Breakers
A circuit breaker controls whether operations should continue reaching a failing dependency. A health monitor observes whether that dependency is operational. They can work together.
API failures
โ
โผ
Circuit Breaker Opens
โ
โผ
Health Monitor observes
โ
โผ
API = Degraded / Unhealthy
โ
โผ
UI switches to offline behavior
But avoid making them the same component.
The circuit breaker is an execution policy.
The health monitor is an observability mechanism.
47. ๐ถ Health Monitoring vs Connectivity
Similarly:
Connectivity Service
tells you about the device's network state.
The health monitor can consume that information.
For example:
No network access
โ
โผ
Don't actively probe API
โ
โผ
API status = Unknown / Unavailable due to connectivity
This avoids pointless network calls.
When connectivity returns:
Network restored
โ
โผ
Trigger API health refresh
Event-driven monitoring is often more efficient than constant polling.
48. ๐งฉ Production Health Snapshot
A richer snapshot might eventually look like:
public sealed record ApplicationHealthSnapshot(
HealthStatus Status,
IReadOnlyList<HealthCheckResult> Checks,
DateTimeOffset Timestamp,
TimeSpan Duration,
string ApplicationVersion,
string Platform);
Example:
Application Health Snapshot
Status:
DEGRADED
Platform:
Android
Version:
5.4.2
Checks:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
API
HEALTHY
Latency: 243 ms
Authentication
HEALTHY
Database
HEALTHY
Synchronization
DEGRADED
Last successful sync: 2h 43m
Upload Queue
HEALTHY
Pending: 4
Storage
HEALTHY
This snapshot can support:
Developer diagnostics
Support tooling
Structured logs
Telemetry
User-facing status indicators
from the same underlying model.
49. ๐๏ธ Production Architecture
A complete implementation might look like this:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ .NET MAUI App โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Application Health Monitor โ
โ โ
โ Scheduling โ
โ Timeouts โ
โ Aggregation โ
โ Failure thresholds โ
โ Snapshot cache โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโ
โ โ โ
โผ โผ โผ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ Active โ โ Passive โ โ Lifecycle โ
โ Checks โ โ Diagnostics โ โ Signals โ
โโโโโโโโฌโโโโโโโโ โโโโโโโโฌโโโโโโโโ โโโโโโโโฌโโโโโโโโ
โ โ โ
โผ โผ โผ
API / DB / Sync history / Foreground /
Storage queues / errors Background
โ โ โ
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Health Aggregator โ
โ โ
โ Healthy / Degraded / Unhealthy / Unknown โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโโ
โผ โผ โผ
Diagnostics Logging Telemetry
โ
โผ
User-facing
behavior
The architecture gives us one central view of operational health without forcing every subsystem to know about every other subsystem.
50. โ ๏ธ Common Mistake: Checking Everything Constantly
Don't turn health monitoring into:
while (true)
{
await CheckApi();
await CheckDatabase();
await CheckStorage();
await CheckAuthentication();
await Task.Delay(1000);
}
On a server this would already be questionable.
On a phone it can be disastrous for:
Battery
Network usage
CPU
Storage
Application lifecycle
Prefer intelligent scheduling and passive signals.
51. โ ๏ธ Common Mistake: Treating Offline as Broken
A mobile device going offline is normal. If your application supports offline operation:
No internet
may mean:
Remote services unavailable
Local application healthy
Synchronization deferred
Overall application degraded
rather than:
Application unhealthy
Health should reflect the application's actual operating model.
52. โ ๏ธ Common Mistake: Using Exceptions as the Only Signal
If a synchronization service catches and logs errors internally, the health monitor may never know it's failing. Critical services should expose operational diagnostics. For example:
public interface IServiceDiagnostics
{
DateTimeOffset? LastSuccess { get; }
DateTimeOffset? LastFailure { get; }
Exception? LastException { get; }
int ConsecutiveFailures { get; }
}
Health checks can inspect this state without rerunning the operation.
53. โ ๏ธ Common Mistake: Automatically Repairing Everything
Self-healing sounds attractive.
But uncontrolled recovery loops can make failures worse. Imagine:
Database check fails
โ
โผ
Delete database
โ
โผ
Recreate
That would be catastrophic if the local database contained unsynchronized user data.
Recovery actions should be:
Explicit
Bounded
Safe
Observable
Idempotent where possible
and destructive recovery should require very strong guarantees.
54. โ ๏ธ Common Mistake: Showing Technical Health to Users
Don't display:
HealthCheckException:
SQLiteException code 11
to end users.
Separate:
Operational diagnostics
from:
User experience
The health monitor may report:
Database = Unhealthy
while the UI says:
We're having trouble accessing your offline data.
Please try again.
Different audiences require different representations.
55. โ ๏ธ Common Mistake: Health Checks with Side Effects
A health check should generally not:
Modify user data
Trigger synchronization
Refresh authentication
Delete cache
Run migrations
Restart workflows
unless that behavior is explicitly part of a separately defined recovery strategy.
Prefer:
Check โ Observe
Recovery โ Act
This makes the architecture predictable.
56. ๐ Best Practices
When designing production health monitoring for .NET MAUI:
- ๐ฉบ Model health as more than a boolean.
- ๐ฆ Distinguish
Healthy,Degraded,Unhealthy, andUnknown. - ๐งฉ Give each subsystem its own health check.
- ๐ฏ Define component criticality.
- ๐ Don't confuse internet connectivity with API health.
- ๐ Observe latency as well as failures.
- ๐ Prefer passive diagnostics when possible.
- โฑ๏ธ Apply timeouts to active checks.
- ๐ฑ Respect foreground/background lifecycle.
- ๐ Avoid excessive polling.
- ๐ Track consecutive failures.
- ๐ Protect against state flapping.
- ๐งฏ Keep recovery separate from monitoring.
- ๐ฆ Cache the latest health snapshot.
- ๐ Log state transitions rather than endless identical status messages.
- ๐ Never expose credentials or sensitive values in diagnostics.
- ๐งช Test failure sequences, not only happy paths.
- ๐ฌ Monitor queue depth and age in offline-first applications.
- ๐ Track the age of the last successful synchronization.
- ๐๏ธ Treat operational health as part of application architecture.
๐ฏ Conclusion
Production reliability is about much more than preventing crashes.
A .NET MAUI application can remain alive while important parts of the system quietly stop functioning.
The API may become unreachable.
Synchronization may stop progressing.
Authentication may become unrecoverable.
A persistent queue may continue growing.
The database may begin rejecting writes.
Initialization may partially fail.
None of those conditions necessarily produce an immediate crash.
That's why production applications benefit from an explicit model of application health.
Instead of seeing the application as:
Running
or
Crashed
we can understand it as a collection of operational components:
Application
โ
โโโ API
โโโ Authentication
โโโ Database
โโโ Synchronization
โโโ Queues
โโโ Storage
โโโ Runtime diagnostics
Each component contributes health signals.
Those signals become:
Health Checks
โ
โผ
Health Monitor
โ
โผ
Aggregation
โ
โผ
Application Health Snapshot
โ
โโโ UI
โโโ Logging
โโโ Telemetry
โโโ Diagnostics
โโโ Recovery Policies
The result isn't simply another monitoring service.
It's an operational model of the application.
And once that model exists, the application can make better decisions:
Should we continue normally?
Should we switch to offline mode?
Should we defer synchronization?
Should we ask the user to authenticate again?
Should we attempt recovery?
Should we expose a diagnostic warning?
Should telemetry record a degradation event?
A production health monitor gives those decisions a common source of truth.
For small applications, this architecture may be unnecessary.
But as a .NET MAUI application grows into a distributed mobile systemโwith APIs, local databases, authentication, offline synchronization, queues, background work, and external servicesโthe ability to answer "Is the app actually healthy?" becomes an important part of production engineering. ๐ฉบ๐
๐ References
- Microsoft Learn โ .NET MAUI Connectivity https://learn.microsoft.com/dotnet/maui/platform-integration/communication/networking
- Microsoft Learn โ .NET MAUI App Lifecycle https://learn.microsoft.com/dotnet/maui/fundamentals/app-lifecycle
- Microsoft Learn โ Dependency Injection in .NET MAUI https://learn.microsoft.com/dotnet/maui/fundamentals/dependency-injection
- Microsoft Learn โ Logging in .NET https://learn.microsoft.com/dotnet/core/extensions/logging
- Microsoft Learn โ
HttpClientGuidelines https://learn.microsoft.com/dotnet/fundamentals/networking/http/httpclient-guidelines - Microsoft Learn โ Cancellation in Managed Threads https://learn.microsoft.com/dotnet/standard/threading/cancellation-in-managed-threads
- Microsoft Learn โ
SemaphoreSlimhttps://learn.microsoft.com/dotnet/api/system.threading.semaphoreslim - Microsoft Learn โ Garbage Collection Fundamentals https://learn.microsoft.com/dotnet/standard/garbage-collection/fundamentals
- Microsoft Learn โ Garbage Collection Performance https://learn.microsoft.com/dotnet/standard/garbage-collection/performance
- Microsoft Learn โ Health Checks in ASP.NET Core https://learn.microsoft.com/aspnet/core/host-and-deploy/health-checks
- Microsoft Learn โ
GC.GetGCMemoryInfohttps://learn.microsoft.com/dotnet/api/system.gc.getgcmemoryinfo - Microsoft Learn โ
GC.GetTotalMemoryhttps://learn.microsoft.com/dotnet/api/system.gc.gettotalmemory
Was this useful?
Sign in to react. Guest comments are still welcome.




Comments (0)
No approved comments yet.