In Go, time.Ticker
offers a lightweight way to execute logic at fixed intervals from within your codebase, making it ideal for deployable units like microservices, agents, daemons, or sidecar workers — where the logic is expected to be active only while the service is alive.
What makes Ticker useful?
A Ticker emits a signal after every time.Duration
interval, which can be as precise as nanoseconds or as broad as hours.
Think of it as an embedded scheduler that lives and dies with the process itself — not a replacement for cron jobs, Kubernetes CronJobs or external orchestrators.
This makes it perfect for:
- Tasks that are contextual to the running service.
- Periodic updates, pings or syncs triggered from in-memory state.
- Lightweight, internal routines with no external dependency.
How to create a new Ticker
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for t := range ticker.C {
fmt.Println("Tick at:", t)
}
}
As you can see, the ticker is initialized using the time.NewTicker(d time.Duration)
constructor, returning a *Ticker
that sends a timestamp on its .C
channel after every interval of duration d
, regardless of the time unit used.
This means it can tick every few milliseconds, seconds or even minutes, depending on what your logic needs.
The ticker has an associated channel where, every interval of duration defined, a time.Time
object is notified.
By executing this file we get the following output:
Tick at: 2025-03-09 19:10:54
Tick at: 2025-03-09 19:10:56
Tick at: 2025-03-09 19:10:58
Tick at: 2025-03-09 19:11:00
Tick at: 2025-03-09 19:11:02
As you can see, since we have used a duration of 2 seconds when we created the ticker, we get a new event notified to the channel every two seconds. We can now use this event to do something every 2 seconds
in our code.
Running a Ticker with context cancellation
In real-world applications, especially when building long-running services or daemons, it’s important to have control over the lifecycle of your ticker. Instead of running it indefinitely, you can combine it with a context.Context
to gracefully stop the ticker when needed. For example, during a shutdown or when receiving a signal.
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // In a real app, cancel might be called from a signal handler
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
// Simulate cancellation after 7 seconds
go func() {
time.Sleep(7 * time.Second)
cancel()
}()
for {
select {
case <-ctx.Done():
fmt.Println("Context canceled, stopping ticker.")
return
case t := <-ticker.C:
fmt.Println("Tick at:", t)
}
}
}
In this example, we use a select
statement inside an infinite for
loop to listen to multiple channels at the same time. If a value is received from the ticker channel, we handle it as a regular tick. If the context is cancelled, the <-ctx.Done()
case is triggered, the loop exits and the function returns.
This approach ensures that the ticker stops automatically when the context it’s running in is cancelled. For example, when an HTTP request times out or a background job is stopped. It’s a clean way to manage the lifecycle of long-running processes.
If we execute this code, we now get the following output:
Tick at: 2025-03-09 19:17:32
Tick at: 2025-03-09 19:17:34
Tick at: 2025-03-09 19:17:36
Context canceled, stopping ticker.
As you can see, the context gets cancelled after 7 seconds, sending the event to the corresponding channel and therefore, exiting the infinite for loop.
See you next time!