By mid-2020, .NET Core 3.1 became my default stack for microservices. It’s been stable since December 2019, with LTS support, and ships with tools that make cloud-native work feel less like a fight.

The Worker Service template in 3.1 handles background tasks like message consumers or scheduled jobs. You get dependency injection and logging out of the box, and it deploys cleanly in containers. I’ve used it for event processors that need to gracefully shutdown when a container is killed.

I've seen this in action with a service that processes around 500 events per second, where the Worker Service template saved us from having to write custom code for handling retries and timeouts. For example, I used the Polly library to implement circuit breakers and handle transient faults, which reduced our error rate by about 30%. This was a significant improvement, especially considering the service was running on a cluster with 10 nodes.

gRPC in ASP.NET Core 3.1 feels like a natural fit. I avoid REST between services now—strongly-typed clients from .proto files cut down on boilerplate. The managed Kestrel server runs gRPC without native dependencies, which simplifies Docker builds. In one of my projects, I used gRPC to communicate between a .NET Core service and a Python service, and the performance improvement was noticeable, with latency reduced by about 25% compared to our previous REST-based implementation.

One of the key benefits of using .NET Core 3.1 for microservices is the ability to use tools like Prometheus and Grafana for monitoring. I've used these tools to monitor the performance of my services, and they've been invaluable in helping me identify bottlenecks and areas for improvement. For instance, I used Prometheus to monitor the CPU usage of my services, and I was able to optimize the configuration of my containers to reduce CPU usage by about 20%.

Health checks in 3.1 work well with Kubernetes. I map SQL Server and Redis probes to the /health endpoint. The Degraded status is handy—if a database is slow but up, the pod stays running but alerts my monitoring system.

I've also used the health checks feature to implement a custom probe for a third-party service that didn't have a built-in health check. This was done by creating a custom class that implemented the IHealthCheck interface, and then registering it in the Startup class. This allowed me to monitor the health of the third-party service and take action if it became unavailable.

Docker builds in 3.1 are tighter. I use mcr.microsoft.com/dotnet/aspnet:3.1 as the base, keep the runtime image under 100MB, and split SDK from runtime in multi-stage builds. It’s the leanest setup I’ve found for .NET in production.