Every language has edges. Most of the time you never touch them, until a counter goes negative in production or a service dies with no exception in the logs.
So I went looking for C#'s edges on purpose. Five questions, each answered by actually running the code. Machine: .NET 10.0.10, Windows 11, 20 cores, Release build unless stated. Your timings will differ, so treat them as "on my machine".
1. How high can an int count?
int counter = 0;
var timer = Stopwatch.StartNew();
while (true)
{
counter++;
if (counter < 0) break; // went past int.MaxValue
}
Console.WriteLine($"Wrapped to {counter:N0} after {timer.Elapsed.TotalSeconds:F2} s");
Wrapped to -2,147,483,648 after 0.47 s
Half a second to count through every positive int, and then it quietly wraps to the most negative one. No exception, no warning. (In a Debug build the same loop took about 1.1 s.)
C# arithmetic is unchecked by default. If an overflow would be a bug, say so:
int counter = int.MaxValue;
counter = checked(counter + 1); // no silent wrap
Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.
Useful at work: wrap counters, IDs and money-adjacent math in checked, or turn on overflow checking for the whole project with <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>.
2. How big can an array get?
Console.WriteLine($"Array.MaxLength: {Array.MaxLength:N0}");
var big = new byte[Array.MaxLength];
Console.WriteLine($"Allocated {big.Length / 1e9:F2} GB");
var tooBig = new byte[Array.MaxLength + 1];
Array.MaxLength: 2,147,483,591
Allocated 2.15 GB
Out of memory.
The first allocation worked. The second, just one element bigger, crashed the process. If you catch the exception instead, its message tells the real story: Array dimensions exceeded supported range.
The limit is the number of elements, not memory. Array.MaxLength is the same for every element type (bigger element types can run out of memory sooner).
Useful at work: more than about 2 billion items, or more than ~2 GB in one byte[], means chunking, streams or memory-mapped files, not a bigger array.
3. How deep can a method recurse?
static int depth = 0;
static void GoDeeper()
{
depth++;
if (depth % 1000 == 0) Console.WriteLine($"depth {depth:N0}");
GoDeeper();
}
try { GoDeeper(); }
catch (Exception e) { Console.WriteLine($"Caught: {e.Message}"); }
depth 15,000
depth 16,000
Stack overflow.
About sixteen thousand calls deep, for this method's frame size. And notice what's missing: Caught: never printed. A stack overflow can't be caught in .NET. The process is simply terminated.
The depth depends on the stack size, so a thread with a bigger stack goes much further:
var thread = new Thread(GoDeeper, maxStackSize: 256 * 1024 * 1024);
thread.Start();
thread.Join();
depth 2,794,000
depth 2,795,000
Stack overflow.
Useful at work: a bigger stack only moves the cliff. If input depth isn't under your control (deep JSON, user-built trees), use a loop with an explicit Stack<T> instead of recursion.
4. A million tasks vs ten thousand threads
var timer = Stopwatch.StartNew();
var tasks = new List<Task>();
for (int i = 0; i < 1_000_000; i++)
tasks.Add(Task.Delay(1000));
await Task.WhenAll(tasks);
Console.WriteLine($"1,000,000 one-second waits took {timer.Elapsed.TotalSeconds:F2} s");
1,000,000 one-second waits took 1.43 s
peak working set: 227 MB
One after another, those waits would take a million seconds, about 11.6 days. Run as tasks, they finished in under a second and a half. A waiting task doesn't hold a thread; it's just a small object and a timer.
Now the same idea with real threads, a hundred times fewer of them:
var timer = Stopwatch.StartNew();
var threads = new List<Thread>();
for (int i = 0; i < 10_000; i++)
{
var t = new Thread(() => Thread.Sleep(1000));
t.Start();
threads.Add(t);
}
threads.ForEach(t => t.Join());
Console.WriteLine($"10,000 threads took {timer.Elapsed.TotalSeconds:F2} s");
10,000 threads took 1.50 s
peak working set: 321 MB
Ten thousand threads worked here, but they used more memory than a million tasks.
To be precise about what this shows: waiting is cheap with tasks. It doesn't mean a million pieces of CPU work would finish in 1.4 seconds.
Useful at work: for I/O (HTTP calls, database queries, timers), use async/await and let tasks wait. Don't dedicate a thread to each waiting job.
5. A float that can't add 1
float big = 16_777_216f;
Console.WriteLine(big + 1 == big);
Console.WriteLine(0.1 + 0.2 == 0.3);
Console.WriteLine(0.1 + 0.2);
True
False
0.30000000000000004
16,777,216 is 2²⁴. A float has 24 bits of precision, so above that number it can only hold even integers. Adding 1 rounds straight back down:
16777216 -> 16777216
16777217 -> 16777216 (rounded)
16777218 -> 16777218
16777219 -> 16777220 (rounded)
And 0.1 has no exact binary representation, which is why 0.1 + 0.2 isn't 0.3 in C#, or in most languages.
decimal stores base-10 digits, so:
decimal a = 0.1m, b = 0.2m;
Console.WriteLine(a + b == 0.3m);
True
Useful at work: decimal for money. Never compare float/double with ==; compare with a tolerance.
Recap
-
intoverflow is silent. Usecheckedwhere it matters. - Arrays cap at 2,147,483,591 elements, whatever your RAM.
- Stack overflows can't be caught. Turn deep recursion into a loop.
- Waiting is cheap with tasks, expensive with threads.
-
floatanddoubleapproximate.decimalfor money.
Try any of these on your machine and post your numbers in the comments. I'm curious how much they vary.
I make short, tested videos about C# and what happens when you push it. More on the Naze Code YouTube channel.
Sources: Microsoft Learn, Array.MaxLength, checked and unchecked, StackOverflowException. Tested on .NET 10.0.10 (SDK 10.0.302), Windows 11, Release build.
Top comments (0)