DEV Community

Kum Jude Bama
Kum Jude Bama

Posted on

What a Restaurant Waiter Taught Me About Go’s Context: Cancel Work Before It Becomes Waste

I went to a restaurant to grab a meal. Well... the waiting time was crazy.
As I sat there waiting, I noticed a guy who had clearly reached his limit. He signalled the waiter and said he couldn't wait any longer.
The waiter immediately signalled the kitchen to cancel the order.

That moment got me thinking.

What happens when a customer cancels an order while the kitchen is preparing it?

If the kitchen has already started preparing the meal, continuing to cook it means the restaurant may end up with food that nobody wants.
The waiter’s signal to the kitchen helps prevent that waste.

And then my backend engineering brain kicked in:

This is exactly the kind of problem Go’s context.Context helps us solve.

Let me explain.

The Restaurant Analogy

Imagine a restaurant with three main actors:

  1. The customer.
  2. The waiter.
  3. The kitchen.

Now map that to a typical Go backend application:

Restaurant Go Backend
Customer HTTP client
Waiter HTTP request handler
Kitchen Database / downstream service
Meal preparation Database query / business operation
Cancelled order Cancelled request context
Signal to kitchen Context cancellation
Stop preparing the meal Stop ongoing work and release resources

The waiter receives the customer's order and passes it to the kitchen.
But the waiter also has information about the order's lifecycle.
If the customer says:

"I can't wait anymore. Cancel my order."

The waiter communicates that cancellation to the kitchen.

The kitchen can then stop preparing the meal, provided it hasn't reached a point where stopping is no longer possible.
This is similar to how cancellation propagates through Go's context system.

What Is Go's Context?

In Go, context.Context carries deadlines, cancellation signals, and request-scoped values across API boundaries and between goroutines.

It is commonly used when handling:

  • HTTP requests.
  • Database queries.
  • External API calls.
  • Background operations tied to a request.
  • Concurrent goroutines.

The important part for this article is cancellation.

A context allows one part of your application to communicate:

"This work is no longer needed. Stop as soon as you can."

That is useful because backend systems often perform work that may become unnecessary before it finishes.

For example:

  • A user closes their browser.
  • A mobile app cancels a request.
  • A request deadline expires.
  • A service calling your API times out.
  • A user navigates away from a page.
  • A downstream operation is no longer needed.

Without cancellation, your application may continue doing unnecessary work.

The Restaurant Flow in Go

Let's visualize the analogy.

sequenceDiagram
    actor Customer
    participant Waiter as HTTP Handler
    participant Kitchen as Database
    participant CustomerDB as Database

    Customer->>Waiter: Place order / HTTP request
    Waiter->>Kitchen: Pass order with context
    Kitchen->>CustomerDB: Execute query

    Customer-->>Waiter: Cancel request
    Waiter-->>Kitchen: Context cancellation

    Kitchen-->>CustomerDB: Stop query if supported
    CustomerDB-->>Kitchen: Release resources
    Kitchen-->>Waiter: Cancellation error
    Waiter-->>Customer: Request cancelled

The analogy is not a perfect one-to-one mapping.

In particular, a database is not literally a kitchen, and context cancellation does not guarantee that every database query stops instantly.

But the core idea is the same:

Propagate cancellation to work that is no longer needed.

Let's See It in Go

Imagine we have an API endpoint that fetches a customer's orders.

A client sends:

GET /customers/123/orders
Enter fullscreen mode Exit fullscreen mode

The handler needs to query the database.

A common mistake is to execute the query without using the request's context.

Without Context Cancellation

package main

import (
    "database/sql"
    "net/http"
)

type OrderHandler struct {
    DB *sql.DB
}

func (h *OrderHandler) GetOrders(
    w http.ResponseWriter,
    r *http.Request,
) {
    customerID := 123

    rows, err := h.DB.Query(
        "SELECT id, amount FROM orders WHERE customer_id = ?",
        customerID,
    )

    if err != nil {
        http.Error(
            w,
            "Failed to fetch orders",
            http.StatusInternalServerError,
        )
        return
    }

    defer rows.Close()

    // Process rows...
}
Enter fullscreen mode Exit fullscreen mode

Here, we execute a database query.

But what happens if the client disconnects while the database is still working?

Depending on the database driver and how the query is executed, the database operation may continue even though the HTTP request is no longer useful.

The waiter has effectively lost the customer, but the kitchen may still be preparing the meal.

That is wasted work.

With Context Cancellation

Go's database/sql package provides context-aware methods such as:

QueryContext()
ExecContext()
PrepareContext()
BeginTx()
Enter fullscreen mode Exit fullscreen mode

Let's use QueryContext().

package main

import (
    "context"
    "database/sql"
    "net/http"
)

type OrderHandler struct {
    DB *sql.DB
}

func (h *OrderHandler) GetOrders(
    w http.ResponseWriter,
    r *http.Request,
) {
    ctx := r.Context()

    customerID := 123

    rows, err := h.DB.QueryContext(
        ctx,
        "SELECT id, amount FROM orders WHERE customer_id = ?",
        customerID,
    )

    if err != nil {
        if ctx.Err() != nil {
            http.Error(
                w,
                "Request cancelled",
                http.StatusRequestTimeout,
            )
            return
        }

        http.Error(
            w,
            "Failed to fetch orders",
            http.StatusInternalServerError,
        )
        return
    }

    defer rows.Close()

    // Process rows...
}
Enter fullscreen mode Exit fullscreen mode

The important line is:

ctx := r.Context()
Enter fullscreen mode Exit fullscreen mode

And then:

h.DB.QueryContext(ctx, query, args...)
Enter fullscreen mode Exit fullscreen mode

We are passing the request's context to the database operation.

Now, if the request context is cancelled, the database driver can be notified and may cancel the query.

The kitchen receives the cancellation signal.

How Cancellation Actually Propagates

Let's break it down.

Step 1: The customer places an order

The HTTP client sends a request.

GET /customers/123/orders
Enter fullscreen mode Exit fullscreen mode

The Go server creates a request context.

ctx := r.Context()
Enter fullscreen mode Exit fullscreen mode

The waiter now has the customer's order and its context.

Step 2: The waiter sends the order to the kitchen

The handler passes the context to the database.

rows, err := db.QueryContext(
    ctx,
    "SELECT id, amount FROM orders WHERE customer_id = ?",
    customerID,
)
Enter fullscreen mode Exit fullscreen mode

The kitchen begins preparing the meal.

Step 3: The customer cancels

The client disconnects, or the request deadline expires.

The request context becomes cancelled.

Conceptually:

Customer cancels
      ↓
HTTP request context cancelled
      ↓
Database query receives cancellation
      ↓
Driver attempts to stop the query
      ↓
Resources can be released
Enter fullscreen mode Exit fullscreen mode

This is the signal from the waiter to the kitchen.

Step 4: The database operation stops if cancellation is supported

The database driver receives the cancellation signal and attempts to interrupt the query.

The query may return an error such as:

context.Canceled
Enter fullscreen mode Exit fullscreen mode

or:

context.DeadlineExceeded
Enter fullscreen mode Exit fullscreen mode

The handler can then stop processing the result.

Important: Context Cancellation Is Cooperative

This is where the analogy needs a little technical precision.

In Go, context cancellation is not a forceful kill switch.

Calling:

cancel()
Enter fullscreen mode Exit fullscreen mode

does not magically terminate every goroutine or operation using that context.

Instead, cancellation is a signal.

Code must observe that signal or use APIs that support it.

For example:

select {
case <-ctx.Done():
    return ctx.Err()

case result := <-resultCh:
    return result
}
Enter fullscreen mode Exit fullscreen mode

The goroutine checks whether the context has been cancelled.

If it has, it stops its work.

The same principle applies to database operations.

A context-aware database method can pass cancellation to the driver, but the actual ability to interrupt the database operation depends on the driver and database implementation.

The waiter can send the cancellation signal. The kitchen must be capable of responding to it.

A Practical Go Example: Context-Aware Work

Let's create a small example that simulates a kitchen preparing a meal.

package main

import (
    "context"
    "fmt"
    "time"
)

func prepareMeal(ctx context.Context) error {
    fmt.Println("Kitchen: Started preparing the meal")

    for i := 1; i <= 5; i++ {
        select {
        case <-ctx.Done():
            fmt.Println(
                "Kitchen: Order cancelled. Stopping preparation.",
            )

            return ctx.Err()

        case <-time.After(1 * time.Second):
            fmt.Printf(
                "Kitchen: Preparing step %d/5\n",
                i,
            )
        }
    }

    fmt.Println("Kitchen: Meal ready!")

    return nil
}

func main() {
    ctx, cancel := context.WithCancel(
        context.Background(),
    )

    defer cancel()

    go func() {
        time.Sleep(2 * time.Second)

        fmt.Println(
            "Customer: I can't wait anymore. Cancel my order.",
        )

        cancel()
    }()

    err := prepareMeal(ctx)

    if err != nil {
        fmt.Println("Result:", err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Possible output

Kitchen: Started preparing the meal
Kitchen: Preparing step 1/5
Kitchen: Preparing step 2/5
Customer: I can't wait anymore. Cancel my order.
Kitchen: Order cancelled. Stopping preparation.
Result: context canceled
Enter fullscreen mode Exit fullscreen mode

Instead of continuing until the meal is ready, the kitchen stops when it receives the cancellation signal.

This is the behavior we want when work is no longer useful.

Why This Matters in Backend Engineering

Now let's move beyond restaurants.

Imagine a fintech API handling a payment request.

The request might involve:

  1. Validating the customer.
  2. Checking account details.
  3. Fetching transaction history.
  4. Calling a payment provider.
  5. Writing transaction records.
  6. Returning a response.

What happens if the client disconnects after step 3?

Should every operation continue blindly?

Not necessarily.

Some operations may no longer be needed.

For example, if you are fetching transaction history solely to build a response that the client will never receive, cancellation can prevent unnecessary work.

A context-aware query can help stop that operation.

func (s *PaymentService) GetTransactionHistory(
    ctx context.Context,
    customerID string,
) ([]Transaction, error) {

    rows, err := s.db.QueryContext(
        ctx,
        `
        SELECT id, amount, status
        FROM transactions
        WHERE customer_id = ?
        `,
        customerID,
    )

    if err != nil {
        return nil, err
    }

    defer rows.Close()

    var transactions []Transaction

    for rows.Next() {
        var tx Transaction

        if err := rows.Scan(
            &tx.ID,
            &tx.Amount,
            &tx.Status,
        ); err != nil {
            return nil, err
        }

        transactions = append(
            transactions,
            tx,
        )
    }

    if err := rows.Err(); err != nil {
        return nil, err
    }

    return transactions, nil
}
Enter fullscreen mode Exit fullscreen mode

The handler calls the service with the request context:

func (h *PaymentHandler) GetHistory(
    w http.ResponseWriter,
    r *http.Request,
) {
    ctx := r.Context()

    transactions, err :=
        h.paymentService.GetTransactionHistory(
            ctx,
            "customer-123",
        )

    if err != nil {
        if ctx.Err() != nil {
            return
        }

        http.Error(
            w,
            "Failed to fetch transaction history",
            http.StatusInternalServerError,
        )

        return
    }

    // Write response...
    _ = transactions
}
Enter fullscreen mode Exit fullscreen mode

The cancellation signal travels through the application layers.

HTTP Handler
     │
     │ context.Context
     ▼
Payment Service
     │
     │ context.Context
     ▼
Repository
     │
     │ QueryContext
     ▼
Database Driver
     │
     ▼
Database
Enter fullscreen mode Exit fullscreen mode

This is one of the reasons context is so important in Go architecture.

Context Is Not Just for Databases

The restaurant analogy can be extended to other operations.

1. HTTP Calls to External Services

Imagine your Go service calls another API to retrieve customer information.

req, err := http.NewRequestWithContext(
    ctx,
    http.MethodGet,
    "https://example.com/customers/123",
    nil,
)

if err != nil {
    return err
}

resp, err := http.DefaultClient.Do(req)

if err != nil {
    return err
}

defer resp.Body.Close()
Enter fullscreen mode Exit fullscreen mode

If the context is cancelled, the HTTP request can be cancelled as well.

The waiter has told another kitchen:

"The customer no longer needs this order."

2. Goroutines

Suppose you launch a goroutine to perform background work.

func processOrder(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()

        default:
            // Perform a unit of work.
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The goroutine listens for cancellation.

When the context is cancelled, it exits.

This prevents goroutines from running indefinitely after the request that created them has ended.

3. Deadlines

Sometimes the customer does not explicitly cancel.

They simply say:

"If my meal isn't ready in 10 minutes, I'm leaving."

That is a deadline.

In Go:

ctx, cancel := context.WithTimeout(
    context.Background(),
    5*time.Second,
)

defer cancel()
Enter fullscreen mode Exit fullscreen mode

Now the operation has a maximum duration of 5 seconds.

If it takes longer, the context is cancelled.

err := prepareMeal(ctx)

if err != nil {
    if errors.Is(
        err,
        context.DeadlineExceeded,
    ) {
        fmt.Println("The order took too long.")
    }
}
Enter fullscreen mode Exit fullscreen mode

The restaurant analogy is straightforward:

A deadline is the customer's maximum acceptable waiting time.

Cancellation vs. Business Transactions

There is another important distinction, especially in fintech.

Not every operation should be cancelled simply because the client disconnected.

For example:

Customer submits payment
       ↓
Validate payment
       ↓
Debit account
       ↓
Record transaction
       ↓
Send response
Enter fullscreen mode Exit fullscreen mode

If the client disconnects after the debit has succeeded, you must not assume the payment was cancelled.

A payment may already have been committed.

The restaurant analogy has limits here.

A customer cancelling before the meal is prepared is different from cancelling after the meal has already been served.

In backend systems:

  • Read-only queries are often good candidates for request cancellation.
  • Long-running reports may be cancellable.
  • External API calls may support cancellation.
  • Database writes may need to complete once a transaction has reached a critical point.
  • Financial operations require idempotency, consistency, and clear transaction semantics.

Context cancellation controls the lifetime of work. It does not automatically roll back business actions that have already been committed.

This distinction is critical when designing reliable payment systems.

The Resource Management Benefit

Why should backend engineers care about stopping unnecessary work?

Because every ongoing operation consumes resources.

For example:

Database resources

  • Database connections.
  • Query execution capacity.
  • Memory.
  • CPU.
  • Locks, depending on the operation.

Application resources

  • Goroutines.
  • Memory allocations.
  • Network connections.
  • Worker capacity.
  • CPU cycles.

External service resources

  • API requests.
  • Rate limits.
  • Connection pools.
  • Provider capacity.

If a client has already gone away, continuing unnecessary work can increase system load.
In a high-traffic backend, this can become expensive.
One cancelled request is not a big deal.

But thousands of cancelled requests that continue processing can contribute to resource pressure.

This is why cancellation is not merely a convenience.

It is part of building efficient and resilient services.

Best Practices for Using Context in Go

1. Accept context as the first parameter

func GetOrders(
    ctx context.Context,
    customerID string,
) ([]Order, error)
Enter fullscreen mode Exit fullscreen mode

This makes cancellation and deadlines explicit.

2. Pass context through your application layers

Handler
   ↓
Application
   ↓
Repository
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

Avoid dropping the context somewhere in the middle.

3. Use context-aware APIs

For databases:

db.QueryContext(ctx, query)
db.ExecContext(ctx, query)
Enter fullscreen mode Exit fullscreen mode

For HTTP:

http.NewRequestWithContext(
    ctx,
    method,
    url,
    body,
)
Enter fullscreen mode Exit fullscreen mode

For goroutines:

select {
case <-ctx.Done():
    return ctx.Err()
}
Enter fullscreen mode Exit fullscreen mode

4. Do not create a new background context unnecessarily

Avoid:

func GetOrders(
    ctx context.Context,
) error {
    ctx = context.Background()

    // Original cancellation signal is lost.
    return nil
}
Enter fullscreen mode Exit fullscreen mode

This breaks the cancellation chain.

Prefer:

func GetOrders(
    ctx context.Context,
) error {
    // Continue using the caller's context.
    return nil
}
Enter fullscreen mode Exit fullscreen mode

5. Always release resources

For example:

rows, err := db.QueryContext(
    ctx,
    query,
)

if err != nil {
    return err
}

defer rows.Close()
Enter fullscreen mode Exit fullscreen mode

Cancellation helps stop work, but proper cleanup is still your responsibility.

6. Do not use context values as a general-purpose parameter bag

Context values are intended for request-scoped data that crosses API boundaries, such as trace IDs.

Avoid stuffing business parameters into context:

ctx = context.WithValue(
    ctx,
    "customerID",
    customerID,
)
Enter fullscreen mode Exit fullscreen mode

Prefer explicit function parameters:

func GetOrders(
    ctx context.Context,
    customerID string,
) error
Enter fullscreen mode Exit fullscreen mode

Your code becomes clearer and easier to test.

The Bigger Lesson

That restaurant experience reminded me of a fundamental principle in software engineering:

When work is no longer needed, stop it as early as possible.

Good backend systems are not only about making operations successful.

They are also about knowing when to stop.

A request that has been cancelled should not blindly continue consuming resources if the work can be safely abandoned.

Go's context provides a clean mechanism for communicating cancellation across layers of an application.

From the HTTP handler to the application layer, from the application layer to the repository, and from the repository to the database driver.

Just like the waiter who tells the kitchen:

"The customer cancelled the order. Don't prepare something that will go to waste."

Final Takeaway

The next time you write:

db.QueryContext(ctx, query)
Enter fullscreen mode Exit fullscreen mode

Remember the waiter.

The waiter knows the customer may no longer be waiting.

The kitchen needs to know that too.

And in backend engineering, context is the signal that helps your system know when work should stop.

Go #SoftwareEngineering

Top comments (0)