Sending newsletters is a classic background task. You don’t want email delivery to block HTTP requests, slow down users, or fail silently when traffic spikes. This is where a .NET Worker Service fits perfectly.
In this article, we’ll build a Newsletter Worker Service using .NET that runs independently, processes newsletter jobs in the background, and can be hosted as a Windows Service, Linux daemon, or Docker container.
Why Use a Worker Service for Newsletters?
Sending newsletters inside a controller or API call is risky:
- Email sending is slow and I/O heavy
- Failures are hard to retry
- App restarts can interrupt delivery
- Scaling becomes painful
A Worker Service solves this by:
- Running independently of web requests
- Processing jobs asynchronously
- Supporting retries, logging, and graceful shutdown
- Scaling horizontally
What Is a .NET Worker Service?
A Worker Service is a long-running background process built on
IHostedService.
Typical use cases:
- Email & notification sending
- Queue consumers
- Scheduled jobs
- Data sync
- Report generation
It runs using the Generic Host, same infrastructure used by ASP.NET Core.
Architecture Overview
Simple Newsletter Flow
- Web App stores newsletter job in DB / Queue
- Worker Service polls or listens
- Worker sends emails
- Status updated (Sent / Failed / Retried)
Web App → Database / Queue → Worker Service → SMTP / Email API
Step 1: Create a Worker Service Project
dotnet new worker -n Newsletter.Worker
cd Newsletter.Worker
Project structure:
Newsletter.Worker
├── Program.cs
├── Worker.cs
├── appsettings.json
Step 2: Configure the Host
Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
services.AddSingleton<INewsletterService, NewsletterService>();
})
.Build();
await host.RunAsync();
Step 3: Implement the Worker
Worker.cs
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly INewsletterService _newsletterService;
public Worker(
ILogger<Worker> logger,
INewsletterService newsletterService)
{
_logger = logger;
_newsletterService = newsletterService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Newsletter Worker started.");
while (!stoppingToken.IsCancellationRequested)
{
try
{
await _newsletterService.ProcessPendingNewslettersAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while processing newsletters");
}
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
_logger.LogInformation("Newsletter Worker stopping.");
}
}
Step 4: Newsletter Service Logic
INewsletterService.cs
public interface INewsletterService
{
Task ProcessPendingNewslettersAsync(CancellationToken token);
}
NewsletterService.cs
public class NewsletterService : INewsletterService
{
public async Task ProcessPendingNewslettersAsync(CancellationToken token)
{
var newsletters = GetPendingNewsletters();
foreach (var newsletter in newsletters)
{
if (token.IsCancellationRequested)
break;
try
{
await SendEmailAsync(newsletter);
MarkAsSent(newsletter.Id);
}
catch
{
MarkAsFailed(newsletter.Id);
}
}
}
private List<Newsletter> GetPendingNewsletters()
{
// Fetch from DB or queue
return new List<Newsletter>();
}
private Task SendEmailAsync(Newsletter newsletter)
{
// SMTP / SendGrid / SES logic
return Task.CompletedTask;
}
private void MarkAsSent(int id) { }
private void MarkAsFailed(int id) { }
}
Step 5: Email Sending Strategy
You can plug in:
- SMTP (
System.Net.Mail) - SendGrid
- Amazon SES
- Azure Communication Services
Always:
- Use async APIs
- Set timeouts
- Log failures
- Avoid blocking calls
Step 6: Configuration via appsettings.json
{
"Worker": {
"IntervalSeconds": 30
},
"Smtp": {
"Host": "smtp.example.com",
"Port": 587,
"Username": "user",
"Password": "password"
}
}
Bind configs using IOptions<T> for clean code.
Step 7: Logging & Observability
Use structured logging:
_logger.LogInformation(
"Sending newsletter {NewsletterId} to {Email}",
newsletter.Id,
newsletter.Email);
Recommended:
- Serilog
- Seq / ELK
- Application Insights
Step 8: Graceful Shutdown
Worker services automatically handle shutdown signals:
CTRL + C- Windows Service stop
- Docker stop
Using CancellationToken ensures:
- No half-sent emails
- Clean exit
- No data corruption
Step 9: Deployment Options
You can host the worker as:
- Windows Service
- Linux systemd service
- Docker container
- Kubernetes Job / Deployment
- Azure App Service (Worker)
Common Enhancements
- Retry policy (Polly)
- Queue-based processing (RabbitMQ, Azure Service Bus)
- Batch sending
- Rate limiting
- Dead-letter handling
- Health checks
When NOT to Use a Worker Service
- Very small apps with few emails
- Real-time user notifications
- One-off scripts
- In those cases, simpler background jobs may suffice.
Final Thoughts
A .NET Worker Service is a clean, scalable, and production-ready way to handle newsletter delivery. It keeps your web app fast, improves reliability, and gives you full control over retries, failures, and scaling.