NTP works. Why does your prototype still show the wrong time?

Your connected sensor is sending readings, the clock says it has synchronized, and yet every event appears eight hours late. Before replacing the clock hardware, check what happens between receiving network time and putting it on the screen. A device can obtain the right time and then apply the same time-zone adjustment twice.

For a desk clock or a sensor logger going to its first users, choose one rule: keep the device's working clock in UTC, the common time reference, and convert it only when showing local time. That makes a reading from one location easier to compare with a reading from another. Label the display and exported timestamps so the next person knows which convention you used.

We ran an Ethernet and NTP experiment on our bench with MicroPython and a WIZnet W5500 interface, and one of our recordings caught the displayed hours disagreeing. This note separates what was recorded from the correction we propose. It is an early prototype exercise, not a measurement of clock accuracy.

First, establish what actually reached the network

The W5500 sat on SPI0: GP18 for the clock, GP19 for MOSI, GP16 for MISO, GP17 for chip select and GP20 for reset, with a 2 MHz SPI clock. These assignments describe our setup; they are not a pin map for every WIZnet board.

The program tries DHCP and falls back to a static address. It waits for the link, resolves a server name, then asks an NTP server for time. Those are separate steps. A name-resolution result does not show that the server answered an NTP request, and a successful ping from the PC (four replies, no loss, in our check) does not show that the board has done either. The PC check is useful when investigating a network problem, but the board's own synchronization log is the evidence that matters next.

Give the clock one job and the display another

NTP supplies a shared time reference. A local display then applies the policy appropriate to its user. A long-running MicroPython community discussion, opened in October 2022, starts with exactly this confusion: network synchronization worked, but the developer still needed local time. A February 2025 reply points out that some time zones need a fractional-hour offset. This is a longstanding implementation problem.

In our first version of the program, synchronization was followed by an eight-hour conversion before the result was written back to the RTC. The main loop then added another eight hours before printing. The clock and the display were both doing the same conversion. Removing that duplication is more useful than asking the server for another time value.

The corrected flow is simple: attempt synchronization; on success, leave the RTC in UTC and return to the application; add the local offset only when displaying. On failure, count the attempt, try the next server after a short wait, and after a fixed number of attempts report that synchronization failed.

Keep the correction small enough to check

The current ntptime implementation in micropython-lib writes UTC to the RTC. With that implementation, and after the network is up, this fragment shows the separation. We have not run it on our hardware yet; confirm the module supplied with your firmware before adopting it.

import ntptime
import time

# Network setup is required before this fragment.
ntptime.timeout = 5  # seconds in the implementation linked above
offset_seconds = 8 * 3600  # fixed UTC+08:00 display in this example

try:
    ntptime.settime()  # keep the RTC in UTC
except OSError:
    print("Network time unavailable; do not mark the clock synchronized")
else:
    local = time.gmtime(time.time() + offset_seconds)
    print("%04d-%02d-%02d %02d:%02d:%02d UTC+08:00" % local[:6])

Do not write local back into the RTC. The offset is a display choice, not a time-zone implementation: it has no daylight-saving rules. For a prototype used in one fixed-offset location, that may be enough. For users who travel or change clocks seasonally, define where the device gets its time-zone setting and how it is updated.

Another easy mistake is mixing time representations. time.localtime returns year, month, day, hour, minute, second, weekday and day of year. An RTC tuple puts the weekday earlier: year, month, day, weekday, hours, minutes, seconds, subseconds. Follow the API you are calling rather than passing one tuple unchanged into another. The MicroPython time documentation describes the first; the RTC documentation describes the second.

Check timeout units too. Our first program passed a value named timeout_ms directly into ntptime.timeout. In the current implementation, that value reaches a socket timeout expressed in seconds, so 5000 would mean 5,000 seconds, not five. The socket documentation confirms the unit. Changing a variable's name does not change its unit.

Watch the hours, not just the success message

We have two records from separate runs. A still from March 2025 shows a PC clock around 22:05 and printed RTC values around the same minute. A screen recording from September 2025 shows a web clock at about 15:48 while the repeating RTC log reads 23:48. Keeping both matters: the success message alone would have hidden the disagreement.

The eight-hour difference is consistent with an extra offset, and the written program contains a duplicate conversion. But the still, the recording and the written code do not belong to one identical software revision. They justify investigating that error; they do not prove a particular edit has fixed the recorded device.

Before handing the prototype to a user, try:

Confirm that the printed time zone, the exported timestamp and the screen all follow the chosen rule. A synchronization failure should stay visible instead of turning an unverified startup clock into apparently valid data. We list these as the next checks to run, not as completed tests.

For a sensor logger, the next useful result is a sample record that another person can interpret without asking which clock it uses. For a desk clock, it is a screen that shows the expected local date and time after startup and clearly handles a failed update.