Introduction
If you run Node in production, sooner or later you'll come across a common bug known as a memory leak.
This was the case with my current side project, expose. I wrote a popular article Six Ways To Drastically Boost Your Productivity As A Node Developer which mentioned it, then suddenly the server was overloaded.
During the period of high load, I could see that a memory leak was forming.
What is expose
?
expose
is a command line app that makes its simple to give a public URL to any web based app you have running locally on your machine. So if you have a local API runnig at http://localhost:8000
and you then run expose 8000
, it will generate a public URL that you can then share.
It works by creating a websocket connection between the client and the expose service, which listens with websocket, http and https. When requests come in to the public URL, they are routed through the websocket connection to the client and then the client hits your server locally.
This has various uses like demoing early work without needing to deploy code anywhere and debugging webhook integrations.
You can install it for Linux, Mac and Windows here
The leak
In the expose
server, I have a Singleton class called Proxy
, which is in TypeScript, the superset of JavaScript with type safety.
This class manages all client connections to the expose service. Anytime you run expose
to get a public url for your project running on localhost, a Websocket connection is created between the client and the service. Those connections are stored in Proxy.connections
.
This is a trimmed down version of the Proxy
class. The real version has extra logic, such as finder methods to help route requests to the right client websocket so that you see your site, not someone elses when you hit the public URL.
import Connection from "./connection";
import HostipWebSocket from "./websocket/host-ip-websocket";
export default class Proxy {
private static instance : Proxy;
connections : Array<Connection> = [];
addConnection(hostname: string, websocket: HostipWebSocket, clientId: string): void {
const connection : Connection = {
hostname,
clientId,
websocket
};
this.connections.push(connection);
}
....
More methods to find the right connections, avoid duplicates etc...
....
listConnections() {
return this.connections;
}
public static getInstance(): Proxy {
if (!Proxy.instance) {
Proxy.instance = new Proxy();
}
return Proxy.instance;
}
}
Every time a client connects, addConnection()
is called. The problem here is that when they disconnect, the Websocket connection stays alive and their entry in Proxy.connections
stays there.
So as more clients connect, the Proxy.connections
array gets bigger and bigger. This is a classic memory leak.
Before the article, this wasn't such an issue as few people were connecting to and using the service. After the article, the server had to deal with more connections, then ran out of memory. I ended up upgrading the instance to a bigger one, which handled the load even with the memory leak.
Fixing the leak
Once the problem was apparent, I went about fixing the leak.
In addConnection()
, I started tagging websocket connections with the client id of the connecting client.
addConnection(hostname: string, websocket: HostipWebSocket, clientId: string): void {
// Tag the connection so it can be found and destroyed later
// when the client disconnects
websocket.exposeShClientId = clientId;
const connection : Connection = {
hostname,
clientId,
websocket
};
this.connections.push(connection);
}
I also added a deleteConnection()
method to the Proxy
class to handle the actual deletion of connections, so they could then be cleaned up by the garbage collector.
deleteConnection(clientId: string) {
for (let i = 0; i < this.connections.length; i++) {
const connection = this.connections[i];
if (connection.clientId === clientId) {
this.connections.splice(i, 1);
}
}
}
I then added a hook on the websocket connections so that when they close, their associated Connection
is deleted
websocket.on('close', (code: number, reason: string) => {
websocket.terminate();
const proxy = Proxy.getInstance();
proxy.deleteConnection(websocket.exposeShClientId);
});
Once this was done, connections in Proxy.connections
were cleaned up as clients disconnected. No more endlessly growing array and no more memory leak.
Conclusion
Memory leaks are common in Node as servers often run as a single process. Anything left over from each connection that grows will cause a memory leak.
So keep an eye out for them next time you see your instance running out of memory.
Tip: If you want to basically almost eliminate memory leaks, consider trying out PHP, my other favorite language. Each request is a separate process so it is basically stateless. It wouldn't work for expose
, because the server needs to maintain state with the connections.
To introduce a memory leak into a PHP application would take alot of effort - not just a bug in the code but also very bad misconfiguration. This is one of the better parts of PHP as you are protected from these kinds of bugs.
Top comments (0)