I have reviewed hundreds of C# codebases across startups, enterprise systems, and large scale distributed architectures. Different industries, different teams, different domains yet the same harmful patterns show up everywhere. What makes these anti patterns dangerous is not that they break local development. They break production under load in ways that are hard to detect and even harder to debug.
This post is not about stylistic preferences. These are the five C# mistakes that have directly contributed to outages and late night incidents in systems I have worked on.
1 Async void and fire and forget operations
Nothing has caused more subtle production bugs in my career than async void. It looks innocent. It compiles. It even works during happy path testing. But it silently breaks exception propagation and hides failure.
public async void ProcessOrder()
{
await _service.HandleAsync();
}
Here is the real danger:
- Exceptions are not observable
- The caller cannot await completion
- Thread pool starvation happens silently
- Application shutdown may occur mid operation
Years ago a payments system used async void inside a controller to send email notifications. During traffic spikes tens of thousands of emails were silently dropped. Logs captured nothing because void swallows exceptions.
2 Not disposing HttpClient or creating it per request
I still see this anti pattern in new codebases more often than I should.
using var client = new HttpClient();
var response = await client.GetAsync(url);
Creating new HttpClient instances exhausts sockets under load. I have seen APIs die because the TIME_WAIT backlog grew uncontrollably. The correct approach is to use IHttpClientFactory which handles:
- Connection pooling
- DNS refresh
- Handler lifetime rotation
services.AddHttpClient("backend", client =>
{
client.Timeout = TimeSpan.FromSeconds(5);
});
3 Swallowing exceptions and returning default results
This is one of the most harmful patterns for distributed architectures. Developers often swallow exceptions to avoid noisy logs or to simplify error handling.
try
{
return await _service.ProcessAsync();
}
catch
{
return null; // harmless right
}
This breaks observability. Failures vanish. Retries never trigger. Upstream callers treat null as a valid business result. Eventually a downstream invariant breaks and everything fails far away from the origin.
In a message driven system I worked on, a swallowed exception caused a single malformed event to be retried endlessly without ever reaching the dead letter queue. It took hours to identify because no logs existed.
4 Overusing static state and caches without boundaries
Static variables seem convenient. They also break multitenancy, concurrency guarantees, and predictability. The damage usually appears only under load.
public static List<string> CachedUsers = new();
The hidden risks:
- Race conditions
- Thread safety violations
- Cross tenant data leakage
- State persisting across requests unexpectedly
- Unit tests polluting each other
I once saw a multitenant system where tenant A occasionally saw data from tenant B. The cause was a static dictionary caching identity claims. Under concurrency it interleaved entries from different tenants.
5 Mixing synchronous and asynchronous code incorrectly
This is the silent killer of throughput.
Developers often write code like:
var data = _repository.GetAsync().Result; // blocks thread
Blocking on async calls leads to deadlocks especially under ASP.NET Core’s request context. It also:
- reduces throughput significantly
- stalls the thread pool
- causes cascading latency increases
A distributed executor service I worked on once slowed to 5 percent of normal throughput. The root cause was a single .Result inside a retry loop. Under peak load it starved the entire thread pool.
What not to do summary
Here are the habits you should eliminate immediately:
- Never use async void outside UI frameworks
- Never instantiate HttpClient directly
- Never swallow exceptions
- Never rely on static mutable state
- Never block async code with Result or Wait
Final Thoughts
C# gives you a powerful ecosystem for building production grade systems. But these anti patterns quietly erode reliability and performance. Avoiding them is not about writing clean code. It is about preventing outages, maintaining predictable behavior under load, and giving your system the resilience it needs to operate in the real world.