Question
I have a method that creates a Random object and returns a number within a range:
public static int RandomNumber(int min, int max)
{
Random random = new Random();
return random.Next(min, max);
}
I call it in a loop to populate a six-byte mac array:
byte[] mac = new byte[6];
for (int x = 0; x < 6; ++x)
{
mac[x] = (byte)(Misc.RandomNumber(0xFFFF, 0xFFFFFF) % 256);
}
When I step through the loop in the debugger, the array contains different values. However, when I place a breakpoint after the loop, every element in mac has the same value. Why does this happen?
Short Answer
You will learn why repeatedly constructing Random can produce the same sequence in a fast loop, why debugging appears to fix the issue, and how to reuse a generator or use the correct API for random bytes.
Concept
Random is a pseudo-random number generator. It does not create truly random values; instead, it produces a deterministic sequence based on an initial value called a seed.
When code creates several new Random() instances very quickly, the instances may receive the same or closely related time-based seed. Generators with the same seed produce the same sequence, so each call to Next(...) can return the same first value.
In the original method, this line runs once for every loop iteration:
Random random = new Random();
The loop is fast enough that all six generators can be initialized at nearly the same time. As a result, they can behave identically.
Stepping through the debugger pauses execution between iterations. Those pauses change the timing, so newly created generators receive different seeds and appear to work correctly. The debugger does not make the code correct; it only hides the timing-related problem.
The usual solution is to reuse one random-number generator. In modern .NET, use Random.Shared.
Mental Model
Think of a Random object as a music player with a playlist chosen from a startup code.
- The seed is the startup code.
- Calling
Next()moves to the next song in that playlist. - Creating several players with the same startup code means every player starts with the same first song.
Your loop creates a brand-new player for every byte. When the startup codes are the same, every player plays the same first song. Reusing one player lets it continue through one varied playlist.
Syntax and Examples
Use one generator for multiple random values.
byte[] mac = new byte[6];
for (int x = 0; x < mac.Length; x++)
{
mac[x] = (byte)Random.Shared.Next(0, 256);
}
Random.Shared is a shared, thread-safe Random instance provided by modern .NET. Next(0, 256) returns an integer from 0 through 255, which fits in a byte.
If your project targets an older framework without Random.Shared, create one Random outside the loop and reuse it:
Random random = new Random();
byte[] mac = new byte[6];
for (int x = 0; x < mac.Length; x++)
{
mac[x] = (byte)random.Next(0, 256);
}
For a whole byte array, a simpler modern approach is:
byte[] values = Random.Shared.NextBytes(6);
This fills the array with pseudo-random bytes.
Step by Step Execution
Consider this problematic code:
for (int x = 0; x < 3; x++)
{
Random random = new Random();
Console.WriteLine(random.Next(0, 256));
}
Without debugger pauses:
- Iteration
0creates a generator and immediately asks for its first value. - Iteration
1creates another generator almost immediately afterward. - Iteration
2does the same. - If the generators are initialized from the same timing information, each starts from the same seed.
- Each generator returns the same first value, so repeated output is possible.
Now reuse one generator:
Random random = Random.Shared;
for (int x = 0; x < 3; x++)
{
Console.WriteLine(random.Next(0, 256));
}
- One generator is selected before the loop.
- The first call returns one item in its sequence.
- The second call advances the same generator and returns the next item.
- The third call advances it again.
Values can still occasionally match by chance, but they are no longer repeated because each loop iteration creates an identically seeded generator.
Real World Use Cases
Reusable pseudo-random generators are useful when an application needs varied but non-security-sensitive values:
- Choosing a random enemy position in a game.
- Sampling records for a report or test run.
- Adding simulated variation to a demo or training program.
- Generating test data, such as ages, scores, or byte arrays.
- Randomly selecting a fallback server or a visual theme.
Do not use Random for secrets, authentication tokens, password reset links, encryption keys, session identifiers, or security-sensitive identifiers. Use RandomNumberGenerator for those cases.
Real Codebase Usage
In real C# projects, developers generally avoid constructing Random inside frequently called methods. Common patterns include:
Use Random.Shared for ordinary application randomness
int delayMilliseconds = Random.Shared.Next(100, 501);
This is concise and suitable for most non-security uses.
Generate a range without modulo
byte value = (byte)Random.Shared.Next(0, 256);
This is preferable to generating a larger value and applying % 256.
Fill a byte array directly
byte[] sample = Random.Shared.NextBytes(32);
This expresses the intent clearly and avoids a manual loop.
Use cryptographic randomness for sensitive data
using System.Security.Cryptography;
byte[] tokenBytes = new byte[32];
RandomNumberGenerator.Fill(tokenBytes);
For a random value in a secure range, use:
int code = RandomNumberGenerator.GetInt32(100000, 1000000);
This avoids predictable values and modulo bias.
Common Mistakes
Creating Random inside a loop
// Problematic: a new generator is created repeatedly.
for (int i = 0; i < 6; i++)
{
byte value = (byte)new Random().Next(0, 256);
}
Create or obtain the generator once instead:
for (int i = 0; i < 6; i++)
{
byte value = (byte)Random.Shared.Next(0, 256);
}
Assuming debugger behavior proves code is correct
Debuggers add delays and can change timing-dependent behavior. Test the program normally, and fix the lifetime of the Random object rather than relying on stepping.
Misunderstanding the maximum value of Next(min, max)
The upper bound is exclusive:
int value = Random.Shared.Next(1, 4);
Possible results are 1, 2, and 3, never 4.
Using % 256 unnecessarily
byte value = (byte)(Random.Shared.Next() % 256);
Modulo can introduce a small distribution bias when the source range is not evenly divisible by 256. Request the desired range directly:
byte value = (byte)Random.Shared.Next(0, 256);
Using Random for security
// Not appropriate for a password-reset token.
string token = Random.Shared.Next().ToString();
Use RandomNumberGenerator for anything an attacker must not predict.
Comparisons
| Option | Best use | Notes |
|---|---|---|
new Random() on every call | Avoid | Fast repeated construction can cause repeated sequences. |
One stored Random instance | Older .NET code | Works when it is safely shared and reused. |
Random.Shared | Most modern non-security code | Simple shared, thread-safe generator. |
Random.Shared.NextBytes(...) | Non-secure random byte arrays | Directly fills an array with pseudo-random bytes. |
RandomNumberGenerator.Fill(...) | Security-sensitive random bytes | Cryptographically secure; use for tokens and secrets. |
Random and RandomNumberGenerator both produce varying values, but they solve different problems. Random prioritizes convenience and speed. RandomNumberGenerator prioritizes unpredictability.
Cheat Sheet
// A random integer: min inclusive, max exclusive
int number = Random.Shared.Next(10, 20);
// A random byte from 0 through 255
byte value = (byte)Random.Shared.Next(0, 256);
// Fill an array with pseudo-random bytes
byte[] bytes = Random.Shared.NextBytes(6);
// Fill an array with cryptographically secure bytes
using System.Security.Cryptography;
byte[] secureBytes = new byte[32];
RandomNumberGenerator.Fill(secureBytes);
- Do not create
new Random()repeatedly in a fast loop or frequently called method. - Reuse a generator; prefer
Random.Sharedin modern .NET. Next(min, max)includesminand excludesmax.- Use direct ranges instead of
%when possible. - Use
RandomNumberGeneratorfor secrets and security-sensitive values.
FAQ
Why do all Random values repeat only when my code runs quickly?
Rapid calls can create generators with the same or similar time-based initialization. Debugger pauses make those initializations occur farther apart.
Does Random.Shared return the same object every time?
It provides a shared random generator interface intended for convenient, thread-safe use in modern .NET applications.
Can random values still be equal after fixing the code?
Yes. Repeated values are always possible by chance. The issue is when an entire fast sequence is suspiciously identical because new generators repeatedly start from the same seed.
Is Random.Next(0, 256) valid for creating a byte?
Yes. It returns values from 0 through 255, all of which fit in a byte.
Why is the second argument to Random.Next excluded?
An exclusive upper bound makes it easy to describe ranges and work with lengths. For example, Next(0, array.Length) always produces a valid array index.
Should I use Random to generate a MAC address?
For a simulated or local identifier, it can be acceptable. For cryptographic or security purposes, use RandomNumberGenerator. A real MAC address also has convention bits that may need to be set depending on your use case.
Mini Project
Description
Create a small utility that generates a locally administered, unicast MAC-style address. The project demonstrates how to fill a byte array using one appropriate random source and how to apply bit rules to a byte.
Goal
Generate and display a six-byte MAC-style address whose bytes do not all repeat because of repeated Random construction.
Requirements
- Create an array containing exactly six bytes.
- Generate the bytes using a reusable random source.
- Set the first byte as locally administered.
- Set the first byte as unicast rather than multicast.
- Format the result as uppercase hexadecimal pairs separated by colons.
Keep learning
Related questions
AddTransient vs AddScoped vs AddSingleton in ASP.NET Core Dependency Injection
Learn the differences between AddTransient, AddScoped, and AddSingleton in ASP.NET Core DI with examples and practical usage.
Best Way to Repeat a Character in C#: Building Repeated Strings Efficiently
Learn the best way to repeat a character in C#, compare StringBuilder, string concatenation, and simpler built-in options.
C# Access Modifiers and static: public, private, protected, and Defaults
Learn how C# public, private, protected, and default access control visibility, and how static differs from instance members.