A time.Ticker runs the same idea on a schedule: your code wakes on a channel at a fixed interval until you stop it. That answers most golang ticker and golang time ticker searches—polling, heartbeats, cache refresh, or progress logs. It is different from a one-shot time.Timer: timers are for “later once,” tickers are for “again and again.”
For background on goroutines, channels, and defer, those guides pair with the patterns below. For cooperative shutdown, stopping goroutines fits the done / context sketches here.
Tested on: Go 1.22 on 64-bit Linux; snippets were run with
go runwhile this article was revised.
Quick answer: repeated work with time.NewTicker
time.NewTicker creates a ticker that sends on C every interval until you call Stop. Defer Stop when the owning function returns so ticks do not outlive the scope.
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for i := 0; i < 3; i++ {
<-ticker.C
fmt.Println("tick", i+1)
}
}You should see three tick lines spaced about half a second apart, then the program exits.
What Is a Ticker in Go?
A ticker is the right tool when something should happen on a wall-clock interval: cleanup every ten minutes, a status line every second, or polling an HTTP endpoint every thirty seconds. You create it with time.NewTicker(duration), read ticks from ticker.C, and call ticker.Stop() when the schedule should end.
When to use time.NewTicker
Use a ticker when the natural shape is “wait for the next tick on a channel,” often combined with select and other channels. If you only need to pause the current goroutine in a tight loop with no cancellation, time.Sleep can be simpler; tickers shine when you mix periodic work with shutdown or I/O.
How ticker.C works
Each tick delivers a time.Time (the tick time). The value is mainly a signal; many programs ignore the timestamp and run fixed work when the receive completes.
Basic Golang Ticker Example
Create the ticker once, loop until you are done, then stop. A for range ticker.C loop runs the body once per tick; break leaves the loop (defer still runs Stop).
package main
import (
"fmt"
"time"
)
func main() {
tk := time.NewTicker(200 * time.Millisecond)
defer tk.Stop()
n := 0
for range tk.C {
n++
fmt.Println("tick", n)
if n >= 5 {
break
}
}
fmt.Println("done")
}Five tick lines print, then done.
Run code every few seconds
Use any positive time.Duration: time.Second, 3*time.Minute, or time.Duration(100)*time.Millisecond.
Stop ticker when work is complete
Always pair NewTicker with Stop when the ticker’s lifetime matches a function or goroutine. Stop is idempotent and safe to call after you leave the receive loop.
Use Ticker with select
select lets one goroutine wait for ticks and for a stop signal at the same time. That avoids relying on ticker.C closing after Stop—it does not close.
Listen for ticker events
Stop ticker using done channel or context
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(300 * time.Millisecond)
done := make(chan struct{})
go func() {
for {
select {
case <-done:
return
case t := <-ticker.C:
fmt.Println("tick at", t.Format("15:04:05.000"))
}
}
}()
time.Sleep(900 * time.Millisecond)
ticker.Stop()
close(done)
time.Sleep(50 * time.Millisecond)
fmt.Println("stopped")
}A few tick lines appear, then stopped after shutdown.
Run Ticker in a Goroutine
Long-running servers usually start a ticker inside a worker goroutine, pass configuration in, and unblock shutdown with context.Context or a done channel. The rules stay the same: create the ticker where its lifetime is clear, stop it when work ends, and make sure the goroutine can return so defer and cleanup run.
Start background recurring work
The goroutine that owns the ticker should call defer ticker.Stop() so the ticker always stops when that goroutine returns, even on error paths.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
t := time.NewTicker(200 * time.Millisecond)
defer t.Stop()
for i := 0; i < 3; i++ {
<-t.C
fmt.Println("background tick", i+1)
}
}()
wg.Wait()
fmt.Println("worker finished")
}Three background tick lines print from the worker, then worker finished after Wait returns.
Stop background ticker safely
Stopping only the ticker does not unblock a plain for range ticker.C. Combine the ticker with context so the goroutine can exit when the context is canceled, then stop the ticker from the controller side.
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 700*time.Millisecond)
defer cancel()
t := time.NewTicker(200 * time.Millisecond)
defer t.Stop()
done := make(chan struct{})
go func() {
defer close(done)
for {
select {
case <-ctx.Done():
return
case <-t.C:
fmt.Println("tick")
}
}
}()
<-ctx.Done()
<-done
fmt.Println("shutdown clean")
}You should see several tick lines while the context is active, then shutdown clean after the worker returns.
Stop and Reset a Ticker
Use ticker.Stop to stop future ticks
Stop turns off the ticker. It does not close C, so a for range ticker.C does not end from Stop alone—combine with break, return, or select.
Use ticker.Reset to change the interval
Reset(d) stops the ticker and schedules the next tick after duration d (must be positive). Use it when the interval changes at runtime; be careful if multiple goroutines touch the same ticker without synchronization.
package main
import (
"fmt"
"time"
)
func main() {
t := time.NewTicker(100 * time.Millisecond)
defer t.Stop()
<-t.C
fmt.Println("first tick")
t.Reset(400 * time.Millisecond)
<-t.C
fmt.Println("after reset")
}You get two spaced receives reflecting the shorter then longer gap.
Timer vs Ticker in Go
time.Timer |
time.Ticker |
|
|---|---|---|
| Fires | Once (unless Reset) |
Repeatedly until Stop |
| Typical constructor | time.NewTimer(d) |
time.NewTicker(d) |
| Channel | timer.C |
ticker.C |
| Common uses | Timeout, single delay | Polling, heartbeat, periodic jobs |
Use a timer or time.After when you need one event in the future. Use a ticker when the same work should run on a cadence.
Common Mistakes with Go Ticker
Forgetting to stop ticker
Call Stop when the ticker is no longer needed so the runtime can release the timer. Defer Stop next to NewTicker in the same function when lifetimes match.
Expecting ticker.Stop to close ticker.C
After Stop, the channel is not closed. Design shutdown with done, context, or an explicit break.
Creating ticker inside a loop
Avoid:
for {
t := time.NewTicker(...) // leaks if not stopped each iteration
}Create one ticker for the loop’s scope, or stop the previous ticker before replacing it.
Assuming every tick is delivered
If the receiver blocks longer than the interval, ticks can coalesce: you may not get one goroutine wake per theoretical tick. For hard real-time guarantees you need a different design.
Using ticker when time.Sleep is enough
A single loop with time.Sleep is fine when there is no select and no early exit. Prefer a ticker when ticks are part of a channel-based event loop.
time.Tick vs time.NewTicker
time.Tick(d) returns <-chan time.Time but does not let you call Stop. Long-lived programs that stop receiving can keep the ticker alive. Prefer NewTicker plus Stop whenever shutdown or tests need a clean end.
Two tickers in parallel (bounded)
When you run multiple tickers, stop both and avoid ending main on a blocking select {}. sync.WaitGroup keeps the example short.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
fast := time.NewTicker(400 * time.Millisecond)
slow := time.NewTicker(800 * time.Millisecond)
defer fast.Stop()
defer slow.Stop()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for n := 0; n < 3; n++ {
<-fast.C
fmt.Println("fast", n+1)
}
}()
go func() {
defer wg.Done()
for n := 0; n < 2; n++ {
<-slow.C
fmt.Println("slow", n+1)
}
}()
wg.Wait()
fmt.Println("all done")
}Interleaved fast and slow lines print until each goroutine finishes its fixed count.
Go Ticker Cheat Sheet
| Goal | Approach |
|---|---|
| Repeating interval | time.NewTicker(d) |
| Read tick | <-ticker.C or for range ticker.C |
| Shutdown | ticker.Stop() plus done or ctx.Done() in select |
| Change interval | ticker.Reset(d) |
| One-shot delay | time.NewTimer or time.After |
| Simple pause in loop | time.Sleep |
Which time function should you use?
- Cadence with channels:
NewTicker. - One deadline:
NewTimer/After. - Quick blocking pause:
Sleep.
Summary
time.NewTicker is how you run golang ticker style work on a fixed interval: receive from C, stop with Stop, and optionally Reset the period. Stop does not close the channel, so combine tickers with select and a stop signal for clean goroutine exits. Prefer NewTicker over time.Tick when you need lifecycle control, and reach for a Timer when the job only fires once.

