Know when to break up with Go's http.DefaultClient
These might be the first set of snippets that you see when trying to use Go’s HTTP client. (taken from the “overview” section of the standard library docs)
1 | resp, err := http.Get("http://example.com/") |
The same set of snippets has the potential to cause your first production outage. It is perfectly good code (up to a certain point). Things will start to get dirty when you introduce the following things into the mix:
- when your program is starting to make a lot of HTTP calls.
- when your program is making HTTP calls to more than one service (host names).
The reason behind it is this little variable declared in the net/http
package.
1 | var DefaultClient = &Client{} |
Meet the DefaultClient
DefaultClient
is of type *http.Client
and http.Client
is the struct that has all the code to perform HTTP calls. DefaultClient
is a HTTP client with all the underlying settings pointing to the default values.
When you try calling those package-level HTTP methods like http.Get
, http.Post
, http.Do
etc., the HTTP call is made using the DefaultClient
variable. Two fields in the http.Client
struct could translate the “default” and “shared” behavior of http.DefaultClient
into potential problems:
1 | type Client struct { |
The default value for Timeout
is zero, so the http.DefaultClient
does not timeout by default and will try to hold on to a local port/socket as long as the connection is live. What if there are too many requests? Combine this with an HTTP server which doesn’t timeout. Bingo! You got a production outage. You will run out of ports and there won’t be newer ports available for making further HTTP calls.
Next up is the Transport
field in the http.Client
. By default, the following DefaultTransport
would be used in DefaultClient
.
1 | var DefaultTransport RoundTripper = &Transport{ |
(a lot of things in there, but turn your attention to MaxIdleConns
)
Here is the doc on what it does:
1 | // MaxIdleConns controls the maximum number of idle (keep-alive) |
Since the DefaultClient
is shared, you might end up making calls to more than one service (host names) from it. In that case, there might be an unfair distribution of the MaxIdleConns
maintained by the default client for the given set of hosts.
A small example
Let us take an example here:
1 | type LoanAPIClient struct {} |
Both LoanAPIClient
and PaymentAPIClient
use the http.DefaultClient
by calling into http.Get
and http.Post
. Let us say our program makes 80 calls from LoanAPIClient
initially and then makes 200 calls from PaymentAPIClient
. By default DefaultClient
only maintains 100 maximum idle connections. So, LoadAPIClient
will capture 80 spots in those 100 spots, and PaymentAPIClient
will only get 20 remanining spots. This means that for the rest of 60 calls from PaymentAPIClient
, a new connection needs to be opened and closed. This will cause unnecessary pressure on the payments API server. The allocation of these MaxIdleConns will soon get out of your hands! (trust me 😅)
How do we fix this?
Increase the MaxIdleConns
? Yes, you can but if the client is still shared between LoanAPIClient
and PaymentAPIClient
then that too shall get out of hand at some scale.
I discovered the sibling of MaxIdleConns
and that is MaxIdleConnsPerHost
.
1 | // MaxIdleConnsPerHost, if non-zero, controls the maximum idle |
This could help in maintaining a predictable number of idle connections for each endpoint (host name).
OK, how do I really fix this?
If your program is calling into more than one HTTP service, then you might most probably want to tweak other settings of the Client too. So, it might be beneficial to have a separate http.Client
for these services. That way we can fine-tune them if needed in the future.
1 | type LoanAPIClient struct { |
It is fine
The conclusion would be this: It is okay to use http.DefaultClient
to start with. But if you think you will have more clients and will make more API calls, avoid it.
Bonus: If you are authoring a library that has an API client, do a favor for your users: provide a way to customize the http.Client
that you are using to make API calls. That way, your users have full control of what they would like to achieve while using your client.
~ ~ ~ ~
HTTP Clients inside an HTTP Server talking to another HTTP Server that has HTTP Clients, all authored by you. That will be your cue.