When should you use time.NewTimer instead of time.After for a timeout in Go?
answer
- one of them hands back a handle
- what can you do with a handle
- stop it, or re-arm it
- one allocation per loop pass, or one total
- Stop and Reset are the whole difference
basics
~20 sUse time.NewTimer when you need the handle: it returns a *time.Timer with a C channel plus Stop and Reset, so one timer can be cancelled and reused. time.After returns only a channel, allocating a fresh timer per call.
solid answer
~50 s`time.After(d)` is a convenience wrapper that hands back only a channel, so the timer it creates cannot be stopped or reused — every call allocates a new timer and a new channel. `time.NewTimer(d)` returns a `*time.Timer` with a `C` field and `Stop`/`Reset` methods, which is what you want in three situations: when the timeout is no longer needed and you want to release it early (`defer t.Stop()`), when the same deadline is re-armed on every pass of a loop (`t.Reset(d)` instead of a fresh allocation), and when a single overall deadline must survive many `select` iterations rather than restarting each time. In a sensor-polling agent running on a device with a tight memory ceiling, reusing one timer per loop rather than calling `time.After` thousands of times a second is a real, measurable difference in allocation and GC pressure.
code
go · 11 linest := time.NewTimer(interval)
defer t.Stop()
for {
select {
case r := <-readings:
_ = r // process the reading
case <-t.C:
// no reading arrived within interval
}
t.Reset(interval) // Go 1.23+: no drain needed first
}go deeper
Know that time.After is the one-line form and time.NewTimer is the version that gives you an object with Stop and Reset. Being able to name the C field is enough at this level.
Be ready to explain the three concrete wins from holding the handle — cancel early, re-arm instead of reallocate, and hold one overall deadline across loop iterations — and to write the reused-timer loop.
Demonstrate that you know the Go 1.23 change: unreferenced timers are now collectable, so 'time.After leaks' is outdated, and growing timer memory means your own code is retaining them. Tie the choice to measured allocation, not folklore.
Own the guidance: when a convenience API is fine and when a component's hot path justifies the more verbose form, and how you keep timeout durations configurable so a device with a tight memory ceiling can be tuned without a code change.
## The two APIs ```go func After(d Duration) <-chan Time func NewTimer(d Duration) *Timer ``` `After` is literally the convenience form: it creates a timer and returns only its channel, throwing the handle away. `NewTimer` gives you the `*time.Timer`, whose useful surface is: - the field `C <-chan time.Time` — the same one-shot channel `After` would have returned; - `Stop() bool` — prevent the timer firing; the bool reports whether it stopped a timer that had not yet fired; - `Reset(d Duration) bool` — re-arm the timer for a new duration. ## Reason one: you can stop it A timeout you no longer need is still a scheduled timer. When a function may return long before its deadline, `defer t.Stop()` releases it at once instead of leaving it pending. With `time.After` there is no handle at all, so "stop it early" is not an option you have. This matters most in the shape this leaf cares about: a long-running agent that arms a deadline for every unit of work. If the work almost always completes quickly, the timers that did not fire are pure waste, and the only way to reclaim them promptly is to hold a handle and call `Stop`. ## Reason two: you can reuse it ```go t := time.NewTimer(interval) defer t.Stop() for { select { case r := <-readings: _ = r // process the reading case <-t.C: // no reading arrived within interval } t.Reset(interval) } ``` One timer, re-armed each pass. The `time.After` version of the same loop calls `time.After(interval)` on every iteration, and each call allocates a timer plus a channel. On a hot loop that is the difference between one allocation and millions. ## Reason three: the deadline can be an overall budget Because `time.After` is re-evaluated every time the `select` executes, a `time.After` case inside a loop gives an **idle** timeout: the clock restarts whenever anything else happens. Creating the timer once, before the loop, and selecting on the same `t.C` each pass gives a **total** budget that really does expire at a fixed moment. The two are different contracts and choosing the wrong one is a classic bug. ## What is not a reason any more Older advice said `time.After` leaks. That was true in a specific sense: before Go 1.23, the timer behind `time.After` was retained by the runtime until it actually fired, so a loop creating one-minute timeouts thousands of times a second accumulated live timers and grew the heap with uptime. Go 1.23 changed this — timers and tickers that the program no longer refers to become eligible for garbage collection immediately, even when `Stop` was never called. On current Go the argument for `NewTimer` is control and allocation cost, not leakage. Saying "time.After leaks" without that qualification dates you. What still costs you on any version is a timer you *do* still reference — one held in a struct field, a map of per-item deadlines, or a pending `time.AfterFunc` whose callback must still run. Those are alive because your program keeps them alive, and only calling `Stop` and dropping the reference removes them. If a heap profile of a long-lived agent shows timer-related allocation climbing with uptime, that registry is where to look, not the individual `select`. ## Small facts worth having - `Timer.Stop` does **not** close `C`. Nothing ever closes a timer channel; `Stop` only prevents a future send. Code that ranges over `t.C` waiting for it to end will wait forever. - `Stop` returning `false` means the timer had already fired or already been stopped. - `time.Tick(d)` is the same convenience trade one level up: it wraps `time.NewTicker` and returns only the channel, so there is no `Ticker` to `Stop`. It also returns `nil` when `d <= 0`, and a receive on a nil channel never proceeds. - Creating a timer is cheap but not free; it is a runtime object placed in a per-processor timer structure, and re-arming an existing one avoids that setup.
- How does time.Tick differ from time.NewTicker, and is it safe to use?`time.Tick` is the same convenience trade: it wraps `NewTicker` and returns only the channel, so there is no `*time.Ticker` to `Stop`. Before Go 1.23 that meant the ticker was never reclaimed, which is why the docs pushed you to `NewTicker`; since 1.23 an unreferenced ticker is collectable even unstopped. It also returns nil for a non-positive duration, and a nil channel never delivers.
- Does Timer.Stop close the timer's channel?No. Nothing ever closes a timer channel — closing it would make waiting receivers see a zero-valued "tick" that never happened. `Stop` only prevents a future send, so after `Stop` a receive on `C` simply blocks. That is why `for range t.C` is always wrong and why you detect "stopped" from your own state, not from the channel.
- A long-running agent's heap profile shows timer allocation climbing with uptime. Where do you look?At timers your own code still references: a map or struct field of per-item deadlines that is never pruned, or pending callbacks that must still run. Since Go 1.23 an unreferenced pending timer is collectable, so a growing count points at retention you own. The fix is to `Stop` the timer when the work it guards finishes and to delete it from whatever holds it.
saying these in an interview costs you the question
- Says time.After leaks on current Go without qualification
- Claims Stop closes the timer's channel
- Thinks Reset allocates a new timer anyway, so reuse is pointless
- Believes a time.After case in a loop holds one fixed deadline
- Assumes Stop returning false means an error occurred