15 questions found
How do you implement retry logic with exponential backoff for a background service processing potentially-failing external calls?
Advanced
Combine a retry policy (either hand-rolled or via a library like Polly) wrapping each unit of work with exponentially increasing delays between attempts (e.g., 1s, 2s, 4s, 8s) up to a maximum retry count, distinguishing between transient failures (worth retrying) and permanent failures (should be logged and skipped/dead-lettered) to avoid infinite retry loops on unrecoverable errors.
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) {
await retryPolicy.ExecuteAsync(() => CallExternalApiAsync());
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
Real-world example
A background service integrating with a flaky third-party payment provider uses Polly's exponential backoff to gracefully absorb transient network blips, only escalating to an alert after three consecutive failed attempts.
Common follow-ups: How do you distinguish transient from permanent failures programmatically?;What's a dead-letter queue and how does it complement retry logic?
Diagnostics & Performance;gRPC Services
What is the significance of the order of AddHostedService registrations relative to StartAsync execution, and can you control it?
Intermediate
By default, IHostedService.StartAsync is called for each registered hosted service in registration order, sequentially, before the host considers startup complete -- if one StartAsync throws, subsequent services won't start and the host fails to launch. Since .NET's default behavior runs StartAsync calls sequentially (not concurrently) but BackgroundService's own ExecuteAsync loop runs concurrently once started, you generally shouldn't rely on strict ordering for long-running work, but initial startup sequencing is deterministic based on registration order.
// Order matters for StartAsync sequencing:
builder.Services.AddHostedService<DatabaseMigrationService>(); // runs first, must complete
builder.Services.AddHostedService<CacheWarmupService>(); // runs second, after migration's StartAsync returns
Real-world example
A team ensures database migrations complete before any other background service starts by registering a DatabaseMigrationService first, relying on the Generic Host's sequential StartAsync ordering to enforce this dependency.
Common follow-ups: What happens if StartAsync itself is long-running -- does it block the whole app from starting?;How would you make hosted services start concurrently instead?
Generic Host;Entity Framework Core & Data Access
How do background services interact with health checks to signal their operational status?
Advanced
A background service can expose its health status by implementing a custom IHealthCheck that inspects shared state (like a 'last successful run' timestamp or an internal flag) the background service updates, letting the health check endpoint report unhealthy if the background service has stalled or crashed -- important for orchestrators like Kubernetes to detect and restart a container whose background processing has silently died even if the web server is still responding.
public class QueueProcessorHealthCheck : IHealthCheck {
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken ct) {
var timeSinceLastRun = DateTime.UtcNow - QueueProcessor.LastSuccessfulRun;
return Task.FromResult(timeSinceLastRun < TimeSpan.FromMinutes(5)
? HealthCheckResult.Healthy()
: HealthCheckResult.Unhealthy("Queue processor stalled"));
}
}
builder.Services.AddHealthChecks().AddCheck<QueueProcessorHealthCheck>("queue-processor");
Real-world example
A Kubernetes liveness probe automatically restarts a pod whose background queue processor has been silently stuck for over 5 minutes, detected via a custom health check monitoring the processor's last successful iteration timestamp.
Common follow-ups: How do you distinguish liveness from readiness for a background-service-heavy application?;What's the risk of a health check that's too aggressive about restarting?
Health Checks & Readiness/Liveness Probes;Diagnostics & Performance
How should a BackgroundService handle a fatal startup dependency failure, such as being unable to connect to a required message queue at launch?
Intermediate
If a background service absolutely cannot function without a dependency being available, StartAsync (or the very first check inside ExecuteAsync) should throw, which by default (in .NET 6+, with the appropriate BackgroundServiceExceptionBehavior configured) can bring down the entire host -- appropriate 'fail fast' behavior for critical dependencies, versus implementing a retry-with-backoff connection loop inside ExecuteAsync if brief startup unavailability should be tolerated instead of crashing the whole app.
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
// Fail-fast option: let it throw and crash the host
await _queueClient.ConnectAsync(stoppingToken); // throws if unreachable
// OR resilient option: retry with backoff instead of crashing
await RetryConnectWithBackoffAsync(stoppingToken);
}
Real-world example
A payment-processing worker service is configured to fail fast and crash immediately if it can't reach its message broker at startup, deliberately triggering a container restart and alert rather than silently running in a degraded, non-functional state.
Common follow-ups: How does BackgroundServiceExceptionBehavior.StopHost differ from the default Ignore behavior?;When is 'fail fast' the wrong choice for a background service?
Generic Host;Diagnostics & Performance
What is the difference between IHostedService and BackgroundService, and when would you implement IHostedService directly?
Beginner
IHostedService is the low-level interface with just StartAsync and StopAsync methods -- you implement it directly when you need precise control over startup/shutdown without an inherent long-running loop, such as a service that just registers something once and cleans up once. BackgroundService is a convenience base class implementing IHostedService specifically for the common case of a continuous work loop, handling the StartAsync/StopAsync plumbing for you so you only need to write ExecuteAsync.
// Direct IHostedService: no continuous loop, just start/stop hooks
public class MetricsRegistrationService : IHostedService {
public Task StartAsync(CancellationToken ct) {
MetricsRegistry.Register("my-service");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken ct) {
MetricsRegistry.Unregister("my-service");
return Task.CompletedTask;
}
}
Real-world example
A service that simply registers the application with a service discovery system at startup and deregisters at shutdown implements IHostedService directly, since it has no ongoing loop -- unlike a queue processor, which extends BackgroundService instead.
Common follow-ups: Why does BackgroundService exist if IHostedService already covers this?;Can a class implement both patterns simultaneously?
Generic Host;Worker Services & IHostedService