I deployed a Mastodon bot a couple weeks ago that polls a US National Weather Service RSS feed for an airport in my county and posts to Mastodon when it updates. I was using the HTTP
“one-shot” methods that spin up an HTTP connection, send the request, then terminate the connection.
I expected this bot to use very little RAM (less than 10MB) because it’s doing very little:
- Makes an HTTP request to get the RSS feed
- Parses the RSS feed
- Iterates over each feed item (there’s only one)
- If it hasn’t been posted yet (tracked by a key in Redis or a record in SQLite), make an HTTP request to the Mastodon API
- Sleep for 1 second
GOTO 1
That’s all it does. The whole thing is only 61 lines of code including whitespace.
What surprised me was that RAM consumption grew over time.
The first two lines show linear memory growth. I restarted the app and it happened again. Then I tried a few more experiments:
- Replace SQLite with Redis
- Replace the RSS feed parser with using stdlib
XML
directly - Remove all XML parsing and just use regexes (yes, I know, don’t worry about it)
- Compile the app using
--mcpu
for the specific aarch64 CPUs I’m using on GCP - Cap memory usage to 20MB so it’ll just restart when it exceeds it
- Use a persistent HTTP connection
The first 4 didn’t do anything useful. Capping memory usage actually caused an additional problem — it caused the app to crash after persisting that it was posting to Mastodon but before actually posting.
The punchline is in the image above, but after almost 24 hours using a persistent HTTP connection (with a connection pool) for both the RSS feed and the Mastodon API, memory usage has tapered at about 5MB. This is the only thing that kept RAM stable at single-digit MB.
That makes me wonder if there’s something in OpenSSL (either in the version of OpenSSL itself that’s included in the 84codes Alpine-based container images or in the Crystal bindings for it) that’s causing it to leak memory. I’ve only ever seen this kind of memory growth in Ktistec, which is why I thought it might be something in SQLite but it happening when running one-shot HTTP requests would also track.
Regardless of the reason, if you want to minimize memory consumption in an app that talks to other HTTP services, reusing HTTP connections will do that.