How SearchValues Saved Us From Scaling Hell
Your string operations are killing your API. You just haven’t measured it yet.
While you’re busy optimizing database queries and adding cache layers, thousands of string searches per second are quietly eating your CPU budget. The problem isn’t visible in your APM dashboard because it’s distributed across every request. But it’s there. Compounding. Scaling linearly with load.
I discovered this the hard way when a log processing API started choking under production traffic. The bottleneck? String validation and sanitization. The fix? A .NET 8 feature that delivered a 5x performance improvement and let us shut down servers instead of adding them. And it’s gotten even better in .NET 9 and 10.
SearchValues<T>
isn’t a nice-to-have optimization. It’s the difference between infrastructure costs that scale with your success versus infrastructure costs that scale with your inefficiency.
The Real Problem: String Operations Don’t Scale Like You Think
Here’s what nobody tells you about string operations: they scale linearly with load. Double the traffic? Double the CPU usage. Add more features that parse strings? Multiply the pain.
Traditional approaches using string.IndexOfAny()
or custom loops work fine when you’re processing dozens of requests per second. They fail silently when you’re processing thousands. No exceptions. No errors. Just slow, expensive CPU burn that compounds into massive infrastructure waste.
Consider a real scenario: a log aggregation service processing application logs in real-time. Each log entry needs sanitization, sensitive data detection, and validation before storage. That’s multiple string operations per log entry. Thousands of log entries per second. Millions of string operations per minute.
With traditional methods, you’re leaving performance on the table. Modern CPUs have SIMD instructions that can process multiple characters simultaneously, but IndexOfAny()
doesn’t use them consistently. It’s a generic solution for a problem that benefits from specialization.
That’s where SearchValues<T>
comes in. It’s a frozen, immutable set of values that .NET analyzes once at creation time, then optimizes specifically for your search pattern using the fastest algorithm available, whether that’s SIMD vectorization, bitmap lookups, or other strategies depending on your value set. The difference isn’t marginal. It’s transformational.
What Makes SearchValues<T>
Different?
SearchValues<T>
was introduced in .NET 8. Most developers still haven’t heard of it. That’s a mistake that’s costing infrastructure budget.
When you create a SearchValues<T>
instance, .NET analyzes your value set once and selects the most efficient search algorithm. SIMD vectorization when possible. Bitmap lookups for dense character sets. Optimized branching for sparse sets. The runtime chooses. You don’t.
You pay the analysis cost once at startup. Then you reuse that optimized instance across millions of operations. That’s the trade-off: slightly more expensive creation, dramatically cheaper execution.
TLDR; Pure performance win.
.NET 9 and .NET 10: Getting Even Faster
SearchValues<T>
didn’t stop at .NET 8. Microsoft kept pushing performance further with each release, expanding both capabilities and raw speed.
.NET 9: Multi-Substring Search
.NET 8 gave us SearchValues<char>
and SearchValues<byte>
for character-level searches. .NET 9 expands this with SearchValues<string>
for searching multiple substrings within a string.
Before (Regex):
// Looking for "error", "warning", or "critical" (case-insensitive)
var regex = new Regex("(?i)error|warning|critical", RegexOptions.Compiled);
bool found = regex.IsMatch(logMessage);
After (SearchValues):
private static readonly SearchValues<string> LogKeywords =
SearchValues.Create(["error", "warning", "critical"], StringComparison.OrdinalIgnoreCase);
bool found = logMessage.AsSpan().ContainsAny(LogKeywords);
The Regex compiler in .NET 9 uses this automatically when it detects multi-substring patterns. Call it directly to skip regex parsing overhead entirely.
Multi-substring searches in log analysis became 3-4x faster in .NET 9. Patterns that were “too expensive” for every log entry suddenly became viable for real-time alerting.
TLDR; .NET 9 is even faster for multi-substring searches.
.NET 10: Hardware-Level Optimizations
.NET 10 takes the optimization further by targeting CPU instruction sets directly. On AVX-512, it replaces two instructions with a single PermuteVar64x8x2
, cutting CPU cycles in half. ARM64 gets cheaper UnzipEven
instructions, delivering better performance on AWS Graviton and Azure Ampere instances. Case-insensitive searches benefit from extended fast-path logic that reduces validation overhead.
If you’re running .NET 9 and seeing good results, .NET 10 makes the same operations 10-20% faster without code changes. Just upgrade the runtime.
For heavy text processing workloads, that 10-20% compounds across millions of operations. It’s the difference between needing an extra worker node versus staying within capacity.
TLDR; Once again, .NET 10 is faster.
Production Numbers
Benchmarks lie. Production doesn’t.
In a real log processing pipeline handling production traffic, 10,000 log entries that previously took ~450ms now complete in ~85ms. That’s 5.3x faster on the same hardware. Not 20% faster optimization theater. That’s handling five times more throughput without adding a single server. Infrastructure costs going down while traffic going up.
This isn’t theoretical performance gains on a conference slide deck. This is the difference between scaling horizontally by adding servers versus scaling efficiently by using what you have. Between infrastructure costs that increase with growth versus costs that stay flat. Between firefighting capacity problems versus preventing them.
The business impact is direct: fewer servers, lower costs, same (or better) performance.
Log Sanitization Without the Performance Tax
Logging should be cheap and invisible. Write to stdout, let your log shipper handle it, done. Reality is messier.
Log sanitization is expensive. Stripping sensitive data and control characters before storage becomes a bottleneck at scale. And every production system does it because compliance demands it. Regulations like GDPR, PCI-DSS, and HIPAA all require sanitizing logs.
Here’s a simplified example of log sanitization that removes dangerous characters and redacts sensitive data patterns:
using System;
using System.Buffers;
using System.Text;
public class LogSanitizer
{
// Characters that could indicate injection attempts or corrupt data
private static readonly SearchValues<char> DangerousCharacters =
SearchValues.Create("\0\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0E\x0F" +
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F");
// Patterns that often precede sensitive data in logs
private static readonly SearchValues<char> SensitiveMarkers =
SearchValues.Create("=:@");
public static string SanitizeLogEntry(string logEntry)
{
ReadOnlySpan<char> span = logEntry.AsSpan();
// Fast path: if no dangerous characters, return original
if (!span.ContainsAny(DangerousCharacters))
{
return ProcessSensitiveData(logEntry);
}
// Slow path: rebuild without dangerous characters
var builder = new StringBuilder(logEntry.Length);
foreach (char c in span)
{
if (!DangerousCharacters.Contains(c))
builder.Append(c);
}
return ProcessSensitiveData(builder.ToString());
}
private static string ProcessSensitiveData(string log)
{
// Find patterns like "password=", "token:", "apiKey@"
ReadOnlySpan<char> span = log.AsSpan();
int markerIndex = span.IndexOfAny(SensitiveMarkers);
if (markerIndex == -1) return log;
// Redact the next 20 characters after sensitive markers
var result = new StringBuilder(log.Length);
result.Append(span.Slice(0, markerIndex + 1));
result.Append("[REDACTED]");
return result.ToString();
}
}
The Impact: 87% reduction in sanitization time. Not in a benchmark. In production. Under real load.
The fast path (clean logs) allocates zero heap memory. Garbage collector stays asleep. Sub-millisecond performance for 10KB log entries is the norm.
Across millions of logs daily, that’s measurable infrastructure savings. We shut down log processing workers instead of adding them. Real money saved.
What You Need to Know
Microsoft’s CA1870 analyzer throws warnings when it detects string searching that could use SearchValues<T>
. The warning appears on code like text.IndexOfAny(new[] { ',', ';', '|' })
and suggests converting to SearchValues<T>
. The fix is straightforward: create a static readonly SearchValues<char> Delimiters = SearchValues.Create([',', ';', '|'])
and use text.AsSpan().IndexOfAny(Delimiters)
. The analyzer is helpful once you know when to act on it.
The most critical mistake is creating instances in loops or hot paths. SearchValues<T>
is powerful but easy to misuse. The analysis work happens at creation time, which is expensive. The lookup cost is cheap. This means you need to store instances as static readonly fields. Pay the creation cost once at startup, then benefit from fast lookups across millions of operations. Creating SearchValues<T>
repeatedly destroys the performance advantage you’re trying to gain.
Premature optimization is still evil, but there’s a clear pattern where SearchValues<T>
delivers real value. The sweet spot is searching for 3+ different characters or values in code that executes hundreds to millions of times, specifically in hot paths, per-request logic, or tight loops. Variable-length input where you can’t predict string size amplifies the benefits. Modern CPUs with SIMD support (most hardware from the last 5-7 years) show the biggest gains, often 5-10x faster. For searching 1-2 characters, just use IndexOf
directly. It’s simpler and performs similarly. Skip SearchValues<T>
for code that runs once at startup, error handling paths that rarely execute, tiny fixed-length strings, or when you’re already bottlenecked on I/O operations. The overhead isn’t worth it for infrequent operations.
The real test is production, not benchmarks. I’ve seen 2x to 10x improvements depending on workload, but your results will vary based on several factors. Character set size matters, where larger sets (10+ characters) benefit more from vectorization than small sets (2-3 characters). String length amplifies the speedup since more characters are processed. Search frequency compounds the benefits over time. CPU architecture plays a role too, with modern SIMD-capable processors showing 5-10x gains while older CPUs show 2-3x improvements. Don’t trust microbenchmarks that optimize for cache locality and predictable branching that production never has. Run load tests with production-like data. Measure wall-clock time and monitor allocations, not just throughput.
Performance improvements in production aren’t synthetic benchmark gains. They’re real, measurable, budget-impacting improvements. Zero-allocation fast paths reduce GC pressure, which compounds in long-running services. Security validators that ran 3-5x faster meant user-facing latency reduction that customers noticed. ETL pipelines that completed 4x faster let us decommission servers instead of adding them. Not “up to” savings in a marketing slide. Actual, budgeted, we-turned-off-these-instances savings.
Not every use case benefits equally. Profile first, optimize second. Small character sets (2-3 characters) show minimal gains. Large character sets (10+ characters) deliver significant wins. Find your workload’s threshold. The creation cost is real, so single searches don’t benefit. This optimization is for repeated operations in high-frequency code paths.
In high-throughput APIs and data pipelines, SearchValues<T>
delivered one of the highest ROI optimizations relative to effort. Minimal code changes. No architectural changes. No dependency updates or compatibility breaks. Measurable immediate gains. But ROI requires return. If you’re not processing thousands of operations per second, this won’t move the needle. Measure your use case and save energy for problems that actually matter.
The Bottom Line
SearchValues<T>
delivers measurable business value when applied correctly, but becomes noise when applied incorrectly. The difference lies in recognizing the pattern: high-frequency operations searching for multiple characters in variable-length input. This pattern shows up in log processing, input validation, CSV parsing, security filtering, and file path sanitization, essentially anywhere you’re repeatedly searching for multiple values in strings or spans where the size varies.
The methodology is straightforward, though discipline matters. Start by measuring and profiling your hot paths to identify actual bottlenecks instead of guessing where problems might be. Apply the optimization selectively, focusing on repeated operations with 3+ characters in performance-critical code paths. Then validate the impact through benchmarking with realistic data under realistic load, not synthetic microbenchmarks that optimize for conditions production never sees. Treat CA1870 warnings as a starting point for investigation, not a mandate for action. Your profiler understands your workload better than any static analyzer ever could.
The log sanitization example from earlier demonstrates the core pattern in action: high frequency, variable input, multiple search targets. This same combination appears across input validation, data parsing, security filtering, and content sanitization throughout production systems. When you find this pattern in your codebase, apply SearchValues<T>
and measure what changes. When it works, it transforms performance in ways that show up directly in infrastructure costs. When it doesn’t deliver results, you’ve invested an hour learning something valuable about your workload’s actual characteristics.
The impact is asymmetric by design. Your infrastructure budget notices the difference immediately through fewer servers, lower costs, and better resource utilization. Your customers notice nothing, which is exactly the point of effective performance optimization. Good performance work is invisible to users while being highly visible in cost structure and operational metrics. One less bottleneck when scaling. One fewer server to provision. One more problem solved before it escalates into a crisis that demands emergency intervention.