go / sleepctx

I use context-aware sleep to respect cancellation in retry loops, polling, and rate limiting.

The problem

time.Sleep blocks unconditionally:

func poll(ctx context.Context) error {
    for {
        if done, err := check(); done {
            return err
        }
        time.Sleep(5 * time.Second) // ignores ctx cancellation
    }
}

If the context is canceled, the goroutine still waits the full duration before noticing.

The pattern

func sleepCtx(ctx context.Context, d time.Duration) error {
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-time.After(d):
		return nil
	}
}

Returns immediately if the context is canceled, otherwise waits for the duration.

Usage

func poll(ctx context.Context) error {
    for {
        if done, err := check(); done {
            return err
        }
        if err := sleepCtx(ctx, 5*time.Second); err != nil {
            return err // context canceled
        }
    }
}

Works well in retry loops:

func fetchWithRetry(ctx context.Context, url string) (*Response, error) {
    delays := []time.Duration{1, 2, 4, 8, 16}
    var lastErr error

    for _, delay := range delays {
        resp, err := fetch(ctx, url)
        if err == nil {
            return resp, nil
        }
        lastErr = err

        if err := sleepCtx(ctx, delay*time.Second); err != nil {
            return nil, err
        }
    }
    return nil, lastErr
}

When to use

When not to use

← All articles