Why `time.Tick` in Go 1.23+ No Longer Needs Explicit `Stop` Calls for Garbage Collection
Starting with Go 1.23, tickers created by time.Tick are automatically reclaimed by the garbage collector when they become unreferenced, eliminating the goroutine leaks that previously required manual Stop calls.
The time.Tick function in Go has historically been a source of subtle resource leaks, but a runtime change in Go 1.23 fundamentally alters how these tickers interact with garbage collection. According to the JetBrains/go-modern-guidelines repository, this improvement allows developers to use time.Tick in infinite loops without explicit cleanup, though specific use cases still benefit from time.NewTicker with manual lifecycle management.
The Pre-1.23 Goroutine Leak Problem
Before Go 1.23, time.Tick created a goroutine that continuously sent ticks on a channel until explicitly stopped. If you forgot to call Stop() on the returned ticker, the underlying goroutine would persist indefinitely, holding memory and scheduler resources even after the ticker variable went out of scope.
This behavior made the following pattern dangerous in long-running applications:
// Pre-1.23: This leaked a goroutine every time the function was called
func doWork() {
for range time.Tick(time.Second) {
// work...
}
}
Without a reference to the Ticker returned by time.Tick, there was no way to stop it, yet the goroutine kept the channel open and the ticker alive, preventing garbage collection.
How Go 1.23 Made Tickers GC-Recoverable
The Go 1.23 runtime introduced a change that makes internal tickers recoverable. When a time.Tick value becomes unreachable—that is, no goroutine holds a reference to it—the garbage collector can now stop the underlying ticker and release its resources automatically.
As documented in the source analysis, this means the runtime no longer requires an explicit Stop() call to prevent goroutine leaks. The ticker detects when it has been abandoned and cleans up its internal goroutine and channel resources during the next garbage collection cycle.
This change specifically targets the time_tick_gc guideline documented in the repository's guidelines file.
The JetBrains Go Modern Guidelines Reference
The JetBrains/go-modern-guidelines repository catalogs this improvement under Guideline ID: time_tick_gc in internal/guidelines/guidelines.json.
According to the guideline entry (lines 605-613), the recommendation is:
- Since version: 1.23
- Category: Time
- Impact: Low
- Guidance: "Use
time.Tickwhen it fits; Go 1.23 can recover unreferenced tickers without requiringStopfor GC."
The project's FEATURES.md (lines 842-845) further clarifies: "Go 1.23 made unreferenced tickers recoverable by the garbage collector," while CHANGELOG.md (line 37) notes the "Improved time.Tick guidance for Go 1.23."
Practical Code Examples
Safe Pattern for Infinite Loops (Go 1.23+)
When using time.Tick for an infinite loop where the ticker lives for the application's lifetime, explicit stopping is unnecessary:
// Go 1.23+: No defer ticker.Stop() needed
for range time.Tick(time.Second) {
fmt.Println("tick")
}
When to Use time.NewTicker Instead
If you need to stop the ticker early, reset its interval dynamically, or bound the loop execution, use time.NewTicker with explicit lifecycle management:
ticker := time.NewTicker(time.Second)
defer ticker.Stop() // explicit stop required for early cleanup
for {
select {
case <-ticker.C:
fmt.Println("tick")
case <-done:
return
}
}
Summary
- Go 1.23 runtime change: Tickers from
time.Tickare now garbage-collectible when unreferenced, preventing goroutine leaks without explicitStop()calls. - Source reference: The
time_tick_gcguideline ininternal/guidelines/guidelines.jsondocuments this behavior for the JetBrains/go-modern-guidelines project. - Safe usage:
time.Tickis appropriate for infinite loops where the ticker runs for the application's lifetime. - Explicit control: Use
time.NewTickerwithdefer ticker.Stop()when you need early termination, interval resets, or bounded execution contexts.
Frequently Asked Questions
Can I safely use time.Tick in short-lived functions now?
Yes, in Go 1.23 and later, you can safely call time.Tick inside functions without storing the ticker reference, provided you are using it for an infinite for range loop. The garbage collector will reclaim the ticker and its goroutine once the function exits and no references remain.
Does this mean I should never call Stop on tickers again?
No. You should still call Stop() when using time.NewTicker or when you have a reference to a ticker that you want to terminate before it becomes unreachable. The GC-only cleanup applies specifically to the anonymous tickers created by time.Tick that you do not store in variables.
What happens if I store the ticker from time.Tick in a variable?
If you assign the return value of time.Tick to a variable that remains reachable, the ticker will continue running until that variable goes out of scope or is set to nil. Only when the ticker becomes fully unreachable will the garbage collector stop it and reclaim resources.
Is there a performance cost to the new GC-recoverable tickers?
The impact is categorized as "Low" in the repository's guidelines. The change primarily adds a finalizer or similar mechanism to detect unreachable tickers. For high-frequency ticker creation and destruction, time.NewTicker with explicit Stop remains more predictable and may offer slightly better performance by avoiding GC overhead.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →