All Benchmarks

Go Benchmark

Time Format vs AppendFormat

Formatting timestamps with a fresh string vs appending into a reused buffer.

timeformatappendformatallocationloggingrfc3339

Formatting timestamps is a hidden hot path in most services — every log line, metric, and API response carries one. time.Time.Format returns a freshly allocated string on every call, while AppendFormat writes into a caller-provided byte slice, allowing the buffer to be reused across calls. This benchmark formats the same time as RFC 3339 with both APIs. The formatting work is identical; the difference is one heap allocation per call — which is exactly why loggers like zap and zerolog are built on the Append* style APIs. The same pattern applies to strconv.AppendInt vs strconv.Itoa and friends.

linux/amd64AMD Ryzen 9 9950X3D 16-Core Processorbenchmarks/time-format-vs-appendformat
Compare atCPUs

1 CPU

Fastest

Time Append Format

16.78 ns/op

Slowest

Time Format

29.55 ns/op · 1.8x slower

32 CPUs

Fastest

Time Append Format

16.77 ns/op

Slowest

Time Format

29.53 ns/op · 1.8x slower

Performance Comparison (lower is better)
CPU:
#1

Time Append Format #

Fastest

t.AppendFormat(buf[:0], time.RFC3339) with a buffer reused across iterations. Identical formatting logic, but the output lands in memory you already own: zero allocations per call once the buffer exists. The natural fit for loggers, encoders, and any code that writes the bytes onward instead of storing a string.

CPU Scaling (lower is better)
func BenchmarkTimeAppendFormat_run(b *testing.B) {
	t := time.Date(2026, time.July, 16, 12, 30, 45, 0, time.UTC)
	buf := make([]byte, 0, 64)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		// Formats into the reused buffer — no allocation per call.
		buf = t.AppendFormat(buf[:0], time.RFC3339)
	}
	sinkInt = len(buf)
}
1 CPU
1.8xfaster(76%)thanTime Format
32 CPUs
1.8xfaster(76%)thanTime Format
#2

Time Format #

Slowest

t.Format(time.RFC3339) — the everyday API. Internally it calls AppendFormat into a temporary buffer and then converts the result to a string, which is where the per-call heap allocation comes from. Perfectly fine anywhere that isn't hot.

CPU Scaling (lower is better)
func BenchmarkTimeFormat_run(b *testing.B) {
	t := time.Date(2026, time.July, 16, 12, 30, 45, 0, time.UTC)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		// Allocates a new string on every call.
		sinkString = t.Format(time.RFC3339)
	}
}
1 CPU
1.8xslower(76%)thanTime Append Format
32 CPUs
1.8xslower(76%)thanTime Append Format

Contributors