Issue 504 · Week of Sep 05, 2026
Feed Jobs Search Platform About Donate

Memory Usage Optimization tricks for C# automation tests - Garbage Collector and others

1. Types of languages - compiled and interpreted

Now on our planet we have 2 categories of languages - compiled and interpreted.

Compiled languages are languages where the source code is first translated (compiled) by a special program called a compiler into machine code (an executable file). This machine code can then be executed directly by the processor.

Examples of compiled languages: C, C++, Rust, Go.

Interpreted languages are languages where the source code is executed line by line or command by command by a special program called an interpreter.

Examples of interpreted languages: Python, JavaScript, PHP, Ruby.

Also, all software development languages can be divided into 2 groups: High-Level and Low-Level.

2. Types of languages - high and low level

What is the difference between High-Level and Low-Level Languages?

High-level programming languages are closer to human language. They abstract away hardware details and allow programmers to focus on program logic rather than memory management or CPU structure.

Examples: Python, Java, C#, JavaScript.

Low-level languages are closer to machine code. Programmers using them work directly with memory, CPU registers, and hardware resources.

Examples: Assembler and C/C++ partially.

3. What main languages are used for automation testing?

Let's sort these languages via categories:

Category Compiled Interpreted
High Level C#, JAVA, SWIFT, Objective C Java Script, TypeScript, Python, RUBY
Low Level N/A N/A

Two key conclusions:

The main question of this article - how we can provide memory optimization on compiled and high-level languages in automation test development (we will use C# for example)?

The second question of this article - do we need to provide this memory optimization?

4. When we need to pay attention on memory optimization on C# Automation tests?

Imagine you have a Desktop application, and you need to check what it will be if you launch separate copies of this application. Your RAM will be fully loaded, and it would be great to save some RAM usage on your automation tests.

Imagine creating automation tests for Web applications which use a lot of 3D Graphics, Videos, Images, Sound effects etc. It will be great to save some RAM usage on automation tests.

MAIN THING - you have no ability to increase hardware resources on your virtual machine on CI system.

You are creating automation tests for 3D applications made via Unity, Unreal engine etc.

When you are writing automation tests in C# it is important to think not only about correctness but also about memory efficiency. This is especially relevant in long running or performance tests, where memory leaks can slow down the entire test suite.

5. "New" keyword and Garbage Collector

In C# if you are trying to create some class instance using "new" keyword, you allocate memory on the heap.

Var listOfItems = new List<string>();

But in C# you have no opposite operator. There is no any "delete" operator in C# which can work similarly to other languages.

Var listOfItems = new List<string>();
deleteFromMemory(listOfItems);

In C#, in the .NET framework we have a Garbage Collector.

Main thing - Garbage Collector will clear all objects from memory which was created by "new" C# keyword.

The Garbage Collector (GC) on the .NET helps automatically manage memory by reclaiming unused objects — and with the right approach, it can be used to optimize test stability and performance.

How the Garbage Collector Works?

The GC tracks objects in the managed heap and frees memory used by objects that are no longer referenced. It runs automatically in the background, but in specific test scenarios, developers can trigger it manually to stabilize memory usage between test runs.

6. Some Typical Advice for Automation Tests

6.1 Log your RAM usage

First of all, try to log your RAM usage. Measure before optimizing.

How to do it:

6.2 Clearing Lists

You can clear any list! At the end of tests, on TearDown - you can clear your lists, it can help you to save memory.

No!Yes!
ArrayList list = new ArrayList(23424)
DoSomething(list)
// We need to make list clear
ArrayList list = new ArrayList(23424)
DoSomething(list);
ArrayList list = new ArrayList(23424)
DoSomething(list)
// We need to make list clear
list.Clear();
DoSomething(list)

6.3 Avoid boxing and unboxing

Int x = 200;
Object a = x;
i= (int)a;

Boxing floods the heap with lots of small objects and puts additional pressure on GC.

6.4 StringBuilder

Do not concatenate the strings. String methods never modify the original string they make a copy and return results. Use StringBuilder methods instead.

using System.Text;

var sb = new StringBuilder("Title: ");
Console.WriteLine(sb); // Title:
Console.WriteLine($"Length: {sb.Length}"); // 10
Console.WriteLine($"Capacity: {sb.Capacity}"); // 16

sb.Append(" Manual");
Console.WriteLine(sb); // Title: Manual
Console.WriteLine($"Length: {sb.Length}"); // 22
Console.WriteLine($"Capacity: {sb.Capacity}"); // 32

sb.Append(" in C#");
Console.WriteLine(sb); // Title: C# Guide
Console.WriteLine($"Length: {sb.Length}"); //
Console.WriteLine($"Capacity: {sb.Capacity}"); // 32

6.5 Correct using LINQ

Do not call .ToList() in LINQ expressions. Avoid calling ToList() method in LINQ expressions as it allocates memory for the entire collection.

int[] numbersArray = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Filtering even numbers and converting to List
List<int> evenNumbersList = numbersArray.Where(n => n % 2 == 0).ToList();

// Modifying the List
evenNumbersList.Add(12);

// Displaying results
Console.WriteLine("Using ToList():");
Console.WriteLine("Even Numbers List: " + string.Join(", ", evenNumbersList));

6.6 Test Parallelization

If you need memory optimization - remove parallelization of tests. Running tests in parallel increases memory consumption significantly.

/// <summary>
/// Verify the links that navigate to counts
/// </summary>
[Test]
[Parallelizable]
public void NavigationToDocumentCounts()
{
    try
    {
        NavigationToDocumentCountsPrivate();

        LogTestCase(GetCurrentMethod());
    }
    catch (Exception ex)
    {
        LogTestCase(GetCurrentMethod(), ex.Message, ex);
    }
}

6.7 Quit and Dispose methods in WebDriver / Playwright

For Web ui tests use Quit and Dispose methods in TearDown section for Selenium/Playwright.

[SetUp]
public void SetupTest()
{
    Driver = WebDriverFactory.GetDriver();
}

[TearDown]
public void TearDown()
{
    if (Driver != null)
        Driver.Quit();
}

6.8 Using "using" keyword

Release resources explicitly: Use "using" statements for objects that implement IDisposable (e.g., HttpClient, Stream, DbContext). This will ensure memory is released timely.

using (var client = new HttpClient()) {
// Your test
} // Automatically calls Dispose()

Additional best practices:

[TestCleanup]
public void Cleanup()
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

6.9 Optimizing HTTP Clients and API Requests

public class TestBase
{
protected static readonly HttpClient Client =
new HttpClient();
}

6.10 Working with Data and Mocks

var mock = new Mock<IService>();
mock.Setup(x => x.GetData()).Returns(new
SmallDataObject());

6.11 Use 64-bit mode

Run tests in 64-bit mode: Make sure tests are compiled as AnyCPU with prefer 64-bit—this will allow for efficient use of RAM.

Benefits of 64-bit mode:

  1. Larger address space.
  2. More Efficient Garbage Collection
  3. Better handling of Large Objects
  4. No pointer size limitation
  5. JIT Optimizations

7. When Manual GC Makes Sense?

Manual garbage collection (GC.Collect()) should be used only in special cases, such as:

Note: Overusing GC.Collect() can hurt performance, as it pauses all threads during collection.

Best Practices:

Example in NUnit Test

using NUnit.Framework;
using System;

namespace MemoryOptimizationTests
{
    [TestFixture]
    public class GarbageCollectorTests
    {
        [Test]
        public void TestMemoryOptimization()
        {
            // Setup: create a lot of temporary objects
            for (int i = 0; i < 1_000_000; i++)
            {
                var temp = new byte[1024];
            }

            // Force GC to clean up
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();

            // Measure memory usage
            long memoryUsed = GC.GetTotalMemory(forceFullCollection:
false);
            Console.WriteLine($"Memory after GC: {memoryUsed / 1024 /
1024} MB");

            Assert.Less(memoryUsed, 200 * 1024 * 1024, "Memory usage is
too high after GC");
        }
    }
}

8. Span collections in C#

Span<T> is a structure that allows you to work with a continuous region of memory without allocating new arrays.

It can point to:

The main goal: reduce heap allocations and speed up data operations.

Creating a Span from an array

No copying — just direct memory view.

int[] numbers = { 1, 2, 3, 4, 5 };
// Span referencing the whole array
Span<int> span = numbers;
// Create a slice without copying
Span<int> slice = span.Slice(1, 3); // elements 2,3,4
slice[0] = 99; // modifies the original array!
Console.WriteLine(string.Join(", ", numbers));
// Output: 1, 99, 3, 4, 5

Working with parts of an array without allocations. You can manipulate parts of an array directly without creating new ones.

byte[] buffer = new byte[100];
Span<byte> header = buffer.AsSpan(0, 10);
Span<byte> body = buffer.AsSpan(10, 90);

Using stackalloc

Creates memory on the stack, which is very fast and GC-free:

Span<int> stackSpan = stackalloc int[5] { 10,
20, 30, 40, 50 };
int sum = 0;
foreach (var x in stackSpan) sum += x;
Console.WriteLine(sum); // 150

Stack memory is automatically freed when the method ends — safe and efficient.

Parsing substrings without allocations (ReadOnlySpan<char>)

string data = "Name: Viktor; Age: 35";
// Without Span:
string namePart = data.Substring(6, 6); // allocates a
new string
// With ReadOnlySpan:
ReadOnlySpan<char> span = data.AsSpan();
ReadOnlySpan<char> nameSlice = span.Slice(6, 6);
Console.WriteLine(nameSlice.ToString()); // Viktor

Finding characters in a string:

string text = "key=value;id=123";
ReadOnlySpan<char> span = text.AsSpan();
int equalsIndex = span.IndexOf('=');
var key = span.Slice(0, equalsIndex);
var value = span.Slice(equalsIndex + 1);
Console.WriteLine($"Key: {key.ToString()}, Value:
{value.ToString()}");

You can parse strings quickly, without allocations and without Split. This is particularly useful for high-performance parsing scenarios.

Copying data without allocations

int[] source = { 1, 2, 3, 4, 5 };
int[] destination = new int[5];
Span<int> srcSpan = source;
Span<int> destSpan = destination;
srcSpan.CopyTo(destSpan);

Working with binary data:

byte[] packet = new byte[8];
Span<byte> span = packet;
// Write an int (4 bytes) into the beginning
BitConverter.TryWriteBytes(span, 12345);
// Read it back
int value = BitConverter.ToInt32(span);
Console.WriteLine(value); // 12345

When working with binary data or performing bulk operations, Span<T> allows you to copy data between regions of memory without creating temporary arrays.

Using Memory<T> together with Span<T>

If you need to hold Span-like data beyond the current method, use Memory<T> — it provides a .Span property:

Memory<int> memory = new int[5];
Span<int> span = memory.Span;
span[0] = 42;
Console.WriteLine(memory.Span[0]); // 42

Example in a test

[Test]
public void Should_Parse_Csv_Line_Efficiently()
{
    string line = "Viktor,35,Ukraine";
    ReadOnlySpan<char> span = line.AsSpan();
    int firstComma = span.IndexOf(',');
    var name = span.Slice(0, firstComma);
    int secondComma = span.Slice(firstComma +
1).IndexOf(',') + firstComma + 1;
    var age = span.Slice(firstComma + 1, secondComma -
firstComma - 1);
    var country = span.Slice(secondComma + 1);
    Assert.AreEqual("Viktor", name.ToString());
    Assert.AreEqual("35", age.ToString());
    Assert.AreEqual("Ukraine", country.ToString());
}

This test parses a CSV line with zero memory allocations, making it highly efficient for performance-critical automation scenarios.

9. Conclusion

Proper use of the Garbage Collector, Span collections, clearing the collections, disposing methods, and attentive monitoring of memory usage in C# Automation tests help maintain memory stability, avoid leaks, and ensure consistent performance.

However, manual GC invocation should be applied carefully and purposefully, keeping in mind the natural flow of the .NET runtime.

By implementing these optimization techniques strategically in your automation test suite, you can significantly reduce memory overhead, improve test execution speed, and ensure your tests run reliably even in resource-constrained CI environments.


Viktor Losev

Software Developer in Test

  • Education: Kyiv Polytechnic Institute
  • Favorite language: C#
  • Experience: 13+ years in IT
  • Companies: GlobalLogic, SoftServe, 3Shape, Ciklum, Outbrain, AMFG, Capgemini
  • Speaker: Selenium Camp