Home »
Articles
Build a Real-Time Visitor Map: Plotting IP Locations With Leaflet and a Geolocation API
Last updated : September 17, 2026
A live map of where your visitors are coming from is one of the most satisfying things you can put on an internal dashboard, and one of the simplest. The IP address of every request already tells you the approximate location; an IP to map pipeline just needs to resolve it, push the coordinates to a browser and drop a marker. This tutorial builds exactly that in four steps with Node, server-sent events and Leaflet, then adds clustering, a search box and the privacy rules that keep the map on the right side of the law.
What We Are Building: IP to Map in Four Steps
- Step 1: on the server, resolve each request's IP to latitude and longitude with a geolocation API, cached so repeat visitors cost nothing.
- Step 2: stream those coordinates to the dashboard page over server-sent events.
- Step 3: plot them with Leaflet, with clustering so a busy site stays readable.
- Step 4: add a lookup box so you can search an IP location on the map by hand.
You need Node 18 or later, the express package, Leaflet (loaded from a CDN in the page) and an IP geolocation API key. Nothing here needs a paid map provider; Leaflet plus OpenStreetMap tiles is free for modest traffic.
Step 1: Server-Side Lookup With a Geolocation API
The lookup belongs on the server for two reasons: the browser cannot see its own public IP, and the API key must not ship to the client. A geolocation API call returns the coordinates along with the country and city, which we keep for the marker popup. The IP geolocator call is wrapped in a small in-memory cache with a one-day lifetime; IPs do not move, and this cuts API usage to a fraction of request volume.
// geo.js
const cache = new Map();
const TTL = 24 * 60 * 60 * 1000;
async function locate(ip) {
const hit = cache.get(ip);
if (hit && Date.now() - hit.t < TTL) return hit.v;
const url = `https://api.ipstack.com/${ip}?access_key=${process.env.IPSTACK_KEY}&fields=country_code,city,latitude,longitude`;
const res = await fetch(url, { signal: AbortSignal.timeout(4000) });
const d = await res.json();
const v = (!d.error && d.latitude != null) ? { lat: d.latitude, lng: d.longitude, city: d.city, cc: d.country_code } : null;
cache.set(ip, { t: Date.now(), v });
return v;
}
module.exports = { locate };
Step 2: Streaming Coordinates to the Browser (SSE or WebSocket)
Server-sent events are enough here: the data flows one way, from server to dashboard, and SSE needs no library. The Express app records a visit on every page request, resolves it in the background, and broadcasts the result to every open dashboard connection. Note the trust proxy setting; behind a load balancer it is what makes req.ip the real client address instead of the balancer's.
// server.js
const express = require('express');
const { locate } = require('./geo');
const app = express();
app.set('trust proxy', 1); // adjust to your proxy depth
const clients = new Set();
const recent = []; // last 500 points for late joiners
app.get('/events', (req, res) => {
res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });
recent.forEach(p => res.write(`data: ${JSON.stringify(p)}\n\n`));
clients.add(res);
req.on('close', () => clients.delete(res));
});
app.use(async (req, res, next) => {
next(); // never block the page on the lookup
if (req.path === '/events' || req.path.startsWith('/dashboard')) return;
const loc = await locate(req.ip);
if (!loc) return;
const point = { lat: round(loc.lat), lng: round(loc.lng), city: loc.city, cc: loc.cc, t: Date.now() };
recent.push(point); if (recent.length > 500) recent.shift();
const msg = `data: ${JSON.stringify(point)}\n\n`;
clients.forEach(c => c.write(msg));
});
const round = n => Math.round(n * 100) / 100; // ~1 km, see privacy section
app.use('/dashboard', express.static('public'));
app.listen(3000);
Step 3: Locate IP Address on the Map With Leaflet Markers
The dashboard page is one HTML file. It opens the event stream and, for each point, adds a marker. To locate an IP address on the map we already have everything we need in the event payload; Leaflet does the projection.
<!-- public/index.html -->
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/MarkerCluster.Default.css">
<div id="map" style="height:100vh"></div>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<script src="https://unpkg.com/[email protected]/dist/leaflet.markercluster.js"></script>
<script>
const map = L.map('map').setView([20, 0], 2);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',
{ attribution: '© OpenStreetMap contributors' }).addTo(map);
const cluster = L.markerClusterGroup();
map.addLayer(cluster);
new EventSource('/events').onmessage = e => {
const p = JSON.parse(e.data);
const m = L.marker([p.lat, p.lng]).bindPopup(`${p.city || 'Unknown city'}, ${p.cc}`);
cluster.addLayer(m);
};
</script>
Clustering and Heat Layers for High-Traffic Sites
The cluster group above already collapses nearby markers into counted circles, which is what keeps an IP address map readable once you have a few thousand points. For very high volume, swap markers for a heat layer (the leaflet.heat plugin) and feed it the same coordinates; you lose popups but gain a density picture that stays smooth at any scale.
Step 4: Search IP Location on the Map (Lookup Box)
The last piece is a small input that lets you search an IP location on the map by hand, useful when a support ticket includes an address and you want to see it in context. Add a server route that proxies the lookup (so the key stays hidden), then a form on the page:
// server.js (add)
app.get('/api/locate', async (req, res) => {
const ip = String(req.query.ip || '');
if (!/^[0-9a-fA-F:.]+$/.test(ip)) return res.status(400).json({ error: 'bad ip' });
const loc = await locate(ip);
res.json(loc || { error: 'not found' });
});
<!-- index.html (add) -->
<form id="find" style="position:absolute;top:10px;left:50px;z-index:1000">
<input name="ip" placeholder="Search an IP"> <button>Locate</button>
</form>
<script>
document.getElementById('find').onsubmit = async e => {
e.preventDefault();
const ip = e.target.ip.value.trim();
const p = await (await fetch('/api/locate?ip=' + encodeURIComponent(ip))).json();
if (p.error) return alert('No location for that IP');
L.marker([p.lat, p.lng]).addTo(map).bindPopup(`${ip}<br>${p.city || ''}, ${p.cc}`).openPopup();
map.setView([p.lat, p.lng], 8);
};
</script>
Accuracy Honesty: Why Markers Land on City Centers
When you zoom in, you will notice markers stacking on the same spot in each city. That is correct behavior. IP geolocation resolves to the location the network infrastructure is associated with, and for a city that is typically the city centroid or the ISP's local hub, not a house. Mobile carriers often resolve to a regional gateway a long way from the phone. Treat the map as a picture of where your audience is at country and metro level, which is exactly what it is good for, and never as a way to see where a specific person lives.
Privacy by Design: Aggregate Counts, Rounded Coordinates, No Raw IP Storage
The server code above makes three deliberate choices. It never stores the IP address; the point that reaches the browser contains coordinates, city and country only. It rounds coordinates to two decimals, roughly a kilometer, which is more than enough for a map and removes any temptation to over-read precision. And it keeps only a rolling buffer of recent points in memory rather than a permanent log. If you later want history, store hourly counts per city, not per-request rows. Under GDPR an IP address is personal data; a map of rounded, unlinkable points that is shown only to your own team is a far smaller footprint than an analytics table of raw addresses.
Full Code and Demo
The complete project is three files: geo.js, server.js and public/index.html, all shown above. Run npm i express, set IPSTACK_KEY, start the server, open /dashboard and load any other page on the site to watch the first marker appear. The lookup endpoint has a free tier, so an ipstack API key is all you need to see your own traffic on the map within a few minutes.
FAQ
Is it legal to show visitor locations on a map?
For an internal dashboard using aggregated, rounded, non-identifying points, generally yes, provided your privacy notice covers analytics processing and you are not storing raw IPs longer than needed. Showing individual visitors publicly, or combining the map with identifying data, changes the analysis. This is general information, not legal advice.
Can I plot IP locations without a paid map service?
Yes. Leaflet is open source and OpenStreetMap tiles are free for reasonable usage under their policy. For heavy internal traffic, self-host tiles or use a tile provider's free tier so you stay within the usage terms.
SC
About the Author
Shubham Chauhan
Shubham Chauhan writes about IP data, web infrastructure, and developer tooling for the team at
ipstack
,
a real-time IP geolocation API trusted by more than 200,000 companies to locate visitors, personalize user experiences, and detect fraud.