The Secret Life of Go: The Context Package

Published: (January 19, 2026 at 11:08 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

How to stop runaway goroutines and prevent memory leaks

How to stop runaway goroutines and prevent memory leaks.

Ethan: “I have a memory leak.”
Eleanor: “Do you?” (not looking up)
Ethan: “Well, not a leak, exactly. But look at my dashboard.” (points to a monitor showing a jagged, climbing line) “This is my API. Every time a user searches for a product, I spawn a goroutine to query the database and the recommendation engine. If the user gets bored and closes their browser, my server keeps working.”
Eleanor: “Because you didn’t tell it to stop.” (walks over)
Ethan: “I thought the HTTP handler handled that?”
Eleanor: “It handles the connection. Your goroutines are independent. You fired them and walked away. They are still running, burning CPU cycles for a user who has already left the building. You’re being rude to your server.”
Ethan: “So how do I tell them the user left?”
Eleanor: “You pass the Context.”

The First Argument

Eleanor pulled up a chair.

“In Go, context.Context is the standard way to carry deadlines, cancellation signals, and request‑scoped values across API boundaries. It is almost always the first argument of a function.”

She opened Ethan’s code.

// Bad: No way to stop this function once it starts
func SlowDatabaseQuery(id string) string {
    time.Sleep(5 * time.Second) // Simulate work
    return "Product Details for " + id
}

func HandleSearch(w http.ResponseWriter, r *http.Request) {
    // We ignore the request context!
    result := SlowDatabaseQuery("12345")
    fmt.Fprintln(w, result)
}

“If I hit this endpoint and cancel the request after one second, your SlowDatabaseQuery still sleeps for the full five seconds. Multiply that by a thousand users, and your server crashes.”

She refactored the code.

// Good: We accept a Context
func SlowDatabaseQuery(ctx context.Context, id string) (string, error) {
    // Use a select statement to listen for cancellation
    select {
    // …
    }
}

Look at the select block. We are racing two things: the work finishing or the context finishing. ctx.Done() is a channel that closes when the context is canceled. If the user disconnects, ctx.Done() fires immediately, and your function returns. You save four seconds of work.

The Timeout (Setting Boundaries)

“That handles cancellation,” Ethan said. “But what if the database is just broken and hangs forever? The user waits, the connection stays open…”

“Then you set a deadline,” Eleanor replied. “Never let a process run forever. We use context.WithTimeout.”

She created a new example.

func CallExternalAPI() error {
    // 1. Create a derived context that dies after 100 ms
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    // 2. ALWAYS defer the cancel function to release resources
    defer cancel()

    // 3. Pass this strict context to the worker
    req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://slow-api.com", nil)
    client := &http.Client{}

    _, err := client.Do(req)
    return err
}

“This is aggressive.”
“If the external API is slower than that, we don’t want the answer,” Eleanor said firmly. “This protects your system. If their server hangs, yours doesn’t pile up thousands of waiting connections. You fail fast and move on.”

“And defer cancel()?”
“Crucial. If the work finishes in 10 ms, the timer is still ticking in the background. Calling cancel() stops the timer and frees the memory immediately. Always defer cancel.”

The Value (Use with Caution)

“I saw context.WithValue in the docs. Can I use that to pass user objects and config settings down to my functions?”

Eleanor frowned.

“You can, but you usually shouldn’t. WithValue is for request‑scoped data—like a request ID or an authentication token. It is untyped and invisible. If you use it to pass required function arguments, you make your code opaque.”

“Opaque?”

“If a function needs a database connection, pass it as an argument: func(db *DB). Do not hide it inside ctx where no one can see it. Explicit is better than implicit.”

The Chain of Command

Ethan looked at his dashboard. The memory usage was flattening out.

“It propagates,” he realized. “If I cancel the parent context in HandleSearch, it cancels the child context in SlowDatabaseQuery, which cancels the HTTP request to the database…”

“Exactly,” Eleanor said. “It is a chain of command. When the top level says ‘stop,’ the order goes all the way down the tree. Every function cleans up its own mess and returns.”

She stood up, picked up her stack of punch cards, and said:

“A server is not a trash can, Ethan. Do not fill it with abandoned processes. Be polite. When the user leaves, stop working.”

Key Concepts from Chapter 16

ConceptWhat It Does
context.ContextCarries deadlines, cancellation signals, and request‑scoped values.
First argumentFunctions that do work should accept a Context as their first parameter.
select on ctx.Done()Allows work to stop early when the context is canceled.
context.WithTimeout / WithDeadlineEnforces a hard upper bound on how long work may run.
defer cancel()Releases resources (timers, goroutine leaks) as soon as the work finishes.
context.WithValueUse sparingly—for metadata only, not for required arguments.
PropagationCanceling a parent context automatically cancels all derived child contexts.

By consistently passing and respecting Context, you prevent runaway goroutines, avoid memory leaks, and keep your server responsive.

Go Context Overview

A context carries deadlines, cancellation signals, and request‑scoped values. It is immutable and thread‑safe.

ctx.Done()

  • A channel that closes when the context is canceled or times out.
  • Use it in a select statement to return early if work is no longer needed.

context.Background()

  • The root context.
  • Use this when you are starting a main function or a top‑level process and have no existing context to inherit from.

context.WithCancel(parent)

ctx, cancel := context.WithCancel(parent)
  • Returns a copy of the parent context with a new Done channel.
  • Calling the returned cancel() function closes that channel.

context.WithTimeout(parent, duration)

ctx, cancel := context.WithTimeout(parent, duration)
  • Returns a copy of the parent that automatically cancels after the specified duration.

Best Practice:
Always defer cancel() to release resources as soon as the function returns, even if the timeout hasn’t fired.

context.TODO()

  • A placeholder context.
  • Use it when you are refactoring and haven’t decided where the context should come from yet.

context.WithValue

ctx = context.WithValue(parent, key, value)
  • Used for passing request‑scoped data (e.g., Trace IDs).
  • Do not use it for optional parameters or core dependencies (like loggers or database handles). Explicit arguments are clearer.

Next chapter: JSON and Tags. Ethan tries to parse a configuration file and learns that Go’s struct tags are the closest thing the language has to magic.


Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Back to Blog

Related posts

Read more »

The Secret Life of Go: Concurrency

Bringing order to the chaos of the race condition. Chapter 15: Sharing by Communicating The archive was unusually loud that Tuesday. Not from voices, but from t...