Back to Blog
C#

C# StringBuilder Usage: When and How to Use It

c# stringbuilder usage: Learn how to use C# StringBuilder effectively, including when it improves performance, common methods, and thread-safety considerations.

StringBuilderC# PerformanceString ConcatenationMemory AllocationC# Best Practices
Diagram comparing string concatenation and StringBuilder memory allocation in C#.

String concatenation in C# creates a new string object each time, which can become a performance problem in loops. C# StringBuilder usage addresses that by providing a mutable character buffer that can be appended to without allocating a new string on every operation. This article explains how to use StringBuilder correctly, when it actually helps, and when it adds unnecessary complexity.

What StringBuilder Does That String Concatenation Doesn't

Strings in C# are immutable. Every time you use the + operator or string.Concat, the runtime allocates a new string and copies the contents of the previous strings into it. For a small number of concatenations, this is fine. But inside a loop that runs thousands of times, the repeated allocation and copying become a measurable cost.

string result = ""; for (int i = 0; i < 10000; i++) { result += i.ToString(); // Allocates a new string each iteration }

Each += creates a new string that contains the entire accumulated value plus the new part. The previous string becomes garbage for the next garbage collection. The time complexity is O(n²) because each step copies the entire growing string.

StringBuilder keeps an internal buffer that can grow as needed. Appending to it writes into that buffer and only reallocates when the buffer is full. This makes repeated appends roughly linear in the total number of characters added.

var builder = new StringBuilder(); for (int i = 0; i < 10000; i++) { builder.Append(i); // Writes into the existing buffer when possible } string result = builder.ToString();

The ToString() call creates a single string from the final buffer. This is the core reason to consider StringBuilder when building a string from many parts.

Basic StringBuilder Usage

The most common operations are Append, AppendLine, and ToString. Append adds a string, numeric value, or any object's ToString() result to the buffer. AppendLine adds a newline after the appended value, which is useful when building multi-line text.

var builder = new StringBuilder(); builder.Append("Name: "); builder.AppendLine("Alice"); builder.Append("Score: "); builder.AppendLine(95); string output = builder.ToString();

This produces:

Name: Alice
Score: 95

You can also chain methods because Append and AppendLine return the same builder instance.

var builder = new StringBuilder(); builder.Append("Error: ").Append(code).AppendLine(" occurred");

Chaining reduces repetitive variable references and makes the code read more linearly.

Formatting, Inserting, and Modifying Text

StringBuilder provides methods beyond simple appending. AppendFormat uses a format string with placeholders, similar to string.Format. This is useful when you need to control alignment, padding, or number formatting.

var builder = new StringBuilder(); builder.AppendFormat("{0,-10} {1,5:F2}", "Item", 12.345);

The Insert method places text at a specific index, shifting the existing content. Replace swaps all occurrences of a substring. Remove deletes a range of characters.

var builder = new StringBuilder("Hello world"); builder.Insert(6, "beautiful "); // "Hello beautiful world" builder.Replace("world", "C#"); // "Hello beautiful C#" builder.Remove(5, 10); // "Hello C#"

These operations modify the buffer in place, avoiding the repeated allocations that would occur if you manipulated immutable strings directly.

Performance Considerations and Allocation Behavior

StringBuilder's performance advantage comes from reducing allocations, but it is not free. The internal buffer starts with a default capacity of 16 characters. When you append more than the capacity, the buffer doubles in size and copies the existing characters into the new buffer. If you know the approximate final length, you can set the initial capacity to avoid multiple reallocations.

var builder = new StringBuilder(256); // Preallocate for expected length

For very large strings, the buffer growth can still cause a few copies, but the total cost is much lower than repeated string concatenation.

There is also a tradeoff: StringBuilder uses more memory than a single string of the same length because the buffer may be larger than the content. If you build a string and then discard the builder, the buffer becomes garbage. This is acceptable when the builder is used for a short time.

Another point is that ToString() creates a new string copy. If you need the string only once, that is fine. But if you need to keep the builder around for further edits, each ToString() call adds a full copy.

When Not to Use StringBuilder

StringBuilder is not always the right choice. For a fixed number of concatenations, the compiler and runtime can often optimize simple cases. For example, string s = "a" + "b" + "c" is compiled to a single string literal. Even with variables, a few concatenations are cheap enough that the overhead of creating a StringBuilder instance and calling methods may be higher than the allocation cost.

string name = "Alice"; int score = 95; string message = "Name: " + name + ", Score: " + score;

This is clear and performant enough for one-off construction. Use StringBuilder when you are building a string in a loop, when the number of parts is unknown and large, or when you need to repeatedly modify a string in memory.

String interpolation is also a good alternative for many cases. It is syntactic sugar over string.Format and is efficient for a small number of values.

string message = $"Name: {name}, Score: {score}";

If you find yourself using += inside a loop, that is a signal to consider StringBuilder. If you are just combining a handful of values, keep the simpler syntax.

Thread Safety and Concurrency Concerns

StringBuilder is not thread-safe. If multiple threads call Append on the same instance without synchronization, the internal buffer can be corrupted or the resulting string can be inconsistent. This is a common pitfall when building a log message from multiple threads.

One approach is to use a lock around each operation, but that serializes access and may hurt performance. Another is to give each thread its own StringBuilder and merge the results later. For a shared builder, you can use lock or a concurrent collection of partial strings.

private readonly object _lock = new object(); private StringBuilder _shared = new StringBuilder(); public void Add(string value) { lock (_lock) { _shared.Append(value); } }

If you need thread-safe string building, consider using string.Concat with an array of strings or a ConcurrentQueue and a single consumer. The key is to avoid sharing a mutable builder across threads without coordination.

Advanced Usage Patterns: Reusing and Clearing

In high-throughput scenarios, repeatedly creating new StringBuilder instances can add allocation pressure. You can reuse a single builder by calling Clear() to reset its length to zero while keeping the underlying buffer. This avoids reallocating the buffer for each use.

var builder = new StringBuilder(1024); for (int i = 0; i < 1000; i++) { builder.Clear(); builder.Append("Item ").Append(i); string item = builder.ToString(); // Use item } ```n This pattern is useful when the builder is used sequentially and the final string is consumed before the next iteration. The buffer is reused, and only the `ToString()` call allocates a new string. Another advanced technique is to use `StringBuilder` as a scratch buffer for parsing or tokenizing, where you need to accumulate characters and then extract the result. You can access the internal buffer via `GetChunks()` to avoid copying in some scenarios, but that is an advanced optimization that is rarely necessary. When you reuse a builder, be aware that the capacity remains at its maximum. If you clear a large builder and then only need a small string, the memory stays allocated. This is a tradeoff between allocation speed and memory footprint. For a long-lived service, pooling a few builders with a fixed capacity can be beneficial, but measure before adding that complexity.
c# stringbuilder usage: Practical Usage and Code Examples | RYUSLOG DEV