Logging in C# can be a double-edged sword. Done right, it illuminates production mysteries; done wrong, it becomes a performance sinkhole and a source of debugging noise. This guide, reflecting widely shared professional practices as of May 2026, identifies the most common logging landmines and provides actionable strategies to sidestep them. We focus on practical, tested approaches—no invented studies, just engineering judgment.
Why Logging Becomes a Nightmare: Performance and Debugging Costs
Many teams treat logging as an afterthought, adding Console.WriteLine or ILogger.LogInformation calls wherever they suspect a bug. This casual approach often backfires: in a typical high-throughput service, each log line can allocate strings, format messages, and block on I/O. Multiply that by thousands of requests per second, and you get CPU spikes, GC pressure, and latency degradation. One team I read about found that their logging code consumed 15% of total CPU time—most of it formatting strings that were never even written to a sink because the log level was filtered out.
The Hidden Cost of String Interpolation
When you write logger.LogInformation($"User {userId} processed order {orderId}"), the string interpolation happens before the log level check. If the minimum log level is Warning, that interpolation is wasted work. The fix is to use structured logging templates: logger.LogInformation("User {UserId} processed order {OrderId}", userId, orderId). This defers formatting until the sink writes the event, and only if the level passes. In practice, this can reduce CPU overhead by 30-50% in high-volume scenarios.
Blocking I/O in the Hot Path
Synchronous file or network I/O inside a log call can stall request threads, especially under load. Many sinks (e.g., Console, File) are synchronous by default. In a web application, this can cause thread pool starvation. The mitigation is to use async logging: configure your sink to write asynchronously (e.g., Serilog's WriteTo.Async(a => a.File(...))). However, async logging adds complexity—events may be lost on process crash if the buffer isn't flushed. Evaluate your reliability requirements before enabling async.
Over-Logging in Loops and High-Frequency Paths
Logging inside a loop that runs thousands of times per second is a common pitfall. Each iteration allocates objects and may flush the sink. A better approach is to aggregate: log a summary after the loop, or use a counter and log only when a threshold is reached. For example, log every 100th iteration or use a sliding window. This reduces volume while still providing visibility.
Core Frameworks: Understanding Log Levels, Sinks, and Structured Logging
To avoid landmines, you need a solid grasp of the building blocks. The three pillars are log levels, sinks (destinations), and structured logging. Each has trade-offs that affect both performance and debuggability.
Log Levels: More Than Just Verbosity
Log levels (Trace, Debug, Information, Warning, Error, Fatal) control filtering. A common mistake is setting the minimum level too low in production (e.g., Debug), which floods sinks and degrades performance. Conversely, setting it too high (Error only) may hide early warnings of failures. Best practice: use Information as default, enable Debug temporarily during troubleshooting, and ensure Trace is never enabled in production without explicit intent. Use appsettings.json to adjust levels per namespace or category.
Sinks: Choosing the Right Destination
Popular sinks include Console (for development), File (for simple apps), Elasticsearch (for centralized logging), and Seq (for structured log analysis). Each has performance characteristics: Console is fast but ephemeral; File can cause disk I/O contention; Elasticsearch adds network latency and indexing overhead. For high-throughput systems, consider a buffered sink or a dedicated log shipper (e.g., Fluentd). The table below compares common sinks:
| Sink | Throughput | Persistence | Searchability | Best For |
|---|---|---|---|---|
| Console | High | None | Low | Development, container stdout |
| File (rolling) | Medium | Good | Low | Single-server apps, compliance |
| Elasticsearch | Low-Medium | Good | High | Distributed systems, analytics |
| Seq | Medium | Good | High | Structured log exploration |
Structured Logging: Why It Matters
Structured logging captures events as key-value pairs, making logs machine-parseable. Instead of a string like "User 42 logged in", you emit { "Event": "UserLoggedIn", "UserId": 42, "Timestamp": "..." }. This enables powerful querying and alerting. However, it introduces overhead: each property must be captured and serialized. Use destructuring (e.g., @ in Serilog) sparingly for complex objects, as it can allocate heavily. Prefer primitive types or simple DTOs.
Execution: Building a Performant Logging Pipeline Step by Step
Here is a repeatable process to design and implement a logging system that avoids common landmines. Follow these steps when setting up a new project or auditing an existing one.
Step 1: Define Logging Requirements
Start by answering: What do you need to know during debugging? What compliance or retention rules apply? For example, a payment service may need to log every transaction for auditing, while a recommendation engine may only need error logs. Document the required log levels per component and the expected volume (e.g., 1000 events/second). This prevents over-engineering.
Step 2: Choose a Logging Framework
Microsoft.Extensions.Logging (MEL) is the default for ASP.NET Core. It provides a facade over third-party providers like Serilog, NLog, or log4net. For new projects, prefer MEL with Serilog as the provider—it offers rich structured logging and a wide sink ecosystem. NLog is a strong alternative with lower overhead. Avoid log4net unless legacy compatibility is required.
Step 3: Configure Sinks and Levels
In appsettings.json, set the default minimum level to Warning or Information. Override per namespace for noisy libraries (e.g., Microsoft.AspNetCore.* to Warning). Use environment-specific configuration: Debug level for development, Information for staging, Warning for production. For sinks, start with Console for development and a rolling file for production. Add a centralized sink (Elasticsearch or Seq) only when you need cross-service correlation.
Step 4: Implement Structured Logging
Use message templates with named placeholders. Avoid interpolated strings. Example: logger.LogInformation("Processing order {OrderId} for customer {CustomerId}", order.Id, customer.Id). For complex objects, use destructuring with @ only when necessary, as it serializes the entire object. Instead, extract only the fields you need. This reduces allocation and serialization cost.
Step 5: Add Async and Batching
Wrap your sink in an async wrapper (e.g., Serilog's WriteTo.Async) to offload I/O to a background thread. Set a reasonable batch size and interval (e.g., 100 events or 5 seconds). This smooths out spikes and reduces thread pool contention. Be aware that events in the buffer may be lost if the process crashes—evaluate if your logs are critical or merely diagnostic.
Step 6: Monitor and Tune
After deployment, monitor log volume and sink throughput. Use application performance monitoring (APM) to detect if logging becomes a bottleneck. Adjust levels and sinks as needed. For example, if file I/O becomes a bottleneck, switch to a network sink or reduce the log level. Regularly review logs to ensure they are actionable—if no one reads them, consider reducing verbosity.
Tools, Stack, and Maintenance Realities
Choosing the right tools and maintaining them over time is crucial. Here we discuss practical considerations for logging infrastructure.
Centralized Logging vs. Local Files
Centralized logging (e.g., Elastic Stack, Seq, Datadog) offers search, alerting, and correlation across services. However, it adds network latency, cost, and operational overhead. For small teams or low-volume apps, local rolling files may suffice. A hybrid approach: log locally and ship a subset of events (e.g., errors only) to a central system. This balances cost and visibility.
Log Retention and Rotation
Disk space is finite. Configure log rotation (size-based or time-based) to prevent disk-full outages. For example, Serilog's rolling file sink can keep logs for 30 days or 100 MB per file. In centralized systems, set retention policies based on compliance needs (e.g., 90 days for audit logs, 7 days for debug logs). Monitor disk usage and set alerts.
Security and Sensitive Data
Logging sensitive data (passwords, credit card numbers, PII) is a compliance and security risk. Use filtering or masking to redact such fields. Serilog's Destructure policies can mask properties by name. For example, .Destructure.With<MaskingPolicy>() can replace password values with "***". Always review log output in development to ensure no sensitive data leaks.
Cost of Logging Infrastructure
Centralized logging services charge by volume (e.g., per GB ingested). A single misconfigured application can generate terabytes per day, leading to surprise bills. Set ingestion limits and use sampling (e.g., log only 1 in 10 events for high-volume paths). Many teams use a two-tier approach: detailed logs go to local files, and aggregated metrics or error summaries go to the central system.
Growth Mechanics: Scaling Logging Without Breaking the Bank
As your application grows, logging can become a bottleneck. Here are strategies to keep logging performant and cost-effective at scale.
Adaptive Sampling
Instead of logging every event, sample based on request rate or error status. For example, log all errors, but only 1% of successful requests. Serilog supports sampling via WriteTo.Map or custom sinks. This reduces volume while preserving error visibility. Adjust sampling rates dynamically based on traffic patterns.
Filtering at the Source
Apply filters early in the pipeline to discard unwanted events before they reach the sink. Use level filtering, namespace filtering, or custom predicates. For example, filter out health-check endpoint logs (/health) to reduce noise. This reduces both CPU and I/O overhead.
Aggregation and Metrics
For high-frequency events (e.g., cache hits, request counts), use metrics instead of logs. Libraries like App Metrics or OpenTelemetry can capture counters and histograms with minimal overhead. Logs should be reserved for events that require detailed context (e.g., exceptions, state changes). This shift can reduce log volume by orders of magnitude.
Distributed Tracing
In microservices, correlating logs across services is challenging. Use distributed tracing (e.g., OpenTelemetry, Jaeger) to propagate a trace ID. Include the trace ID in log events so you can search across services. This reduces the need to log every request detail in each service.
Risks, Pitfalls, and Mistakes: What to Avoid
Even with good intentions, teams make mistakes. Here are the most common logging landmines and how to avoid them.
Logging in Exception Filters and Middleware
Avoid logging inside exception filters or middleware that runs on every request. This can double-log errors or log non-error events. Instead, let the framework handle logging centrally (e.g., ASP.NET Core's built-in exception handler). Only add custom logging for specific business rules.
Using Logging as a Debugger
Some developers add log statements to trace code paths during development and forget to remove them. This leads to log pollution. Use conditional compilation (#if DEBUG) or log level filtering to keep development-only logs out of production. Better yet, use a debugger or unit tests for local debugging.
Ignoring Sink Backpressure
If a sink (e.g., network sink) becomes slow, it can block the logging pipeline and cause thread pool starvation. Use bounded buffers and drop policies (e.g., drop oldest events) to handle backpressure. Serilog's Async wrapper supports buffer limits and overflow actions.
Not Testing Logging Under Load
Logging performance is rarely tested during development. Run load tests with realistic log volumes to identify bottlenecks. Use profiling tools to measure CPU and memory impact. Adjust configuration based on results.
Mini-FAQ: Common Questions About C# Logging
Should I use async logging everywhere?
No. Async logging adds complexity and potential data loss. Use it only in high-throughput or I/O-bound scenarios. For low-volume applications, synchronous logging is simpler and sufficient.
How do I choose between Serilog and NLog?
Serilog offers richer structured logging and a larger sink ecosystem. NLog is slightly faster and has a smaller memory footprint. Both are excellent. Choose Serilog if you need deep structured log analysis; choose NLog if raw performance is critical.
What is the best way to log exceptions?
Log exceptions at the point they are caught, not thrown. Include the exception object and relevant context (e.g., request ID, user ID). Use LogError(exception, "Failed to process order {OrderId}", orderId). Avoid logging the same exception multiple times in the call stack.
How do I prevent log injection attacks?
Sanitize user input before logging. Use structured logging with placeholders—the framework will escape the values. Avoid concatenating user input into log messages. Also, ensure log viewers do not interpret log content as code (e.g., prevent CRLF injection in file logs).
Synthesis and Next Actions: Auditing Your Logging Setup
Logging is not a set-it-and-forget-it activity. Use this checklist to audit your current setup and make improvements.
- Check log levels: Are you logging at Information or higher in production? Reduce Debug/Trace.
- Review string interpolation: Replace all
$"..."with message templates. - Configure async sinks: Wrap file or network sinks with async wrapper if volume exceeds 100 events/second.
- Set up log rotation: Ensure rolling files or retention policies are in place.
- Filter noisy logs: Suppress health-check and framework-level logs.
- Mask sensitive data: Implement destructuring policies to redact PII.
- Test under load: Run a load test with expected log volume and measure CPU/memory.
- Monitor log volume: Set alerts for sudden spikes.
By addressing these points, you can transform logging from a nightmare into a reliable debugging ally. Start with the highest-impact items (levels and interpolation) and work through the list. Remember: logs are a tool, not a goal. Keep them lean, structured, and actionable.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!