Getting the public IP in Node.js and Express — no API key, no rate limits, no hassle
Node.js is one of the most popular runtimes for building APIs, backend services, and CLI tools that need to know about network context. This article covers how to get the public IP address — your own server's or your client's — across the most common Node.js scenarios, using IPPubblico.org: free, no key required, HTTPS, CORS enabled.
Use case 1 — Your server's own public IP (script / CLI)
The simplest case: a Node.js script that needs to know its own public IP. Useful in deployment scripts, diagnostics, DDNS updaters, or any automation that needs to report its own address.
With fetch (Node 18+):
async function getPublicIP() {
try {
const res = await fetch('https://ipv4.ippubblico.org/', { signal: AbortSignal.timeout(5000) });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.text()).trim();
} catch (err) {
console.error('Failed to get public IP:', err.message);
return null;
}
}
const ip = await getPublicIP();
console.log('Public IP:', ip); // 203.0.113.42
With axios:
import axios from 'axios';
async function getPublicIP() {
try {
const { data } = await axios.get('https://ipv4.ippubblico.org/', { timeout: 5000 });
return data.trim();
} catch (err) {
console.error('Failed to get public IP:', err.message);
return null;
}
}
With node-fetch (older Node versions):
import fetch from 'node-fetch';
async function getPublicIP() {
const res = await fetch('https://ipv4.ippubblico.org/');
return (await res.text()).trim();
}
Use case 2 — Full geolocation data
When you need country, city, ISP and timezone in addition to the IP:
async function getIPInfo() {
const res = await fetch('https://ippubblico.org/?api=1', {
signal: AbortSignal.timeout(5000)
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return {
ip: data.ip,
country: data.geo?.country,
countryCode: data.geo?.country_code,
city: data.geo?.city,
region: data.geo?.region,
isp: data.isp,
timezone: data.timezone,
lat: data.geo?.lat,
lon: data.geo?.lon,
};
}
const info = await getIPInfo();
console.log(info);
// {
// ip: '203.0.113.42',
// country: 'Italy',
// countryCode: 'IT',
// city: 'Milan',
// region: 'Lombardy',
// isp: 'Telecom Italia',
// timezone: 'Europe/Rome',
// lat: 45.4654,
// lon: 9.1859
// }
Use case 3 — Express middleware for client IP geolocation
The most common Express scenario: detecting where your users are connecting from. This middleware resolves the client's IP using IPPubblico and attaches the result to req.ipInfo for use in any route.
import express from 'express';
const app = express();
// Simple in-memory cache to avoid hitting the API on every request
const geoCache = new Map();
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
async function resolveIP(ip) {
const cached = geoCache.get(ip);
if (cached && Date.now() - cached.ts < CACHE_TTL) {
return cached.data;
}
try {
const res = await fetch(`https://ippubblico.org/?api=1`, {
signal: AbortSignal.timeout(3000)
});
const data = await res.json();
const result = {
ip: data.ip,
country: data.geo?.country,
countryCode: data.geo?.country_code,
city: data.geo?.city,
isp: data.isp,
timezone: data.timezone,
};
geoCache.set(ip, { data: result, ts: Date.now() });
return result;
} catch {
return null;
}
}
// Middleware
app.use(async (req, res, next) => {
const forwarded = req.headers['x-forwarded-for'];
const clientIP = forwarded ? forwarded.split(',')[0].trim() : req.socket.remoteAddress;
req.clientIP = clientIP;
req.ipInfo = await resolveIP(clientIP);
next();
});
// Use in any route
app.get('/hello', (req, res) => {
const country = req.ipInfo?.country ?? 'Unknown';
res.json({
message: `Hello from ${country}!`,
ip: req.clientIP,
info: req.ipInfo,
});
});
app.listen(3000);
Use case 4 — Country-based access control
Restricting or customising content based on country — without a paid API key:
const BLOCKED_COUNTRIES = ['XX', 'YY']; // ISO country codes
const PREMIUM_COUNTRIES = ['US', 'GB', 'DE', 'FR', 'IT'];
app.use(async (req, res, next) => {
const forwarded = req.headers['x-forwarded-for'];
const ip = forwarded ? forwarded.split(',')[0].trim() : req.socket.remoteAddress;
try {
const geoRes = await fetch('https://ippubblico.org/?api=1', {
signal: AbortSignal.timeout(3000)
});
const data = await geoRes.json();
req.countryCode = data.geo?.country_code ?? null;
} catch {
req.countryCode = null;
}
next();
});
app.get('/content', (req, res) => {
if (BLOCKED_COUNTRIES.includes(req.countryCode)) {
return res.status(403).json({ error: 'Not available in your region' });
}
const isPremiumRegion = PREMIUM_COUNTRIES.includes(req.countryCode);
res.json({
content: 'Here is your content',
tier: isPremiumRegion ? 'premium' : 'standard',
currency: req.countryCode === 'GB' ? 'GBP' : 'EUR',
});
});
Use case 5 — DDNS updater script
A complete Node.js DDNS updater that checks the public IP every 5 minutes and logs when it changes:
import fs from 'fs/promises';
import path from 'path';
const CACHE_FILE = '/tmp/last_known_ip.txt';
const CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes
async function getPublicIP() {
const res = await fetch('https://ipv4.ippubblico.org/', {
signal: AbortSignal.timeout(5000)
});
return (await res.text()).trim();
}
async function getLastKnownIP() {
try {
return (await fs.readFile(CACHE_FILE, 'utf-8')).trim();
} catch {
return null;
}
}
async function saveIP(ip) {
await fs.writeFile(CACHE_FILE, ip, 'utf-8');
}
async function updateDDNS(newIP) {
// Add your DDNS provider API call here
console.log(`[DDNS] Updating record to ${newIP}`);
}
async function check() {
try {
const currentIP = await getPublicIP();
const lastKnownIP = await getLastKnownIP();
if (currentIP !== lastKnownIP) {
console.log(`[IP changed] ${lastKnownIP ?? 'unknown'} → ${currentIP}`);
await updateDDNS(currentIP);
await saveIP(currentIP);
} else {
console.log(`[IP unchanged] ${currentIP}`);
}
} catch (err) {
console.error('[Error]', err.message);
}
}
// Run immediately then every 5 minutes
check();
setInterval(check, CHECK_INTERVAL);
Use case 6 — IPv4 and IPv6 detection
When you need to know both protocols:
async function getBothIPs() {
const res = await fetch('https://ippubblico.org/?text=1', {
signal: AbortSignal.timeout(5000)
});
const text = await res.text();
const lines = text.trim().split('\n');
const parse = (line, prefix) => {
const val = line.replace(prefix, '').trim();
return val === 'NONE' ? null : val;
};
return {
ipv4: parse(lines[0], 'IPv4:'),
ipv6: parse(lines[1], 'IPv6:'),
};
}
const ips = await getBothIPs();
console.log(ips);
// { ipv4: '203.0.113.42', ipv6: null }
// or
// { ipv4: '203.0.113.42', ipv6: '2001:db8::1' }
Handling rate limits
If your application makes many requests from the same IP in a short period, the API responds with 429 Too Many Requests and a Retry-After header. A robust fetch wrapper:
async function fetchWithRetry(url, maxRetries = 2) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '20', 10);
console.warn(`Rate limited. Waiting ${retryAfter}s...`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
continue;
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res;
}
throw new Error('Max retries exceeded');
}
// Usage
const res = await fetchWithRetry('https://ippubblico.org/?api=1');
const data = await res.json();
In practice, caching results (as shown in the Express middleware above) eliminates most rate limit concerns.
Quick reference
| Need | Endpoint | Response |
|---|---|---|
| IPv4 only | https://ipv4.ippubblico.org/ |
203.0.113.42 |
| IPv6 only | https://ipv6.ippubblico.org/ |
2001:db8::1 or NONE
|
| Both protocols | https://ippubblico.org/?text=1 |
IPv4: x\nIPv6: x |
| Full geolocation | https://ippubblico.org/?api=1 |
JSON with country, city, ISP |
Full API documentation: ippubblico.org/docs.html
Conclusion
IPPubblico covers Node.js use cases from a one-liner in a CLI script to a full Express middleware with caching and retry logic. The native fetch API available since Node 18 makes the integration particularly clean — no extra dependencies for the basic case.
The DDNS updater pattern is worth bookmarking: it's a complete, production-ready script that works on any server or VPS with Node.js installed, handling IP change detection, persistence, and error recovery in under 50 lines.
Using this in a Node.js project? Share what you built in the comments.
Top comments (0)