We built an API gateway on top of Gin. After deploying it, we found that memory would surge in a short time and then the process would be OOM-killed. The memory trend looked like this:
Zooming in on one occurrence:
In the second chart you can see how fast memory grows: nearly 2 GB in a minute and a half.
For problems where memory spikes this fast, it’s hard to catch the state in time with pprof — unless you write a script that samples pprof on a timer.
Root cause: every request cached the response body
Look at the code again: the logic keeps a complete copy of every response body in memory. For requests with large response bodies, that alone causes memory to spike — a log download, for example.
Fix: skip caching by request type
Once the cause was clear, the fix was simple: skip file-download requests based on the response headers, and skip SSE and WebSocket requests based on the request headers — streaming responses are of little value in the audit log, and they also break JSON serialization.
// serialize the Body and put it in the ctx // the response Body is read so it can be written to the audit log }
// AbnormalPrint logs diagnostic info on abnormal conditions func AbnormalPrint(ctx *gin.Context, typ string, rawResp string) { // actual code omitted }
Postmortem: when you copy code, mind your own use case
The real point of this post is not how the problem was fixed, but this:
When you copy code, pay attention to whether it fits your context.
Reuse existing libraries instead of rolling your own.
While hand-writing the API gateway, we also ran into the following:
The first version of the network handling was hand-written too, which meant it handled the various Content-Types badly;
Because we had to parse the body, and didn’t have the capacity to support every compression scheme, compression had to be forcibly disabled at the gateway;
For example: we ship a browser-based terminal that connects to a K8S cluster over WebSocket. Suppose the service behind that connection is service A (browser <—-> gateway <—-> service A <—-> K8S cluster). Later, some requests going through the gateway would hit service A directly — skipping the gateway’s API router entirely and reusing the WebSocket connection — even when those APIs did not belong to service A.
A sketch of the first hand-written version of the request handling: