This is a personal note that I decided to share. It reflects my understanding of a subject and may contain errors and approximations. Feel free to contribute by contacting me here. Any help will be credited!


Recently, with my team, we ran into a network issue involving a dual-stack host (IPv4 and IPv6) in a Flutter project. We explored the Dart SDK and uncovered some interesting details about its networking. This note summarizes what I learned.

Thanks to Adrien Audouard for his contribution and his review.

The Dart SDK includes an HTTP Client in dart:io package, providing a simple and lightweight client to communicate with servers. It leverages platform-specific code for low-level network operations. In most applications, the Dart HTTP client is a Singleton (cf. Singleton Pattern).

dio is a library built on top of the Dart HTTP Client. It adds support for several common features such as:

  • Interceptors.
  • Automatic JSON Deserialization.
  • Timeouts.

Internally, dio uses a Dart HTTP Client. dio is widely used in the Dart community, especially with the framework Flutter.

Internal Structure of dart:io HTTP Client: Connection Management & Pooling

The implementation of the Dart HTTP client is located in files http.dart and http_impl.dart.

The HTTP client stores a map of _ConnectionTarget instances, each representing a remote server. Targets are keyed by host, port, and scheme. For example, if your client communicates with https://alexis-segura.com and https://google.com, then it will have two connection targets.

Each target maintains two sets of _HttpClientConnection objects:

  • _idle connections: waiting to handle requests.
  • _active connections: currently handling requests.

Those two sets form a connection pool. Each connection is a TCP socket with the server. When multiple requests are made, connections are reused, which improves performance. When there is no available connection in the _idle set, a new connection is created, according to the maxConnectionPerHost property. When the pool reaches the maximum number of connections to a host, it does not open a new connection right away. Instead, it queues up a connection request using a Completer. When one of the existing connections closes, Dart dequeues the next connection request.

It is possible to provide a connectionFactory to override the default connection behavior.

DNS, sockets, and connection logic

In Dart HTTP Client, most of the code is written in Dart. However, Dart delegates specific network actions to platform code (native code from the underlying OS).

On a new connection, the client:

  • Performs DNS lookup. It returns IPv4 and IPv6 (if supported). This lookup is performed by platform code.
  • Establishes a socket connection using a RawSocket for HTTP, or a SecureSocket with a RawSocket for HTTPS.
  • Uses _NativeSocket inside RawSocket to bridge Dart code with the platform’s socket APIs.

A diagram to understand how it works (probably inaccurate)

Connection Initialization: A Non-Standard Approach

As we saw, Dart leverages only specific networking actions from the underlying OS. It means that several parts of HTTP communications are managed by Dart code. This is the case to connect to remote hosts over TCP after DNS lookup.

Let’s say we want to perform a GET on an API. We initiate a call to https://google.com, the client:

  • Creates a _ConnectionTarget and a _HttpClientConnection.
  • Instantiates a RawSocket and a _NativeSocket.
  • A DNS lookup is performed, returning a set of IPv4 and IPv6 (in the case of dual-stack hosts).
  • Then the method tryConnectToResolvedAddresses in _NativeSocket is called, to connect over TCP to one of the returned IPs.
  • Once the connection is set up, the request is made.

The tryConnectToResolvedAddresses method can be found in the socket_patch.dart file. If we take a look at it, we can see that it attempts to connect to each resolved IP address in sequence. If a connection does not succeed within 250ms (default _retryDuration), it moves on to the next address. This pattern is known as staggered parallelism, an approach where connection attempts are made in sequence with a short delay.

However, the way Dart is managing this connection initialization does not follow the standard defined by IETF: RFC 6555 and RFC 8305. Dart does not interleave IPv6 and IPv4 addresses. As a result, if the first addresses are from the same family (e.g., IPv6) and if it fails or is slow, the user may experience a delay before the fallback (e.g., IPv4) is attempted.

⚠ Happy Eyeball Algorithm

In order to manage dual-stack hosts, the IETF specified an algorithm in 2012: Happy Eyeball (RFC 6555). An updated version has been released in 2017: Happy Eyeball Version 2 (RFC 8305).

The RFC abstract states :

When a server’s IPv4 path and protocol are working, but the server’s IPv6 path and protocol are not working, and a dual-stack client application experiences significant connection delay compared to an IPv4-only client. This is undesirable because it causes dual– stack client to have a worse user experience. This document specifies requirements for algorithms that reduce this user-visible delay and provides an algorithm.

Happy Eyeball algorithm triggers several connection attempts in parallel, first with IPv6, then with IPv4 after a short delay. The first connection that succeeds will be used.

Alternatives: Platform’s HTTP Clients

Dart developers are not locked in with the Dart HTTP Client. It is possible to use other clients, especially the ones available on the underlying platform.

If you are using dio, the Dart community developed a package called native_dio_adapter. It provides Android (cronet_http), iOS, and macOS (cupertino_http) adapters for dio.

While the Dart HTTP client delegates certain tasks, like DNS resolution, to the platform, it bypasses the complete platform networking stack.

By using cronet_http or cupertino_http with dio, the application instantiates and uses the complete network layer of the platform. OS like Android and iOS have more complete APIs, that follow standards much more closely. Notably, iOS has implemented the Happy Eyeball algorithm since version 5 (2011).

References