For my 4rd-year Software Engineering final project at university, I’m building an AI Meeting Assistant.
The core feature is simple: A user uploads a 20-minute audio recording of a meeting. My backend downloads it from Backblaze B2, sends it to an selfhosted transcription service to transcribe the speech, and then passes that text to Google Gemini to generate structured meeting minutes that are sent to you email.
There was just one massive problem: It takes over a minute to process.
If I ran this logic inside my standard API Controller, the user’s browser would just show a spinner for 90 seconds, eventually hitting a 504 Gateway Timeout.
The solution? ASP.NET Core Background Services. Here is how I implemented a background worker to handle the heavy AI lifting, and the 3 massive "Gotchas" I ran into along the way.
To understand why Background Services are necessary, think of a fast-food restaurant:
The Controller (The Cashier): The cashier takes your order, hands the ticket to the kitchen, and gives you a receipt. They don't cook the food. If the cashier goes to the back to grill your burger, the line of customers out the door gets angry.
The BackgroundService (The Kitchen Staff): The kitchen staff doesn't interact with customers. They sit in the back, continuously checking the ticket machine. When a new ticket pops up, they cook the food.
In ASP.NET Core, Controllers are short-lived (created and destroyed for every HTTP request). But a BackgroundService is long-lived. It is created once when the app starts, and runs continuously until the app shuts down.
Building the Transcription Worker
In .NET, creating a background worker is incredibly easy. You just create a class that inherits from BackgroundService and override the ExecuteAsync method.
Here is the exact skeleton of the TranscriptionWorker I built for my project:
public class TranscriptionWorker : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<TranscriptionWorker> _logger;
public TranscriptionWorker(IServiceProvider serviceProvider, ILogger<TranscriptionWorker> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Transcription Background Worker started.");
while (!stoppingToken.IsCancellationRequested)
{
try
{
// 1. Check the database for newly uploaded audio files
using var scope = _serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var pendingAudio = await context.AudioFiles
.FirstOrDefaultAsync(a => a.Status == Status.Pending, stoppingToken);
if (pendingAudio != null)
{
// 2. Hand it off to the processing service (Groq -> Gemini)
var processor = scope.ServiceProvider.GetRequiredService<IAudioProcessingService>();
await processor.ProcessAudioAsync(pendingAudio.Id);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in TranscriptionWorker loop.");
}
// 3. Go to sleep for 30 seconds before checking again
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}
To turn it on, I just registered it in my Program.cs:
builder.Services.AddHostedService<TranscriptionWorker>();
Now, when a user uploads an audio file, my API instantly saves it to the database with a Pending status and replies to the user: "Upload successful! We'll notify you when the minutes are ready." Meanwhile, the worker silently picks it up in the background.
If you are building a .NET app that integrates with AI APIs, file processing, or heavy database tasks, stop doing it in the HTTP request pipeline. Give BackgroundService a try!
Have you used Background Services in your projects? What kind of tasks are you running in the background? Let me know in the comments below!