Question

Why C# Random Generates Repeated Numbers in a Loop

csharprandom

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.

Take Quiz

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:

  1. Iteration 0 creates a generator and immediately asks for its first value.
  2. Iteration 1 creates another generator almost immediately afterward.
  3. Iteration 2 does the same.
  4. If the generators are initialized from the same timing information, each starts from the same seed.
  5. 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));
}
  1. One generator is selected before the loop.
  2. The first call returns one item in its sequence.
  3. The second call advances the same generator and returns the next item.
  4. 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.

Take Quiz

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

OptionBest useNotes
new Random() on every callAvoidFast repeated construction can cause repeated sequences.
One stored Random instanceOlder .NET codeWorks when it is safely shared and reused.
Random.SharedMost modern non-security codeSimple shared, thread-safe generator.
Random.Shared.NextBytes(...)Non-secure random byte arraysDirectly fills an array with pseudo-random bytes.
RandomNumberGenerator.Fill(...)Security-sensitive random bytesCryptographically 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.

Take Quiz

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.Shared in modern .NET.
  • Next(min, max) includes min and excludes max.
  • Use direct ranges instead of % when possible.
  • Use RandomNumberGenerator for 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.

Take Quiz

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.
Take Quiz

Keep learning

Related questions

View more