beevik/ntp asks an NTP server what time it is, from Go
a simple ntp client package for go
At a glance
- What is it?
- The beevik/ntp package is a small SNTP client for Go built on RFC 5905, offering a one call time lookup, a full Response structure with offset, stratum and error bounds, and configurable timeouts, protocol versions and authentication. It is a client library with no daemon, no server and no configuration file, which is exactly what makes it easy to embed and easy to misuse.
- Who is it for?
- beevik/ntp fits a Go service that needs a defensible timestamp from a remote NTP server without pulling in a daemon or a heavyweight time library, and its Response structure and Validate method give you the fields you need to decide whether a reading is good enough.
- Can I use it commercially?
- Yes. BSD-2-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 29 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One function is the whole quick path
The stated purpose of the package is small and precise: an implementation of a Simple NTP client based on RFC 5905, letting a program connect to a remote NTP server and request information about the current time. If that is all you need, the entire API is one call:
time, err := ntp.Time("0.beevik-ntp.pool.ntp.org")That is the right shape for a service that stamps records with server time instead of trusting the local clock, or for a system that wants to detect drift on a machine it does not control. It is also the whole product. There is no daemon to install, no configuration file to write and no server mode, and the top level of the repository is a flat set of Go files: ntp.go, ntp4.go, ntp5.go, auth.go, conn.go, plus platform specific connection files for Darwin, Linux and Windows and a clock file split between Windows and everything else.
For a Go developer, importing a package and calling one function is a smaller commitment than running chrony or systemd-timesyncd on the target machine, and the trade is that you get a reading rather than a disciplined clock.
The Response struct is where the real information sits
The second entry point, Query, returns a Response structure and that structure is the reason to use this package rather than a raw UDP call. Alongside the offset it carries the diagnostic fields a time professional would want: ClockOffset, the estimated offset of the local clock relative to the server clock; Time, when the server transmitted its response by its own clock; RTT, the round-trip delay estimate; Precision of the server clock reading; and Stratum, the number of hops to the reference clock, where a stratum 1 server is directly attached to the reference clock.
Then it goes further. RootDelay is the server's aggregate round-trip delay to its stratum 1 source, RootDispersion is the estimated maximum measurement error relative to that reference clock, RootDistance estimates the root synchronization distance between client and stratum 1 server, and MinError gives a lower bound on the clock error between client and server. Leap carries the leap second indicator for the current month's last minute, Poll is the maximum polling interval between messages, and ReferenceID plus ReferenceTime identify the consulted reference clock and when it last updated its own clock setting.
The usage pattern is to add the offset to any later reading of the local clock:
response, err := ntp.Query("0.beevik-ntp.pool.ntp.org")
time := time.Now().Add(response.ClockOffset)QueryOptions carries every knob, including the ones you will forget
Behaviour beyond the defaults is set through QueryWithOptions and the QueryOptions struct. Timeout bounds how long the client waits for a response. Version selects protocol version 2, 3 or 4. TTL caps the number of IP hops before the packet is discarded. LocalAddress picks the local IP used for the query, which the documentation notes is useful when a host has several network interfaces.
Two of these options exist for testability, and that is the interesting design choice. GetSystemTime is a custom function used to override the platform's default system clock call, and Dialer is a custom network connection dialer that overrides the default UDP dialer. Both let you feed the client a controlled clock or a controlled transport, which is how you can test offset computation without a real server.
Auth carries the symmetric authentication key and algorithm, and the same information is used by the client to authenticate the server's response, not only the server to authenticate the query. Extensions can modify a query before it is transmitted and process responses after they arrive, which is the hook the NTS package builds on.
options := ntp.QueryOptions{ Timeout: 30*time.Second, TTL: 5 }
response, err := ntp.QueryWithOptions("0.beevik-ntp.pool.ntp.org", options)
time := time.Now().Add(response.ClockOffset)Validate and the kiss of death decide whether a reading counts
A returned offset is not the same as a usable offset, and the package draws that line explicitly. The Response structure's Validate function performs additional sanity checks to determine whether the response is suitable for time synchronization purposes:
err := response.Validate()
if err == nil {
// response data is suitable for synchronization purposes
}The other half of that is Stratum zero. A server that answers with stratum zero has responded with what the documentation calls the kiss of death, and the reason is carried in a separate four character KissCode field. KissCode also appears in the normal response structure and is documented as empty when nothing is wrong.
This matters in practice because a client that only reads ClockOffset will happily apply an offset from a server that has told it, in effect, to go away. A server under load, a server that has been rate limited, or a misconfigured server can all produce stratum zero, and the KissCode is where the reason is. Reading the offset without reading the stratum is the most common way to get a wrong answer from a client that otherwise works correctly.
The Log output exposes the four timestamps behind the offset
The response has a Log method that writes a full breakdown to a writer, which is the fastest way to understand what the offset was computed from. The documented output shows the client's and server's timestamps for each stage of the exchange, which is where the round trip estimate comes from:
Version: 4
ClockOffset: -374.811087ms
RTT: 67.989037ms
SystemTime: Sun Sep 20 2026 18:36:41.96194660 (PDT)
~TrueTime: Sun Sep 20 2026 18:36:41.58713551 (PDT)
ClientXmit: Mon Sep 21 2026 01:36:41.89013280 (UTC)
ServerRecv: Mon Sep 21 2026 01:36:41.54931623 (UTC)
ServerXmit: Mon Sep 21 2026 01:36:41.54937899 (UTC)
ClientRecv: Mon Sep 21 2026 01:36:41.95818460 (UTC)
Stratum: 3
Leap: No Warning
Flags: [Synchronized] (0x00000001)
Era: 0
Timescale: UTC
Poll: 1s
Precision: 29ns
RootDelay: 20.111084ms
RootDisp: 366.211µs
RootDist: 44.416271ms
Supported: [4]
MinError: 340.816569ms
RefTime: Mon Sep 21 2026 01:35:21.44779491 (UTC)
RefID: 158.51.99.19 (0x9e336313)
KissCode: <empty>The Log method is called on the error path in the documented example, which reads oddly at first and is in fact a sensible habit: when a query fails you want the partial response fields, and the sample code does exactly that by logging when err is not nil. Two things stand out in the sample output. The client's local clock and the true time differ by roughly 374 milliseconds, and the MinError field says the error could be at least 340 milliseconds, so the offset is close to the noise floor and a single reading says little. The Supported field lists the protocol versions the server accepts.
NTPv5 lives here but is marked experimental
The package supports the current draft of NTP version 5, and the documentation is unusually direct about the status of that support: the implementation is considered experimental, subject to change as the draft specification evolves, and for that reason the NTPv5 features are not recommended for use in production environments.
The repository layout backs that up. ntp5.go and ntp4.go sit alongside ntp.go, with ntp4_test.go and ntp5_test.go as separate test files, so protocol versions are separated by file rather than tangled together, and auth.go with auth_test.go handles the symmetric key support independently of the version specific code.
The Version option accepts 2, 3 or 4, and the documented logging example uses Version 4 with RequestSupportedVersions set to true, which suggests the client can ask the server which versions it understands. If you need NTPv5 features, this package is where they are implemented, but you would be building on a moving specification and you would be doing so against the package's own advice.
The pool is a shared resource and NTS is a separate package
Two practical notes in the documentation deserve more attention than they usually get.
The first is about the pool. The NTP pool is a shared resource provided by the NTP Pool Project and used by people and services all over the world, and the documentation asks that applications avoid querying the standard pool.ntp.org zone names. The suggested alternative is to request your own vendor zone or to join the pool. The reason is overload protection, and it means the example hostnames used throughout the README, such as 0.beevik-ntp.pool.ntp.org, are for demonstration rather than for production use. A service that hardcodes one of them inherits the cost of everyone else doing the same.
The second is Network Time Security, defined by RFC 8915 as an enhancement to NTP that adds authentication and message integrity. The package does not implement it, and points at the separate beevik/nts repository instead, which is implemented as an extension to this package. That split is sensible because NTS needs key exchange and a secure channel in addition to the packet work, and it means an application that needs authenticated time has two dependencies to reason about rather than one.
Where a client library stops
The boundary is scope, and it is worth stating plainly. This package answers a question. It does not set a clock, it does not discipline a frequency, and it does not slew a local clock toward the reference over time. A reading taken once tells you the offset at that moment along with an error bound, and the sample output shows how wide that bound can be relative to the offset itself.
Three concrete constraints follow. First, the pool guidance means the example hostnames are not production defaults, so you need a vendor zone or a pool membership, and that is a decision someone on your side has to make. Second, if your requirement includes authenticated time, you need the nts package as well and the Extensions option to carry it, which is a two package dependency. Third, the NTPv5 path is documented as not for production, so protocol version choice is really a choice between 2, 3 and 4 for anything you ship.
One more practical point sits in the build metadata. The module requires Go 1.26.0, and its dependencies are golang.org/x/net and golang.org/x/sys with stretchr/testify for tests. That is a recent toolchain floor, so a service on an older Go release cannot consume it without upgrading. The release history is thin but real: v1.6.0 on 2026-09-24, v1.5.0 on 2025-10-03 and v1.4.3 on 2024-05-30, and the last push was on 2026-09-24. The project is licensed BSD-2-Clause.
Editorial conclusion
beevik/ntp fits a Go service that needs a defensible timestamp from a remote NTP server without pulling in a daemon or a heavyweight time library, and its Response structure and Validate method give you the fields you need to decide whether a reading is good enough. It is the wrong tool if you need to set the system clock, hold a long running time discipline loop, or require authenticated NTP, because NTS lives in the separate beevik/nts package and the NTPv5 support in this one is documented as experimental and unsuitable for production. Verify three things before you depend on it: that the server you query is a pool vendor zone rather than the shared pool.ntp.org names, that you call response.Validate() rather than trusting the offset blindly, and that the version you pin matches the Go toolchain your build uses, since the module now declares go 1.26.0 and the last release was v1.6.0 on 2026-09-24.
Frequently asked questions
How do I get the current time from an NTP server in Go with beevik/ntp?
Call ntp.Time with a server hostname, which returns the time and an error. The package documents 0.beevik-ntp.pool.ntp.org as the example host, but it also asks applications to request their own vendor zone or join the pool rather than query the standard pool.ntp.org names.
What is the difference between ntp.Time and ntp.Query?
Time returns only the current time according to the remote server. Query returns a Response structure that also carries ClockOffset, RTT, Stratum, RootDelay, RootDispersion, MinError, KissCode and the other synchronization fields.
How do I set a timeout and protocol version in beevik/ntp?
Pass a QueryOptions value to QueryWithOptions. The Timeout field bounds how long the client waits, Version selects protocol version 2, 3 or 4, and TTL caps the number of IP hops before the packet is discarded.
What does a stratum zero response mean?
The documentation calls it the kiss of death: the server has responded with stratum zero and you should examine the KissCode, a four character string describing the reason. It is a signal to reject that reading rather than apply its offset.
Does beevik/ntp support NTS authentication?
Not in this package. Network Time Security is defined by RFC 8915 and is implemented in the separate beevik/nts package, which is built as an extension to this one. This package does expose an Auth option for the symmetric key and algorithm used to authenticate the query and the server's response.
Is the NTPv5 support in beevik/ntp ready for production?
No. The documentation states that the NTPv5 implementation is considered experimental, is subject to change as the draft specification evolves, and is not recommended for use in production environments.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/beevik-ntp)Was this useful?