Tickers are for executing code repeatedly at regular intervals. While timers fire once, tickers fire periodically until stopped.
Basic Ticker Usage
package main
import (
"fmt"
"time"
)
func main() {
// Create a ticker that ticks every 500ms
ticker := time.NewTicker(500 * time.Millisecond)
done := make(chan bool)
go func() {
for {
select {
case <-done:
return
case t := <-ticker.C:
fmt.Println("Tick at", t)
}
}
}()
// Let it tick for 1600ms
time.Sleep(1600 * time.Millisecond)
ticker.Stop()
done <- true
fmt.Println("Ticker stopped")
}
Output:
Tick at 2026-03-03 10:00:00.5 +0000 UTC
Tick at 2026-03-03 10:00:01.0 +0000 UTC
Tick at 2026-03-03 10:00:01.5 +0000 UTC
Ticker stopped
The ticker’s channel C receives the current time value on each tick.
Stopping a Ticker
ticker := time.NewTicker(time.Second)
defer ticker.Stop() // Important: always stop tickers
for i := 0; i < 5; i++ {
<-ticker.C
fmt.Println("Tick", i+1)
}
Always call Stop() on tickers when done. Tickers consume resources and the garbage collector cannot reclaim them until stopped.
Ticker Patterns
1. Periodic Task
func periodicCleanup(interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
cleanupOldFiles()
compactDatabase()
fmt.Println("Cleanup complete")
}
}
2. Heartbeat
func heartbeat(interval time.Duration) <-chan struct{} {
heartbeat := make(chan struct{})
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
select {
case heartbeat <- struct{}{}:
default:
// No receiver ready
}
}
}()
return heartbeat
}
3. Status Reporter
func statusReporter(interval time.Duration, quit <-chan struct{}) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
reportStatus()
case <-quit:
fmt.Println("Reporter shutting down")
return
}
}
}
4. Rate Limiter
func rateLimiter(rate time.Duration, requests <-chan Request) {
ticker := time.NewTicker(rate)
defer ticker.Stop()
for req := range requests {
<-ticker.C // Wait for next tick
processRequest(req)
}
}
Ticker vs time.Tick()
time.Tick() - Convenience Function
// Simple, but cannot be stopped
for t := range time.Tick(1 * time.Second) {
fmt.Println("Tick:", t)
}
time.Tick() creates a ticker that can never be stopped, leading to resource leaks. Only use it in infinite loops where the program lifetime matches the ticker lifetime.
time.NewTicker() - Recommended
// Recommended: can be stopped
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for t := range ticker.C {
fmt.Println("Tick:", t)
}
Practical Examples
Metrics Collection
type MetricsCollector struct {
ticker *time.Ticker
quit chan struct{}
requests atomic.Int64
}
func NewMetricsCollector(interval time.Duration) *MetricsCollector {
mc := &MetricsCollector{
ticker: time.NewTicker(interval),
quit: make(chan struct{}),
}
go mc.run()
return mc
}
func (mc *MetricsCollector) run() {
for {
select {
case <-mc.ticker.C:
count := mc.requests.Swap(0)
fmt.Printf("Requests in last interval: %d\n", count)
case <-mc.quit:
mc.ticker.Stop()
return
}
}
}
func (mc *MetricsCollector) RecordRequest() {
mc.requests.Add(1)
}
func (mc *MetricsCollector) Stop() {
close(mc.quit)
}
Connection Pool Cleanup
type ConnectionPool struct {
connections []*Connection
mu sync.Mutex
cleanupTicker *time.Ticker
}
func (cp *ConnectionPool) StartCleanup(interval time.Duration) {
cp.cleanupTicker = time.NewTicker(interval)
go func() {
for range cp.cleanupTicker.C {
cp.mu.Lock()
var active []*Connection
for _, conn := range cp.connections {
if conn.IsAlive() {
active = append(active, conn)
} else {
conn.Close()
}
}
cp.connections = active
cp.mu.Unlock()
}
}()
}
func (cp *ConnectionPool) StopCleanup() {
if cp.cleanupTicker != nil {
cp.cleanupTicker.Stop()
}
}
Progress Indicator
func longRunningTask() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
done := make(chan bool)
go func() {
// Simulate long task
time.Sleep(10 * time.Second)
done <- true
}()
elapsed := 0
for {
select {
case <-ticker.C:
elapsed++
fmt.Printf("Working... %d seconds elapsed\n", elapsed)
case <-done:
fmt.Println("Task complete!")
return
}
}
}
Cache Expiration
type Cache struct {
items map[string]*CacheItem
mu sync.RWMutex
ticker *time.Ticker
}
type CacheItem struct {
Value interface{}
Expires time.Time
}
func NewCache() *Cache {
c := &Cache{
items: make(map[string]*CacheItem),
ticker: time.NewTicker(1 * time.Minute),
}
go c.cleanup()
return c
}
func (c *Cache) cleanup() {
for range c.ticker.C {
now := time.Now()
c.mu.Lock()
for key, item := range c.items {
if now.After(item.Expires) {
delete(c.items, key)
}
}
c.mu.Unlock()
}
}
func (c *Cache) Stop() {
c.ticker.Stop()
}
Resetting Ticker Interval
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
// Change interval to 2 seconds
ticker.Reset(2 * time.Second)
for t := range ticker.C {
fmt.Println("Tick:", t)
}
Reset() changes the ticker’s interval. The next tick will occur after the new duration.
Multiple Tickers with Select
func multipleTickers() {
fast := time.NewTicker(100 * time.Millisecond)
slow := time.NewTicker(1 * time.Second)
defer fast.Stop()
defer slow.Stop()
for {
select {
case <-fast.C:
fmt.Println("Fast tick")
case <-slow.C:
fmt.Println("Slow tick")
return
}
}
}
Graceful Shutdown Pattern
type Worker struct {
ticker *time.Ticker
quit chan struct{}
done chan struct{}
}
func NewWorker(interval time.Duration) *Worker {
w := &Worker{
ticker: time.NewTicker(interval),
quit: make(chan struct{}),
done: make(chan struct{}),
}
go w.run()
return w
}
func (w *Worker) run() {
defer close(w.done)
defer w.ticker.Stop()
for {
select {
case <-w.ticker.C:
w.doWork()
case <-w.quit:
fmt.Println("Worker shutting down gracefully")
return
}
}
}
func (w *Worker) doWork() {
fmt.Println("Working...")
}
func (w *Worker) Shutdown() {
close(w.quit)
<-w.done // Wait for worker to finish
}
Ticker Accuracy
Tickers aim for regular intervals but are not guaranteed to be perfectly precise. System load and scheduler decisions can introduce slight variations.
// Measure ticker accuracy
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
last := time.Now()
for i := 0; i < 10; i++ {
t := <-ticker.C
diff := t.Sub(last)
fmt.Printf("Tick %d: %v (expected 100ms)\n", i, diff)
last = t
}
Common Pitfalls
Forgetting to Stop
// BAD: Ticker leaks resources
func bad() {
ticker := time.NewTicker(1 * time.Second)
for i := 0; i < 5; i++ {
<-ticker.C
}
// Forgot ticker.Stop()!
}
// GOOD: Always stop
func good() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for i := 0; i < 5; i++ {
<-ticker.C
}
}
Using time.Tick() Inappropriately
// BAD: Cannot be stopped, leaks if loop exits
for t := range time.Tick(1 * time.Second) {
if shouldStop() {
break // Ticker still running!
}
process(t)
}
// GOOD: Use NewTicker
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for t := range ticker.C {
if shouldStop() {
break // Properly stopped
}
process(t)
}
Blocking Ticker
// BAD: Slow processing blocks ticks
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for range ticker.C {
slowOperation() // Takes 500ms - misses ticks!
}
// GOOD: Process in goroutine
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for range ticker.C {
go slowOperation() // Doesn't block next tick
}
Tickers are lightweight (~150 bytes). You can have thousands of tickers in a single program without significant overhead.
Benchmarks (approximate)
- Creating ticker: ~100-200 ns
- Stopping ticker: ~20-30 ns
- Receiving tick: ~50-100 ns
- Memory per ticker: ~150 bytes
Best Practices
- Always stop tickers - Use
defer ticker.Stop()
- Avoid time.Tick() - Use
time.NewTicker() instead
- Don’t block ticker processing - Process ticks quickly or in goroutines
- Use select for shutdown - Combine ticker with quit channel
- Consider interval vs work duration - Ensure interval > work time
- Clean up on exit - Stop tickers in defer or shutdown handlers
Ticker vs Timer vs Sleep
| Feature | Ticker | Timer | Sleep |
|---|
| Fires | Repeatedly | Once | Once |
| Can stop | Yes | Yes | No |
| Use case | Periodic tasks | Delayed event | Simple delay |
| Resource cleanup | Must stop | Should stop | N/A |