HttpClient requires an anti-pattern for performance

During the development of the Cloud Replicator app, we ran into strange issues with HttpClient. In this video, I show and discuss how to get HttpClient to be fast and reliable.

https://youtu.be/baN0UHlCxEQ

In this video, Erik demonstrates a surprising performance issue (and reliability problem) with the HttpClient data type in AL for Business Central. The fix involves what many would consider an anti-pattern: using a global variable instead of a local one. Erik walks through the problem, explains why it happens under the hood, and shows benchmark results that prove the performance difference is significant — especially when making external HTTP calls.

The Problem: HttpClient and Socket Exhaustion

The HttpClient data type in AL is the primary way to communicate with web services from Business Central. A typical usage pattern looks straightforward: declare a local HttpClient variable, call Get (or Post, Put, Send), read the response, and exit the procedure. Each time the procedure is called, a new HttpClient instance is created and then disposed of when the procedure ends.

However, under the hood, AL’s HttpClient is backed by .NET’s System.Net.Http.HttpClient, and that class has a well-known quirk: it tries to pool sockets and reuse connections for performance. When you create and dispose of HttpClient instances rapidly, the underlying sockets don’t get released immediately — they linger in a TIME_WAIT state for approximately four minutes. If you’re making many calls in a loop, you will eventually exhaust the available sockets on the machine.

Erik discovered this the hard way while building a “Cloud Replicator” app — an application that moves data from Business Central to a cloud database as fast as possible using multiple sessions. After roughly 28,479 calls in one test run, Client.Get simply stopped working. Shortly after, the entire machine became unresponsive because the socket pool was completely exhausted.

The critical issue is that in AL, developers cannot control IDisposable behavior the way normal .NET developers can. The AL runtime manages the lifecycle of .NET objects, so there’s no way to explicitly use patterns like using blocks or HttpClientFactory — the recommended solutions in standard .NET development.

The Anti-Pattern Fix: Use a Global Variable

The solution — and the reason Erik calls it an “anti-pattern” — is to declare the HttpClient as a global variable on the object rather than a local variable inside the procedure. This way, the same HttpClient instance is reused across multiple calls, which is exactly what the underlying .NET class was designed for. It keeps connections alive and reuses sockets efficiently.

Here’s the final version of Erik’s test code:

pageextension 50123 CustomerListExt extends "Customer List"
{
    actions
    {
        addfirst(processing)
        {
            action(Http)
            {
                Caption = 'HttpClient Test';
                ApplicationArea = all;
                trigger OnAction()
                var
                    s: Time;
                    i: Integer;
                begin
                    s := Time();
                    for i := 1 to 100 do begin
                        if not HttpTest() then
                            error('Failed after %1 calls', i);
                    end;
                    message('Succes! %1', Time() - S);
                end;
            }
        }
    }
    procedure HttpTest(): Boolean
    var
        //Client: HttpClient;  // <-- DO NOT use a local variable!
        Resp: HttpResponseMessage;
        Txt: Text;
    begin
        if Client.Get('https://www.google.com', Resp) then begin
            if Resp.IsSuccessStatusCode() then begin
                Resp.Content().ReadAs(Txt);
            end;
            exit(true);
        end;
        exit(false);
    end;

    var
        Client: HttpClient;  // <-- Global variable: reused across calls
}

Notice that the Client variable is declared as a global variable at the bottom of the page extension, outside any procedure. The commented-out local declaration inside HttpTest() shows the original (problematic) approach.

Error Handling: Don't Forget the Return Value

Before diving into the performance testing, Erik also highlights an important point from the official documentation: the Get method (as well as Send, Post, and Put) returns a Boolean. If the call fails completely — meaning it couldn't even establish a connection — the return value is false, and attempting to access properties on the HttpResponseMessage will cause a runtime error.

From the docs:

"Accessing the HttpContent property of the message in case when the request fails will result in an error. If you omit this optional return value and the operation does not execute successfully, a runtime error will occur."

So always check the return value:

if Client.Get(url, Resp) then begin
    // Safe to access Resp here
    ...
    exit(true);
end;
exit(false);

Benchmark Results

Erik ran several benchmarks to quantify the performance difference. Here's a summary of what he observed:

Local Calls (within Docker, port 8080)

  • 500 calls with local variable: ~1.48 seconds
  • 1500 calls with local variable: ~4.0–4.3 seconds
  • 1500 calls with global variable: ~2.9–3.0 seconds

For local connections, the improvement was noticeable but not dramatic — roughly 25–30% faster.

External Calls (to google.com)

  • 100 calls with local variable: ~15 seconds
  • 100 calls with global variable: ~7 seconds

When making actual external network calls, the difference was roughly 2x — the global variable approach cut execution time nearly in half. This makes sense because the connection setup overhead (DNS resolution, TCP handshake, TLS negotiation) is far more expensive for remote connections, and reusing the HttpClient instance allows all of that to be amortized across calls.

Why This Matters

This issue has two dimensions:

  1. Reliability: With a local variable in a loop, you will eventually exhaust the system's available sockets. The exact threshold depends on the machine's state and what else is running, but Erik hit it at around 28,000 calls. In production scenarios with multiple sessions running concurrently, this number could be much lower.
  2. Performance: Even before you hit socket exhaustion, you're paying a significant performance penalty by creating new connections for every call instead of reusing them. For external endpoints, the overhead can double your total execution time.

Conclusion

If you're using HttpClient in AL — especially in loops or high-frequency scenarios — declare it as a global variable on your object rather than a local variable inside your procedures. While using global variables is generally considered an anti-pattern in most programming contexts, it's the correct approach for HttpClient in AL because AL doesn't provide the tools (.NET's IHttpClientFactory or explicit disposal control) to manage the underlying connection lifecycle properly.

Review your existing code for any instances where HttpClient is declared locally inside procedures that are called repeatedly. Making this one simple change can double your throughput on external API calls and prevent mysterious socket exhaustion errors from bringing down your environment.