# How `context.AfterFunc` Prevents Goroutine Leaks Over Manual `ctx.Done()` Watching

> Discover how context.AfterFunc prevents goroutine leaks by registering callbacks directly with context cancellation, avoiding manual ctx.Done() watching and memory accumulation.

- Repository: [JetBrains/go-modern-guidelines](https://github.com/jetbrains/go-modern-guidelines)
- Tags: deep-dive
- Published: 2026-09-04

---

**`context.AfterFunc` registers cleanup callbacks directly with a context's internal cancellation state, eliminating the dedicated goroutines that block on `<-ctx.Done()` and accumulate as memory leaks.**

Manually spawning goroutines to monitor context cancellation is a prevalent source of resource exhaustion in Go applications. The `JetBrains/go-modern-guidelines` repository explicitly recommends `context.AfterFunc` as the modern alternative, demonstrating how this standard-library helper achieves the same functional result without the overhead of orphaned goroutines.

## The Manual `ctx.Done()` Pattern That Causes Leaks

When executing cleanup logic after cancellation, the conventional approach launches a new goroutine for every registration:

```go
func doWork(ctx context.Context) {
    go func() {
        <-ctx.Done()           // blocks until context cancellation
        cleanup()              // runs cleanup after cancellation
    }()
    // … other work …
}

```

This pattern creates **one dedicated goroutine per watcher**. Each goroutine consumes stack memory and scheduler resources while blocking on the channel receive. Even after `cleanup()` executes, the goroutine lingers until garbage collection. Under high concurrency—such as per-request processing or connection pooling—these accumulators become a classic *goroutine leak*, wasting memory and CPU cycles indefinitely.

## How `context.AfterFunc` Eliminates Goroutine Leaks

The `context.AfterFunc` helper, stabilized in Go 1.21, provides equivalent functionality without spawning additional goroutines.

**No Extra Goroutine Overhead**

When you invoke `context.AfterFunc(ctx, cleanup)`, the runtime stores the callback inside the context's existing cancellation machinery rather than launching a new goroutine. According to the guideline definition in [`internal/guidelines/guidelines.json`](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/guidelines/guidelines.json) (lines 1162-1167), this avoids "starting a goroutine whose only job is to wait on `ctx.Done()`". The context's internal manager—which already handles cancellation propagation—invokes your callback when `Done` closes.

**Cancelable Registration**

The function returns a `stop` function that removes the callback registration when no longer needed:

```go
func doWork(ctx context.Context) {
    stop := context.AfterFunc(ctx, cleanup) // registers cleanup with context
    defer stop()                            // removes registration if no longer needed
    // … other work …
}

```

Calling `stop()` ensures that if the guarded operation completes successfully before cancellation, no lingering callback remains attached to the context. This deterministic cleanup prevents the accumulation of obsolete registrations that would otherwise consume memory until the parent context expires.

## Source Implementation in JetBrains/go-modern-guidelines

The leak-prevention rationale is documented in [`internal/guidelines/guidelines.json`](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/guidelines/guidelines.json) (lines 1162-1167), which states: "`context.AfterFunc` registers work to run after cancellation and returns a stop function." The concrete examples in lines 1170-1178 contrast the manual goroutine approach with the `AfterFunc` implementation, providing the before-and-after migration path.

The [`internal/guidelines/guidelines.go`](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/guidelines/guidelines.go) file parses this JSON schema via `schema.Parse` and exposes the guideline to the project's CLI tooling, enabling static analysis tools to enforce this anti-leak pattern across Go codebases.

## Summary

- **Manual `ctx.Done()` watching** spawns a goroutine per registration that blocks indefinitely, causing resource exhaustion under high concurrency.
- **`context.AfterFunc`** registers callbacks within the context's existing cancellation infrastructure without creating new goroutines.
- **The returned `stop` function** allows explicit unregistration, ensuring cleanup resources are released deterministically rather than waiting for context expiration.
- **Implementation evidence** in `JetBrains/go-modern-guidelines` specifically documents this pattern in [`internal/guidelines/guidelines.json`](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/guidelines/guidelines.json) as a critical practice for production Go code.

## Frequently Asked Questions

### What exactly constitutes a goroutine leak?

A goroutine leak occurs when spawned goroutines never exit, continuing to consume memory and scheduler resources indefinitely. In the context of manual watching, each `go func() { <-ctx.Done(); cleanup() }()` remains alive until the context cancels—even if the parent operation already completed or the cleanup became unnecessary—creating unbounded growth during high-load scenarios.

### How does `context.AfterFunc` differ from `context.WithCancel`?

`context.WithCancel` creates a derived context node that can be canceled manually, returning a `cancel` function. `context.AfterFunc` attaches a callback to an existing context's cancellation event without creating a new context tree node. It specifically targets post-cancellation execution rather than cancellation propagation, and it returns a `stop` function for unregistering the callback.

### When should I call the stop function returned by `context.AfterFunc`?

Invoke the stop function using `defer` immediately after registration, or call it explicitly when the guarded operation completes successfully before context cancellation. This removes the callback from the context's internal registry, preventing it from executing unnecessarily and freeing associated memory immediately rather than at context expiration.

### Is `context.AfterFunc` available in all Go versions?

No, `context.AfterFunc` was introduced in **Go 1.21**. Codebases running earlier versions must continue using manual `ctx.Done()` watching with careful goroutine lifecycle management, or implement custom callback registries to achieve similar leak-prevention semantics.