GPS Raspberry Pi NTP Server

GPS Raspberry Pi NTP Server

This post details how to create a stratum-1 NTP Server using a Raspberry Pi utilizing GPS and PPS, and get time within 100 nanoseconds of real time, directly from the atomic clocks located in the GPS satellites above your head. The best part about this guide is that this will work with no internet. After initial setup, you could disconnect it from the internet and they would work. All of the other guides out there that I have found do not include the configuration necessary for this.

The goal for this project for me was to limit my reliance on the internet for services. If the internet were to go out for a few weeks during a natural disaster, I don't want to have to worry about NTP not working. I also think its silly to get the time from a server across the internet, which eventually gets it from an atomic clock, when we can get it DIRECTLY from those clocks ourselves. Its also very cool to see an $11 piece of hardware get data from 12 GPS satellites that are over 12,000 miles above the earth. What a time to be alive!

In this post I will also detail how to do this same thing with USB, instead of serial. The USB config might be something to play with if you don't have a Raspberry Pi or something with GPIO pins or you don't want to solder anything. The serial config on this guide will be specific to a Raspberry Pi, however the USB config applies to pretty much any linux computer. You will be less accurate with USB though, not that it matters much for a home network. Its much more fun doing the serial config though. I actually did the USB config first, just to see how things would work out, and then I moved to the Serial config.

Inspiration, references, etc:

I got this entire idea from these 2 blog posts. However I think they are missing some crucial information (For my use case, at least), and also rely on you watching the YouTube videos, which means there is more chance of the information going missing. Thanks Austin for putting all this great information together, there is no way I would have been able to build this without the blog posts and the videos. I highly suggest you also read these, as he does go into a lot of detail.

Microsecond accurate NTP with a Raspberry Pi and PPS GPS - Austin’s Nerdy Things
For around $100, you can have a Stratum 1 NTP PPS GPS system providing microsecond time to all of your computers & equipment.
Millisecond accurate Chrony NTP with a USB GPS for $12 USD - Austin’s Nerdy Things
For around $100, you can have a Stratum 1 NTP PPS GPS system providing microsecond time to all of your computers & equipment.

I got a lot of information about auto starting gpsd on boot from here

gpsd failing on boot, but starts manually - Raspberry Pi Forums

And I spent way too many hours looking through the Chrony config pages, which now don't seem to go anywhere...

I also got some great tips and tricks on how to review the data from gpsmon from @t2mf on the Discord

My interest in NTP and time started when I watched these Jeff Geerling videos

Notes before we start:

I am not a Raspberry Pi expert, an NTP expert, a Chrony expert, a GPS expert or even a Linux expert. There is likely some stuff in here that could be improved, and there may even be some things I'm not doing correctly. If you spot something, please get in contact with me! And if you follow this guide, do check back every now and then, as I will update the guide (I will note what I update)


  • You will need a computer. I made 2 servers, one with a Raspberry Pi 3 B+ and one with a Raspberry Pi 1B+. Even the Raspberry Pi 1B+ does fine, so if you have an old one sitting around, its PERFECT for this. If you plan on doing the USB config, then pretty much anything will work. Just make sure you have a solid network connection to your local network. There is no point setting all this up and then connecting over a slow wireless connection. Ideally your computer should have Ethernet. You will of course need everything to make your computer or Pi work, such as an SD card, power supply, case etc.
  • a GT-U7 GPS module which costs between $10 and $13 USD (This guide is specific to the GT-U7) GPS Module Receiver,Navigation Satellite Positioning NEO-6M (Arduino GPS, Drone Microcontroller, GPS Receiver) Compatible with 51 Microcontroller STM32 Arduino UNO R3 with Antenna High Sensitivity : Electronics GPS Module Receiver,Navigation Satellite Positioning NEO-6M (Arduino GPS, Drone Microcontroller, GPS Receiver) Compatible with 51 Microcontroller STM32 Arduino UNO R3 with Antenna High Sensitivity : Electronics
  • If you are doing USB connection, a GOOD Micro USB cable. I went through about 5 to get one that actually functioned well for data, as it seems most are designed for charging. If you plan on doing the Serial/PPS config (The best one) then you don't need a USB cable.
  • If you are doing the serial config, some GPIO cables like this, you need 5 cables, so this will give you a ton spare after. I did find these were a little bit long, so you could go a bit shorter. All you need is the female-female ELEGOO 120pcs Multicolored Dupont Wire 40pin Male to Female, 40pin Male to Male, 40pin Female to Female Breadboard Jumper Ribbon Cables Kit Compatible with Arduino Projects : Electronics ELEGOO 120pcs Multicolored Dupont Wire 40pin Male to Female, 40pin Male to Male, 40pin Female to Female Breadboard Jumper Ribbon Cables Kit Compatible with Arduino Projects : Electronics
  • Optionally, a better GPS Antenna. I got okay results with the stock one for initial setup, but once I went to place it somewhere my signal was poor. I got this, which works great and has a magnetic base, so you can just stick it wherever you need. Its an SMA connection for the antenna, but it includes the IPEX to SMA adapter. Bingfu Waterproof Active GPS Navigation Antenna Adhesive Mount SMA Male GPS Antenna with 15cm 6 inch U.FL IPX IPEX to SMA Female RG178 Coaxial Pigtail Cable for GPS Module Receiver Tracking : Electronics Bingfu Waterproof Active GPS Navigation Antenna Adhesive Mount SMA Male GPS Antenna with 15cm 6 inch U.FL IPX IPEX to SMA Female RG178 Coaxial Pigtail Cable for GPS Module Receiver Tracking : Electronics

Now you know what we are doing and what you need, lets get going. This setup assumes you are using a Raspberry Pi. If you are using something else, you may need to adjust the configuration.

Step 0: Initial Config

I'm calling this step zero, because its really up to you. Get your Pi in order with Raspbian or whatever OS you are using, connect it to the network, call it what you want, etc and then follow along. We will be configuring this completely via terminal.

Step 1: Backups

The very first thing that I am going to do is configure my Raspberry to have weekly backups. These 2 servers will critical to my network, so I suggest you do the same. It also means you can easily go back a step if you mess something up (Perhaps during initial config do hourly backups?)

I made a whole post on that, so I'll leave it here

Backup a running Raspberry Pi over the network
For the longest time I’ve been looking for a way to backup my Raspberry Pi’s over to the network, to an easy to restore format. But every solution I’ve looked at has come up short. This post details a solution that works very well. Best of all, it makes incremental

Step 2: Software

You'll need to make sure your Pi is up to date and has the required packages installed

sudo apt update
sudo apt upgrade -y
sudo apt install gpsd gpsd-clients pps-tools chrony jq tcpdump -y

I also found there was some software I didn't need that was using up resources, so I removed them, This step is completely optional

sudo apt purge --remove lxde*; sudo apt autoremove -y
sudo apt-get remove --auto-remove lxpanel
sudo apt-get purge avahi-daemon

Step 3: Initial preparation

The first thing we want to do is enable the Serial Port on your Raspberry Pi. Enter the raspi-config by doing

sudo raspi-config

and you should see this screen. Go down to number 3, interface options

From there, Serial Port

when it asks if you want a shell login over serial, select No

And then select YES to enable the serial port

Now use Tab to get to Finish, and reboot if it prompts you to.

Next we can configure the GPIO pin for PPS, and add the PPS module. If you are using USB you can skip this, but it won't hurt to do it anyway if you plan on perhaps upgrading to that setup down the line.

sudo bash -c "echo '# the next 3 lines are for GPS PPS signals' >> /boot/config.txt"
sudo bash -c "echo 'dtoverlay=pps-gpio,gpiopin=18' >> /boot/config.txt"
sudo bash -c "echo 'enable_uart=1' >> /boot/config.txt"
sudo bash -c "echo 'init_uart_baud=57600' >> /boot/config.txt"

Note that the baud rate here is quite high, as this GPS module supports it. If you are following this guide with a different GPS module, try 9600

Now we can add the PPS module (Also skip if using USB)

sudo bash -c "echo 'pps-gpio' >> /etc/modules"

Now your Pi is ready to physically connect the GPS module, which we will cover in the next step.

Step 4: Hardware Configuration

Now you can connect your GPS module to the Pi. If you are using USB, just plug it in. If you are using serial, you will have to solder the header to the GPS module, and then plug it into the Pi GPIO pins. It doesn't matter if you solder the connector on with the tall pins at the top of the module or the bottom, just do what works for you. I did it so the tall pins were on the side with the LED, so I could lay it flat on a table while testing. Make sure to turn the Pi off while plugging into the GPIO. You can shut down your pi with

sudo shutdown -h now

I will use this as a reference for the pinout to connect the wires

V-IN GPS —> RPi Pin 4


RX GPS —>RPi Pin 8

TX GPS —>RPi Pin 10

PPS GPS —>RPi Pin 12

As you can tell, they are all in a line which makes it easy

Also make sure to connect the antenna

USB is of course very easy to connect

Go ahead and turn your Pi on. Note that I tried to tape the antenna directly on top of the PCB to make all in one type package, and it did not work. There is some kind of interference that completely stops the GPS lock.

Step 5: Initial Config and Tuning

Now we have all the pre-requisites setup and the hardware connected, we can start testing and configuring things.

If you have connected via serial, you can check for PPS pulses to see if the PPS config is correct by doing

sudo ppstest /dev/pps0

You should see an output like this. Do Control + C to exit out

trying PPS source "/dev/pps0"
found PPS source "/dev/pps0"
ok, found 1 source(s), now start fetching data...
source 0 - assert 1689037624.000000607, sequence: 19592 - clear  0.000000000, sequence: 0
source 0 - assert 1689037625.000001413, sequence: 19593 - clear  0.000000000, sequence: 0
source 0 - assert 1689037625.999999218, sequence: 19594 - clear  0.000000000, sequence: 0
source 0 - assert 1689037627.000001023, sequence: 19595 - clear  0.000000000, sequence: 0
source 0 - assert 1689037627.999999829, sequence: 19596 - clear  0.000000000, sequence: 0
source 0 - assert 1689037629.000000634, sequence: 19597 - clear  0.000000000, sequence: 0
source 0 - assert 1689037630.000000440, sequence: 19598 - clear  0.000000000, sequence: 0

Now we need to edit the gpsd configuration file, this applies to USB and Serial configurations.

sudo nano /etc/default/gpsd

If you are using USB, this is all you need in the config file

# this could also be /dev/ttyUSB0, it is ACM0 on raspberry pi

For Serial, on my Raspberry Pi 3 this is the config

DEVICES="/dev/ttyS0 /dev/pps0"

But, I did notice that on my Pi 1, the Serial Port name was different, it was ttyAMA0. So my config looked like this

DEVICES="/dev/ttyAMA0 /dev/pps0"

If you are usure, you can go to /dev/ and list the output, and get a clue. Trial and error is your friend here, you won't break anything. You can see below here that serial0 is mapped to ttyAMA0, which is where I got that device name from.

cd /dev/
ls -al


brw-rw----  1 root disk      1,   8 Jul 10 14:35 ram8
brw-rw----  1 root disk      1,   9 Jul 10 14:35 ram9
crw-rw-rw-  1 root root      1,   8 Jul 10 14:35 random
crw-rw-r--  1 root netdev   10, 242 Jul 10 14:36 rfkill
lrwxrwxrwx  1 root root           7 Jul 10 14:35 serial0 -> ttyAMA0
drwxrwxrwt  2 root root          40 May 13 05:36 shm
drwxr-xr-x  3 root root         180 Jul 10 14:36 snd
lrwxrwxrwx  1 root root          15 May 13 05:36 stderr -> /proc/self/fd/2
lrwxrwxrwx  1 root root          15 May 13 05:36 stdin -> /proc/self/fd/0
lrwxrwxrwx  1 root root          15 May 13 05:36 stdout -> /proc/self/fd/1
crw-rw-rw-  1 root tty       5,   0 Jul 10 14:36 tty
crw--w----  1 root tty       4,   0 Jul 10 14:36 tty0

Once you have your config in place, execute the below command to make gpsd start on boot

sudo ln -s /lib/systemd/system/gpsd.service /etc/systemd/system/

Now go ahead and reboot. When it comes back, launch gpsmon and you should see the output


If you don't see the above, you may need to adjust your serial name, or the connection is wrong. If you are connected via USB, you won't see PPS.

Your PPS offset will likely be MUCH higher than mine, as you've not yet tuned the system time which we will do later.

Note that it requires 4 satellites to get accurate time (Source below)

The Global Positioning System: Global Positioning Tutorial
Using the Global Positioning System (GPS), every point on Earth can be given its own unique address -- its latitude, longitude, and height. The U.S. Department of Defense developed GPS satellites as a strategic system in 1978. But now, anyone can gather data from them. For instance, many new cars ha…

If you have got to this point, you now have your GPS module connected to the Pi, and should notice the LED light blinking every second. If you ever notice it go off, or not blink every second, its because you are losing the PPS signal and don't have 4 satellites or have completely lost the GPS lock. You may need to reposition your GPS module, or get a better antenna, or make sure your antenna connector is fully clipped in.

If you want to monitor the number of satellites and move around your antenna, you can use this handy command

gpspipe -w | jq ".uSat| select( . != null )"

It will just keep scrolling down the screen how many you have, which can be useful. Just do Control + C to exit

Now we can configure the NTP server Chrony to use the GPS module. We will wait to configure additional options in the next step, right now we just want Chrony to see and use the GPS time. Start by opening the config file

sudo nano /etc/chrony/chrony.conf 

If you are using USB, add the below line below the entry

refclock SHM 0 refid NMEA offset 0.000 precision 1e-3 poll 3 noselect

This tells Chrony to add the NMEA (GPS) source, but don't use it for time (The noselect) We can dig into what the rest means later

If using Serial, add the below lines. There is an extra line to get the PPS data. Since PPS only tells you seconds, we lock it to the NMEA source to get the rest of the information.

refclock SHM 0 refid NMEA offset 0.000 precision 1e-3 poll 3 noselect
refclock PPS /dev/pps0 refid PPS lock NMEA poll 3

Scroll down (With pagedown) and find this line

Do what it says and uncomment it, which will turn on logging. With that changed and the new lines for the GPS, save and exit nano.

Restart Chrony

sudo systemctl restart chrony

Now, do the following

sudo cat /var/log/chrony/statistics.log | sudo head -2; sudo cat /var/log/chrony/statistics.log | sudo grep NMEA

It will spit out some information like this. And each time you run it, you will have more data

The reason we are collecting this data is to get the "Est offset" number, so we can tune that in the chrony config, to get the time spot on. You will want to let this run for at least 10 mins, but the longer the better. Personally I got it working, and then came back at a later date and re-did the logging with 4-5 hours of data (As it turns out, it was the same...)

Once you have waited as long as you wanted to, go ahead and paste that whole table into a txt file. I did this on my Windows computer. We are going to now use Excel to get the average of the Est Offset, which will punch into chrony.

Open Excel and in the Data Tab, click From Text and select your txt file.

Now set the Delimiter to Space (I'm sure there is a better way to do this, but it works...)

Click Load. From here we can just delete A through Q to get to the data we want

Now go to the bottom, click in the next cell and click Autosum

Change it to average and note down the number.

Now go back to the chrony config

sudo nano /etc/chrony/chrony.conf 

And change your offset on the NMEA line to the number we found. Personally I added a whole new line, so I can easily switch back to 0.00 for tuning in the future

If you are using Serial, leave noselect. If you are using USB, remove noselect.

Also, go back down and re-comment the line to disable logging, save and exit. We can also delete the old log files now, and also restart Chrony

sudo rm /var/log/chrony/statistics.log
sudo systemctl restart chrony

Now do the following command

watch -n 1 chronyc sources

This will look at the Chrony sources, and refresh every 1 seconds

If you are using Serial/PPS, your time should look something like this with the PPS now getting the primary reference, notated by the *. The internet NTP sources will probably have the ^ symbol, notating that they are ready to take over, and have valid time. The NMEA has the ? saying it will not be used, as we used the noselect option on it

If you are using GPS, the same as the above is true, but your NMEA will have the * and there will be no PPS. If your tuning was correct, it will take over. If your tuning was wrong, its possible one of the internet NTP sources will still be primary.

If you add -v to the command, you will get an explanation

watch -n 1 chronyc sources -v

From here there is not much else to do with the GPS configuration, you really just want to make sure its getting a good offset, but note that it will fluctuate.

If you are using USB, because of the overhead of USB, you may notice it fluctuating wildly. But in my testing its always still better than the internet NTP sources. And if you are using Serial, the last sample for the NMEA source may be wildy out of whack. I do not know why, but it doesn't affect the time. Here as an example it shows my NMEA as +13 milliseconds! When we are dealing with time in the nanoseconds, a millisecond is eternity.

Another very handy command is

watch -n 1 chronyc tracking

With PPS, you can get literally SPOT ON to real time. Mine is often below 100, and often time sits at exactly 0. In this screenshot we are indicated 2 nanoseconds off real time. That is an insane amount of accuracy. If you are using USB, your number will be a lot higher. (Though, there is some overhead from the Pi and controller not accounted here)

If you got to here, we have now got the GPS completely tuned, and we can move on to the NTP server configuration.

Note, if you decide now you want to re-tune your GPS and gather more data by turning on logging, you MUST revert to a 0.000 offset first in chrony.

Step 6: NTP Server Configuration

Now we can get on to the other NTP configuration, the part most other guides just skip over.

edit your chrony config file

sudo nano /etc/chrony/chrony.conf 

First, if you are happy with your PPS time and its stable, add prefer to the end, so it looks like this

Somewhere in there (Anywhere, it doesn't matter), we need a line to allow clients to connect to us, if we don't enter this, nothing can connect to Chrony, and its not exactly much of an NTP SERVER. I added the following to allow connections from anywhere, as this device is only on my local network anyway, and I want any and all subnets to connect to it, even in the future.


if your home network was in the IP space, you could enter the above, or


The CIDR /24 is probably correct. If you are on a network with something different, you most likely configured it yourself and would know.

next, we need a manual directive which enables support at run-time for the settime command in chronyc. Easy, its just one word


Then we need an option which took me forever to find and decide on, and its the option for orphan mode and which stratum to report. Here is some more info on stratums

NTP Monitor Driver Guide - Understanding NTP Stratum Ranking and how this Affects Clock Accuracy (Introduction to the NTP Monitor Driver)

The orphan mode is when the internet gets disconnected, and there are no other time servers available abd we are alone. The default configuration for Chrony is that when this happens, it marks your highly accurate GPS time source as unusable. To me, this is crazy. No other guide mentions this, as I suspect they never tested it. Why would I want clients to have no NTP time, vs a highly accurate GPS time source?

I decided to add the line below. Which tells chrony to report to clients, even when there are no other sources online, that this is a stratum 1 time server, which is correct.

local stratum 1

If there is an NTP expert out there who thinks I am wrong, please let me know. All the documentation online wants you to set it to stratum 8 or something even higher. I don't understand why, as the GPS time is CORRECT and even more correct than internet NTP sources usually.

Finally, if you don't want to use, enter your own time servers and comment out that line and add your own. I decided on the following:

# Default NTP
#pool iburst

# Good Time Servers
server iburst
## DON'T USE GOOGLE## server iburst

IBURST sends a burst of eight packets to shorten the time until first sync. Some let you do it, and some really hate it, like I had it enabled, and ended up getting rate limited. Don't add, as it was 20ms off real time (WTF???)


EDIT July 24th 2023. Don't use! Time servers selected here should only serve accurate time. And does NOT, as it does time smearing for the leap second and therefore will not be giving you accurate time, and it will not be in sync with the rest of the servers.

My current recommendations for public NTP servers are the following

server iburst
server iburst

I've not had good luck with using iburst on the US Naval Observatory NTP servers, or NIST. Since we have a GPS clock and other servers in the list, its not really needed anyway. I added the Apple server as so far, its been extremely reliable. It pointed me to a local server in Dallas which is less than 2ms away from my home internet, and the time has stayed very stable from looking at the stats.

People asked my why I don't use, and the answer is that I ended up with a bunch of really wacky domains and addresses in my IPS logs. I guess some of the people volunteering in the NTP Pool are on blacklists which is just not something I wanted to deal with. I also read that there have been many cases of pool members using Google's servers and other smeared time servers as sources, which is something you want to avoid. Personally I don't see much of a reason to point at another stratum 2, or stratum 3 servers, when we can point at the NIST and USNO servers.


For leap seconds, you don't have to add any extra config. Chrony uses 1 of 2 modes to handle this, by default. There are other options also, check the documentation link below the screenshot. If you want to smear the time across a full day like Google is doing, you can do it locally


Bug#974845: chrony: Add leapsecond info
chrony – chrony.conf(5)

At this point you are probably a big enough time nerd you can determine the NTP servers you like (Or at least SOUND like you are a time nerd)

Here is a screenshot of how mine looks now

Go ahead and save the config and reload chrony

sudo systemctl restart chrony

Step 7: Monitoring and verification

Now our NTP server is completely configured. At this point you'll want to use some commands to verify everything is working, such as the ones we've already used

watch -n 1 chronyc sources


watch -n 1 chronyc tracking

Note that refreshing that every 1 seconds takes it toll on low power devices like the Pi 1, so maybe switch to 5 seconds, etc.

Its at this point you will want to button up your hardware and get it where it needs to be. Personally I got it fairly neat, but I plan on getting a better case and making it look better

And into the mess it goes!

Here is the magnetic GPS antenna

In the house its a bit better

Keep monitoring your offsets and make sure nothing goes wonky, and use the command to see how many sats you are seeing now that its been moved

gpspipe -w | jq ".uSat| select( . != null )"

It was at this point where I found I needed a better antenna. On my desk I was getting 8-10 sats, but where they would be located, I was hardly getting 4 on a good day.

Go ahead and set a few systems to use your NTP server, and then you can monitor that traffic with this command

sudo tcpdump -Qin -ni any port 123

Here you can see that clients are actually using it

Another good tool for Windows is this Galleon NTP Check

Galleon Systems - NTP Check
Free NTP check to test the accuracy of any NTP server. Enter the IP address of your time server to submit a time request and receive a full report. Download now.

You just enter an address and it tells you all the details

Thats all! if you've monitored it for a little while and everything is working, go ahead and point all your clients to it! I added it to my DHCP server options for all my subnets, and manually set all the static networking devices.

July 24th 2023 Update

I made the following post, where I found a great adapter to make a Pi Zero into a regular sized Pi

Turn a Raspberry Pi Zero into a full Raspberry Pi with Ethernet
I needed another Raspberry Pi for a project, but with the limited availability I couldn’t find one. The project doesn’t need a very fast CPU, but it must have ethernet. I have an old Raspberry Pi Zero W, but being limited to wireless is a real problem. I also need

And I'm making that into the third NTP server. The mounting hole on the corner of the Pi Zero, which ends up in the middle of the Pi once you add the adapter, makes it perfect for this.

Now with 3 NTP servers on the network, I updated my DHCP server to servce all 3, and went through all my clients to add all 3, but I found a few that only have 2 spaces for NTP servers. So I made some DNS entries

  • (,,
  • (,

Now, if it only has 1 field for servers, I can enter time.* and if it has 2, I enter as the first, and time2.* as the second. Leaving me with all clients accessing all 3 severs

always welcome, please let me know of any mistakes!