Skip to main content
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.
// 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
}

Performance Considerations

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

  1. Always stop tickers - Use defer ticker.Stop()
  2. Avoid time.Tick() - Use time.NewTicker() instead
  3. Don’t block ticker processing - Process ticks quickly or in goroutines
  4. Use select for shutdown - Combine ticker with quit channel
  5. Consider interval vs work duration - Ensure interval > work time
  6. Clean up on exit - Stop tickers in defer or shutdown handlers

Ticker vs Timer vs Sleep

FeatureTickerTimerSleep
FiresRepeatedlyOnceOnce
Can stopYesYesNo
Use casePeriodic tasksDelayed eventSimple delay
Resource cleanupMust stopShould stopN/A

Build docs developers (and LLMs) love