Protobuf’s Timestamp type is a surprisingly opinionated way to represent time.

Here’s a person.proto file demonstrating its use:

syntax = "proto3";

import "google/protobuf/timestamp.proto";

message Person {
  string name = 1;
  google.protobuf.Timestamp birth_date = 2;
  google.protobuf.Timestamp registration_time = 3;
}

When you compile this with protoc, you’ll get generated code that includes methods for working with Timestamp objects. For instance, in Go, you might create a Person like this:

package main

import (
	"fmt"
	"time"

	"google.golang.org/protobuf/types/known/timestamppb"

	pb "your_module/person" // Assuming your generated code is here
)

func main() {
	now := time.Now()
	birth := time.Date(1990, 5, 15, 10, 30, 0, 0, time.UTC)

	person := &pb.Person{
		Name: "Alice",
		BirthDate: &timestamppb.Timestamp{
			Seconds: birth.Unix(),
			Nanos:   int32(birth.Nanosecond()),
		},
		RegistrationTime: timestamppb.New(now),
	}

	fmt.Printf("Person: %+v\n", person)
	fmt.Printf("Birth Date (Go time): %v\n", person.BirthDate.AsTime())
}

Running this would output something like:

Person: &{Name:Alice BirthDate:{Seconds:642871800 Nanos:0} RegistrationTime:{Seconds:1678886400 Nanos:123456789}}
Birth Date (Go time): 1990-05-15 10:30:00 +0000 UTC

The Timestamp well-known type, defined in google/protobuf/timestamp.proto, is a standardized way to represent a point in time. It’s not just a simple integer or string; it’s a structured message containing seconds and nanoseconds since the Unix epoch (January 1, 1970, 00:00:00 UTC). This format ensures consistency across different languages and systems, avoiding the common pitfalls of timezone ambiguity, leap seconds, and varying date/time representations. It’s fundamentally an RFC 3339 compliant timestamp, serialized as a Protobuf message.

The power of this type comes from its explicit definition. Instead of relying on a language’s built-in time.Time or a string format like ISO 8601, you’re using a type that’s understood by the Protobuf ecosystem. This means libraries can provide optimized serialization and deserialization, and you get guaranteed interoperability. When you see google.protobuf.Timestamp in a .proto file, you know exactly what you’re dealing with: a precise moment in time, independent of any local clock or timezone settings.

When you use timestamppb.New(time.Now()) in Go, for example, the library handles the conversion from the Go time.Time struct to the Seconds and Nanos fields of the Protobuf Timestamp message. The Seconds field stores the number of whole seconds elapsed since the Unix epoch, and the Nanos field stores the fractional part of the second, from 0 to 999,999,999. This granular representation allows for microsecond precision.

The AsTime() method (or equivalent in other languages) is crucial for converting the Protobuf Timestamp back into a native language time object. This makes it easy to perform date and time arithmetic, formatting, or comparisons using familiar language constructs.

A common mistake is to represent timestamps as simple int64 or string fields in your Protobuf messages. This leads to a fragmentation of how time is handled: one service might store seconds since epoch as an int64, another might store milliseconds since epoch, and a third might use an ISO 8601 string with or without timezone information. This makes inter-service communication fragile and debugging a nightmare. Using google.protobuf.Timestamp centralizes this logic and provides a single source of truth for time representation.

The Timestamp well-known type doesn’t inherently store timezone information. It represents a specific point in time in UTC. If you need to associate a timezone with a timestamp (e.g., for display purposes), that information must be stored separately, perhaps in another string field. The AsTime() method in most generated code will return a time.Time object that is already in UTC, reflecting the Protobuf Timestamp’s definition.

The most surprising thing about google.protobuf.Timestamp is how it forces you to confront the absolute nature of time versus its human-centric, localized representation. The Seconds and Nanos fields are inherently UTC. When you convert a time.Time object that has timezone information into a Timestamp, the library implicitly converts it to UTC. This means that a time.Time object representing 2023-03-15 10:00:00 PST will be serialized into the Timestamp as the equivalent UTC time, which would be 2023-03-15 18:00:00 UTC (assuming PST is UTC-8). This conversion is critical for ensuring that the timestamp represents a consistent, unambiguous point in history, regardless of where or when the data was created.

After you’ve standardized on google.protobuf.Timestamp, the next challenge is how to efficiently query and index these timestamps in your databases.

Want structured learning?

Take the full Protobuf course →