Open-source project
KimMachineGun/automemlimit avatar
KimMachineGun/automemlimit

automemlimit: keeping Go's GC inside a container's memory limit

Automatically set GOMEMLIMIT to match Linux cgroups(7) memory limit.

570 stars23 forksGoMIT

At a glance

What is it?
automemlimit is a small Go package that reads the Linux cgroup memory limit and sets GOMEMLIMIT to match it, so the garbage collector starts working before the container is killed rather than after. The import-and-forget form is a single blank import, and the v1.0 API adds an explicit Set function, a lower bound option and a provider fallback chain.
Who is it for?
Adopt automemlimit if you deploy Go services in containers with a memory limit and you see them killed by the OOM killer instead of returning errors, since the Go runtime does not know about cgroup limits on its own. Do not adopt it if your containers have no memory limit, in which case there is nothing to match and the fallback will pick up system memory instead, which is usually not what you want for a shared host.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem is that Go cannot see the limit

automemlimit automatically sets GOMEMLIMIT based on the Linux cgroups(7) memory limit, and the readme links to the Go documentation on the memory limit for the detail.

The underlying issue is a mismatch between two components that each know only half the picture. The Linux cgroup gives a container a hard memory limit, and exceeding it means the kernel kills the process, usually with no stack trace and no chance to return an error. The Go runtime has a soft memory limit, GOMEMLIMIT, which tells the garbage collector to work harder as the heap approaches a threshold. It is soft in the sense that exceeding it is not fatal, but it is the mechanism that makes the runtime behave well as memory runs short.

The Go runtime does not consult the cgroup limit on its own, so a container with a 512 megabyte limit and a Go service inside it will happily grow the heap past 512 megabytes and get killed, rather than collecting more aggressively and staying alive. The cgroup limit is enforced hard and immediately. GOMEMLIMIT is a hint the collector uses to start working earlier. Setting one from the other is a small piece of glue that closes a real gap.

The dependency list is a hint about the implementation. The module requires github.com/pbnjay/memory at a pinned revision, and that library is the well known Go package for reading total system memory, which is what makes the system memory fallback possible at all. The module targets Go 1.21.0, and the licence is MIT.

One blank import, or an explicit call

There are two ways to use the package, and the difference is whether you want the defaults or control.

The simple form is a blank import, and the comment in the example explains what it does: importing this package sets GOMEMLIMIT automatically from the cgroup memory limit. By default it sets GOMEMLIMIT to 90% of the cgroup memory limit, and you can set the AUTOMEMLIMIT environment variable to a ratio in the range from zero exclusive to one inclusive, or to off. The import is a blank import, so nothing is called by you:

go
package main

// Importing this package sets GOMEMLIMIT automatically from the cgroup memory limit.
// By default, it sets GOMEMLIMIT to 90% of the cgroup memory limit.
// Set the AUTOMEMLIMIT environment variable to a ratio in (0.0, 1.0], or "off".
import _ "github.com/KimMachineGun/automemlimit"

The blank import runs its setup in the package initialiser, which means the limit is in place before your main function starts. For most services that is exactly right, since the alternative is a window at startup where the heap is unbounded.

For more control, you use memlimit.Set directly:

go
package main

import (
	"log"

	"github.com/KimMachineGun/automemlimit/memlimit"
)

func main() {
	_, err := memlimit.Set(
		memlimit.WithRatio(0.9),
		memlimit.WithMin(100*1024*1024),
		memlimit.WithProvider(
			memlimit.ApplyFallback(
				memlimit.FromCgroup,
				memlimit.FromSystem,
			),
		),
	)
	if err != nil {
		log.Fatal(err)
	}
}

The options map onto the decisions you would otherwise make by hand. WithRatio sets the fraction of the limit to use. WithMin sets a lower bound for GOMEMLIMIT, so a container with a very small limit does not end up with a GC target so low that it thrashes. WithProvider chooses where the limit comes from, and ApplyFallback chains them, trying the cgroup first and falling back to system memory.

That last option is the important one, because the fallback changes the meaning of the setting. Reading the cgroup limit is what you want in a container. Reading system memory is what you want on a bare machine, and it is a different number entirely.

What v1.0 changed, and why the API was reshaped

The notice section is worth reading closely, because a reshaped public API is a breaking change and the project is explicit about it.

Version v1.0.0 simplifies the public API and improves cgroup memory limit detection, and the key changes from the 0.x line are listed. memlimit.Set replaces the memlimit.SetGoMemLimit* functions, so the entry point is one function rather than a family of them. memlimit.Set returns the previous GOMEMLIMIT instead of zero when configuration is skipped or an error occurs, which is a small but real change for anyone checking the return value to work out what happened. memlimit.Set sets GOMEMLIMIT to math.MaxInt64 when the provider returns memlimit.ErrNoLimit, which is the correct encoding of no limit for the Go runtime. memlimit.FromCgroup replaces the version-specific cgroup providers, so there is one provider name rather than a family of them. memlimit.WithEnv, AUTOMEMLIMIT_EXPERIMENT and AUTOMEMLIMIT_DEBUG were removed, with the advice being to use memlimit.FromSystem for the system memory fallback. memlimit.WithRefreshInterval now takes a context.Context for cancellation, so a background refresher can be stopped. And memlimit.WithMin was added to set a lower bound for GOMEMLIMIT.

The removal of the environment variable overrides is the part that will catch people out in an upgrade. The blank import form still honours AUTOMEMLIMIT, but the experimental and debug switches are gone, and the way to get the old behaviour is now to call Set with the provider you want.

The refresh interval option is what makes this more than a one-shot setting. The cgroup limit of a container can change while it runs, and a memory limit that shrinks under a running Go service is exactly the case where a static GOMEMLIMIT is wrong. Refreshing it on an interval with a context that can cancel it is the mechanism for keeping the two in step.

The examples directory reflects the range of use: a dynamic example, a logger example and a system example, which map onto the three cases of a changing limit, a service that wants to report the limit it applied, and a run outside a container.

Installing it, and the shape of the package

The installation is one line, and it is a Go module dependency rather than a separate binary or a sidecar:

shell
go get github.com/KimMachineGun/automemlimit@latest

What you get is a library, not a service, and the repository layout reflects that. There is a root automemlimit.go file, which is the package whose import does the automatic setup, and a memlimit directory that holds the configurable API. The examples directory holds three separate cases, a dynamic one, a logger one and a system one, which map onto the three ways you can end up needing this: a limit that changes, a service that wants to say what limit it applied, and a process running outside a container where the cgroup value does not exist.

The go.mod is worth a second look for what it is not carrying. Apart from the system memory library, there are no dependencies, and the module targets Go 1.21.0. There is no cgroup parsing library in the list, which means the cgroup reading is implemented in the package itself rather than pulled in. That is consistent with the v1.0 change that replaced the version-specific cgroup providers with a single FromCgroup, and it is a reasonable thing for a package this small to do rather than depend on.

The build and test story is visible in the badges rather than described. There is a Go Reference badge pointing at the package documentation on pkg.go.dev, a test workflow badge, and a license badge. A package with a single dependency and a public API this small does not need much in the way of a build system, and the absence of one is a positive signal rather than a gap.

The recent releases are v1.0.0 from 2026-08-25, v0.7.5 from 2025-10-23 and v0.7.4 from 2025-07-11, with the last push on 2026-09-02. The jump from a 0.7 patch line to 1.0 in a single release is the kind of version bump that signals the author is done reshaping the API, and the notice section is where that promise is made explicit.

What to think about before you turn it on

The mechanics are simple enough that the interesting part is the number you choose.

The default is 90% of the cgroup limit, and that default is a reasonable first guess rather than a law. GOMEMLIMIT accounts for the Go heap, not for the whole process. There is memory outside the heap that the collector cannot reclaim: goroutine stacks, the runtime's own structures, memory held by cgo or by an allocator the runtime does not own, and buffers in flight. Setting GOMEMLIMIT to the full cgroup limit means the collector is told to wait until the moment the container is already in trouble. Setting it too low means the collector works constantly and the service trades throughput for headroom it did not need.

This is where WithMin earns its place. A container with a 64 megabyte limit and a service whose live set is 40 megabytes is a bad pairing, and a GOMEMLIMIT of 57 megabytes would have the collector fighting for space that does not exist. The lower bound option lets you say, in effect, never go below this, whatever the ratio produces.

The second thing to think about is what happens when there is no cgroup limit. The fallback chain in the example tries the cgroup first and then system memory, and the system memory number on a shared host is the memory of the machine, not of your service. A GOMEMLIMIT derived from that is either useless or actively harmful, depending on how large the host is. If your deployment has no cgroup limit, the tool has nothing useful to match and you should think about whether you want the system fallback at all.

The third is the interaction with your other memory settings. If your service already sets GOMEMLIMIT, or if you run with a memory limit and expect the OOM killer to be your backstop, this changes the shape of the failure. With automemlimit the process degrades first and dies later, and that is usually what you want, but it changes your alerting, because you will see timeouts and slow responses rather than a clean kill.

Editorial conclusion

Adopt automemlimit if you deploy Go services in containers with a memory limit and you see them killed by the OOM killer instead of returning errors, since the Go runtime does not know about cgroup limits on its own. Do not adopt it if your containers have no memory limit, in which case there is nothing to match and the fallback will pick up system memory instead, which is usually not what you want for a shared host. Before you commit, check the ratio you are choosing, because a limit set too close to the container limit leaves no headroom for non-heap memory, and read the v1.0 changes, since the public API was reshaped and the environment variable overrides were removed.

Frequently asked questions

What does automemlimit do?

It automatically sets GOMEMLIMIT based on the Linux cgroups(7) memory limit. The Go runtime does not read the cgroup limit itself, so a container with a memory limit would otherwise be killed by the kernel rather than having its garbage collector work harder first. The default is 90% of the cgroup limit.

How do I use automemlimit in a Go program?

With a blank import, which sets the limit in the package initialiser before main runs, and the AUTOMEMLIMIT environment variable to set a ratio between zero exclusive and one inclusive, or off. For more control, call memlimit.Set with WithRatio, WithMin, WithProvider and ApplyFallback options.

What changed in automemlimit v1.0.0?

The public API was simplified: memlimit.Set replaces the memlimit.SetGoMemLimit* functions and memlimit.FromCgroup replaces the version-specific providers. memlimit.Set now returns the previous GOMEMLIMIT rather than zero on skip or error, and sets it to math.MaxInt64 when the provider returns memlimit.ErrNoLimit. The AUTOMEMLIMIT_EXPERIMENT and AUTOMEMLIMIT_DEBUG variables were removed, and WithRefreshInterval now takes a context.Context.

What does the ApplyFallback provider option do?

It chains providers so the first one that returns a usable limit wins, and the example uses it to try the cgroup limit first and then fall back to system memory. The fallback matters because the system memory number is the whole machine rather than your container, so it is usually not what you want in a containerised deployment.

What is the memlimit.WithMin option for?

It sets a lower bound for GOMEMLIMIT, added in v1.0.0. It exists so that a container with a very small memory limit does not end up with a garbage collection target so low that the collector thrashes, which is the failure mode a plain ratio produces when the limit is small relative to the live set.

Official sources

  1. Issues
  2. KimMachineGun/automemlimit on GitHub
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/kimmachinegun-automemlimit.svg)](https://hysenlabs.com/projects/kimmachinegun-automemlimit)

Was this useful?