Back to Blog
C#

C# Static Readonly Usage: Syntax and Tradeoffs

c# static readonly usage: Learn how static readonly fields work in C#, how they differ from const, and when to use them for runtime constants.

C#static readonlyconst vs readonlyC# fieldsruntime constants
Diagram comparing compile-time const constants with runtime static readonly constants in C#

In C#, static readonly is the modifier combination used when a value must be shared across all instances of a type but cannot be known at compile time. The most common point of confusion in C# static readonly usage is how it differs from const, and that difference determines which one belongs in a given class or struct.

What static readonly Means

A static readonly field belongs to the type itself rather than to any instance. The readonly keyword means the field can be assigned only during declaration or inside a constructor — for instance fields — or inside a static constructor for static fields. After that, the field cannot be reassigned.

public class AppConfig { public static readonly string DefaultConnectionString = LoadDefaultConnection(); private static string LoadDefaultConnection() { return Environment.GetEnvironmentVariable("DEFAULT_CONNECTION") ?? "localhost"; } }

The field is initialized once when the type is first accessed. Every subsequent read returns the same value, so the field behaves as a runtime constant for the lifetime of the application.

static readonly vs const

const fields are compile-time constants. The compiler substitutes the literal value wherever the field is referenced. That means the value must be a compile-time constant — a string, numeric literal, bool, enum, or null reference — and the value is baked into the referencing assembly at compile time. If you change the constant in the defining assembly, every referencing assembly must be recompiled to pick up the change.

static readonly fields are runtime constants. The value is computed when the type is initialized, so it can come from configuration, environment variables, file content, or any method call. Referencing assemblies read the field at runtime, so a change in the defining assembly does not require recompilation of consumers.

Criterionconststatic readonly
Value known atCompile timeRuntime
Type restrictionsPrimitive, string, enum, nullAny type
ReassignmentNeverOnly in static constructor or initializer
Consumer behaviorValue baked into consumerValue read at runtime
Best fitFixed literalsConfigurable or computed values

When to Use static readonly

Use static readonly when the value is not a compile-time constant but should still be shared and immutable. Common cases include reading from configuration or environment variables, building a collection that should not be modified after initialization, and referencing types like DateTime or Guid that cannot appear in a const declaration.

public static readonly Guid AppInstanceId = Guid.NewGuid();

This pattern is valid because Guid.NewGuid() is not a compile-time constant, yet the field should remain fixed for the lifetime of the application. The same reasoning applies to values derived from environment settings or feature flags.

Initialization Options

A static readonly field can be initialized in two places: at the declaration site or inside a static constructor.

public static readonly int MaxRetries = 3; static AppConfig() { MaxRetries = int.Parse(Environment.GetEnvironmentVariable("MAX_RETRIES") ?? "3"); }

The static constructor runs once before any static member of the type is accessed. If initialization is a single expression, a field initializer is clearer. If initialization requires multiple steps, validation, or error handling, the static constructor keeps that logic in one place and makes the order of initialization explicit.

Runtime Behavior and Thread Safety

The runtime guarantees that a static constructor runs exactly once per application domain or process. Type initialization is thread-safe by default: the runtime acquires a lock during initialization, so concurrent threads cannot observe a partially initialized type. This makes static readonly safe to read from multiple threads without additional synchronization.

For a field that requires expensive setup, the Lazy<T> pattern avoids the work until the field is actually read.

public static readonly Lazy<HttpClient> Client = new Lazy<HttpClient>(() => new HttpClient());

This is a deliberate choice when initialization cost is high and the field may never be accessed during a particular run.

Common Mistakes and Edge Cases

A frequent mistake is assuming static readonly makes the referenced object immutable. It does not. The field reference cannot be reassigned, but the object it points to can be mutated.

public static readonly List<string> AllowedHosts = new List<string> { "example.com" }; // This compiles and mutates the list: AllowedHosts.Add("another.com");

If the intent is an immutable collection, use ReadOnlyCollection<T> or ImmutableArray<T> instead. Another mistake is using static readonly for a value that should be a compile-time constant. If the value is a literal that never changes, const is simpler and avoids runtime initialization overhead. The decision should be based on whether the value can change between deployments.

Maintainability Considerations

Changing a const in a shared assembly requires recompiling all consumers. Changing a static readonly value does not, because consumers read the field at runtime. This makes static readonly the safer choice for values that may change across deployments, such as connection strings, version numbers, and feature flags.

However, the runtime lookup means the field is read each time it is accessed. In hot paths, caching the value in a local variable can avoid repeated field access, though the JIT often optimizes this in practice. The real tradeoff is between compile-time substitution, which is fastest but requires recompilation on change, and runtime lookup, which is more flexible but slightly less direct. Choose static readonly whenever the value is not a compile-time constant or when you need consumers to observe changes without rebuilding.