public-ip: your public IP address, fetched over DNS or over HTTPS
Get your public IP address - very fast!
At a glance
- What is it?
- public-ip is a small ES module that answers a question every application eventually asks: what address does the internet see when it reaches me. In Node.js it asks OpenDNS and Google DNS first, which is fast, and it falls back to HTTPS services such as icanhazip and ipify. In a browser it goes straight to the HTTPS services, because a browser cannot open a raw DNS socket.
- Who is it for?
- public-ip fits a Node.js or browser application that needs the egress address of the current connection, for logging, geolocation headers or a diagnostic panel, and it is a good default because it tries the fast path first and falls back to the secure one. It does not fit a server that needs to know the address of the client rather than of the connection, since the result is the address of the outbound interface and everything behind a proxy shares it.
- Can I use it commercially?
- Yes. MIT 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 35 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two transports, chosen by the environment
The whole library exists because there are two ways to ask a remote machine what address it sees you coming from, and they have very different properties.
In Node.js, public-ip queries the DNS records of OpenDNS and Google DNS, and also has HTTPS services in its fallback chain. In browsers, it uses the icanhaz and ipify services through HTTPS, because a browser cannot open a raw DNS socket to a resolver.
That split is the design, and it explains the package structure. The exports map in package.json has three entries: types pointing at source/index.d.ts, a node condition pointing at source/index.js, and a default condition pointing at source/browser.js. The bundler or the runtime picks the implementation that it can actually run, and the same import works in both places.
The public API is three functions. publicIp returns a Promise of a string, your public IPv4 or IPv6 address, and it tries IPv6 first and falls back to IPv4. publicIpv4 and publicIpv6 ask for one family specifically. All three reject on error or on timeout.
The result is deliberately unadorned: a string containing an address.
import {publicIp, publicIpv4, publicIpv6} from 'public-ip';
console.log(await publicIp()); // Tries IPv6 first, falls back to IPv4
//=> 'fe80::200:f8ff:fe21:67cf'
console.log(await publicIpv4());
//=> '46.5.21.123'The DNS path is fast because it is a resolver, not a web server
Why start with DNS at all, when an HTTPS request to an address echo service would be more obvious? The answer is in the mechanics of the two approaches.
A DNS lookup of a special name goes to a resolver, and the resolver answers with the address it saw the query come from. There is no HTTP connection, no TLS handshake and no request payload, and the resolver infrastructure already exists and is already fast. The cost is that DNS is a protocol designed to be lied to. A resolver on the path can answer with whatever it likes, and the address you get back is only as trustworthy as the resolver that answered you.
The HTTPS path goes to a service that returns the address as a plain string over a TLS connection, and the address in the response is bound to the connection that fetched it, which is why the README calls the HTTPS check much more secure and tamper-proof. The price is a full connection setup, and the README says it plainly: it is also a lot slower.
The default behaviour reflects the trade. The default behaviour is to check against DNS before using the HTTPS fallback, and the onlyHttps option, when set to true, will only check against HTTPS. In the Node.js version, that option means a request to the icanhazip.com service, with ipify.org used as a fallback if icanhazip.com fails.
The option is only available in the Node.js version, which is consistent with the transport split. A browser has no DNS socket to skip, so the browser build goes to HTTPS regardless.
Fallbacks, one timeout for the whole operation, and cancellation
Three options control how the lookup behaves, and each of them addresses a different failure.
fallbackUrls is a list of custom HTTPS endpoints to get the public IP from, and they will only be used if everything else fails. There is one requirement on them: any service used as fallback must return the IP as a plain string. The example in the README shows the shape, an array of URLs passed to a publicIpv6 call.
import {publicIpv6} from 'public-ip';
await publicIpv6({
fallbackUrls: [
'https://ifconfig.co/ip'
]
});The timeout option is the one detail worth reading twice. It is the time in milliseconds until the operation is considered timed out, the default is 5000, and it applies to the entire operation, not individual requests. That is the right choice for a fallback chain, since otherwise a stack of four slow services would take four times the timeout to fail, and the caller waiting on a single promise has no way to know which service is slow.
The signal option takes an AbortSignal to cancel the operation, and if both timeout and signal are provided, the operation will be aborted when either the timeout is reached or the signal is aborted. Both routes to the same outcome are supported, which matters in a browser where a component unmounting and a server where a request deadline are the two ways this gets cancelled in practice.
Two error shapes, and only one of them is interesting
The error handling section of the README shows the pattern the library expects, and it is worth reading because the distinction it draws is a good one.
import {publicIpv4, IpNotFoundError} from 'public-ip';
try {
const ip = await publicIpv4({timeout: 5000});
console.log(ip);
} catch (error) {
if (error instanceof IpNotFoundError) {
console.log('Could not determine public IP address');
} else if (error.name === 'AbortError') {
console.log('Request was cancelled');
} else {
console.log('An error occurred:', error.message);
}
}IpNotFoundError is the error thrown when the public IP address could not be found, and it is the one the library defines itself. The other branch is an abort, which is detected by the standard error name rather than by a class, so the abort path needs no import and no instanceof.
Everything else falls into the generic branch and is reported by message. That is the right division of labour. The error the library can do something about, the answer is that the address could not be determined, has a type. The errors it cannot, a network failure or a service returning nonsense, are left to the caller with their message intact.
The one thing a caller must not do is treat a failure as an empty string. A rejected promise and an empty address are very different states, and the library keeps them separate.
A modern ES module with a deliberately small dependency list
The package metadata is worth reading, because it says what kind of library this is.
It is an ES module, with type set to module, and the exports map that we have already looked at, with a node condition and a default condition. The engine requirement is Node >= 20, the published files are just the source directory, and sideEffects is false, so a bundler can drop the import entirely if nothing is used from it.
The runtime dependencies are two: dns-socket, which is the piece that opens the raw DNS query in Node, and is-ip, which validates that what came back is actually an address. There is no HTTP client dependency, because the HTTPS fallback uses whatever the platform provides, and no retry library, because the fallback chain is the retry.
The development side is the usual one for this kind of package: xo for linting, TypeScript for the type declarations, and the shared tsconfig. The test script runs the linter, then the Node test file, then a type check of the declaration file with skipLibCheck.
The repository layout matches. There is a source directory, and three test entry points: test-node.js, test-browser.js and test-browser.html. The browser test is a separate HTML page rather than a bundler-based test, which is the simplest way to check that the browser build works in the environment it actually targets.
The licence is MIT, and the related projects listed in the README are a CLI for this module, public-ip-cli, and a companion library for the other half of the address problem, internal-ip.
What the address is, and where the library stops
There is a conceptual limit that the API does not hide, and it is worth being explicit about before anyone builds on this.
The value returned is the address the internet sees for the connection the request left from. If the machine is behind a NAT, that is the public address of the NAT, not of the device. If there is a proxy or a VPN in the path, it is the address of the last hop that did the request. On a server with several interfaces, the resolver or the service answers with whichever source address the request was made from, so the result depends on the routing that happened to be used.
That is exactly right for the question the library is named after, and wrong for the question a server usually wants answered, which is who called me. A web server that wants the client's address needs the forwarding headers the proxy set, not an outbound lookup, and the two can disagree.
The second limit is the trust question, and the README's own wording points at it. The DNS path is the default because it is fast, and the option to switch to HTTPS only is there because the fast path is easier to manipulate. For a diagnostic panel shown to a developer on their own machine, the default is entirely reasonable. For a value that ends up in an audit log or is used to make an access decision, the slower path is the one to choose, and the cost of that choice is a connection per call.
The third limit is that this is a network call with a five second default budget, made to a third party. A call in a request hot path will add latency and will fail when the third party is down, and neither of those is visible in the library's interface. It is a diagnostic utility, and it behaves like one.
Editorial conclusion
public-ip fits a Node.js or browser application that needs the egress address of the current connection, for logging, geolocation headers or a diagnostic panel, and it is a good default because it tries the fast path first and falls back to the secure one. It does not fit a server that needs to know the address of the client rather than of the connection, since the result is the address of the outbound interface and everything behind a proxy shares it. Before you ship it, decide whether the DNS-based default is acceptable for your threat model, and if the answer depends on the network, enable onlyHttps and accept the slower path.
Frequently asked questions
How do I get my public IP address in Node.js?
Install the package with npm install public-ip and call publicIp(), which returns a Promise of your public IPv4 or IPv6 address, trying IPv6 first and falling back to IPv4. publicIpv4() and publicIpv6() ask for one address family specifically.
What is the difference between the DNS and HTTPS lookups in public-ip?
In Node.js the default is a DNS query to OpenDNS and Google DNS, with HTTPS services as a fallback, and in browsers it uses the icanhaz and ipify services over HTTPS because a browser cannot open a raw DNS socket. The README describes the HTTPS check as much more secure and tamper-proof, but also a lot slower.
How do I make the public-ip lookup use HTTPS only?
Set the onlyHttps option to true, which in the Node.js version queries the icanhazip.com service with ipify.org as a fallback. The option is only available in the Node.js version, since the browser build always uses HTTPS.
How do I add my own service to the public-ip fallback chain?
Pass an array of URLs in the fallbackUrls option. Those endpoints are only used if everything else fails, and any service used as a fallback must return the IP as a plain string.
What happens when public-ip cannot determine the address?
The promise rejects. The library defines IpNotFoundError for the case where the public IP address could not be found, and the abort case is identified by the standard AbortError name, so you can distinguish the two in a catch block.
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/sindresorhus-public-ip)Was this useful?