Fast browser public-IP lookup using WebRTC ICE candidates and a STUN server.
import { getIP, prefetchIP } from "webrtc-ip";
void prefetchIP().catch(() => undefined); // start as early as possible
const ip = await getIP();webrtc-ip is a browser/client-side library. It requires RTCPeerConnection, so it does not run during SSR or in plain Node.js. In unsupported environments, v5 rejects with a typed WebRTCIPError instead of returning an empty string.
npm install webrtc-ipbun add webrtc-ipimport { getIP } from "webrtc-ip";
try {
const ip = await getIP();
console.log(ip);
} catch (error) {
console.error("Could not resolve IP", error);
}Use it from a client component only:
"use client";
import { useEffect, useState } from "react";
import { getIP, prefetchIP } from "webrtc-ip";
void prefetchIP().catch(() => undefined);
export default function Home() {
const [ip, setIp] = useState<string | null>(null);
useEffect(() => {
getIP()
.then(setIp)
.catch(() => setIp(null));
}, []);
return <p>{ip ?? "Resolvingβ¦"}</p>;
}const ip = await getIP({
stun: [
"stun:stun.l.google.com:19302",
"stun:stun1.l.google.com:19302"
],
timeout: 3000,
cache: true,
cacheTtl: 60_000
});| Option | Default | Description |
|---|---|---|
stun |
"stun:stun.l.google.com:19302" |
STUN URL or URL array. Pass [] with includeLocal: true for local-only candidates. |
timeout |
3000 |
Maximum time to wait for ICE candidates, in ms. |
cache |
true |
Reuse an in-flight/completed lookup for matching options. |
cacheTtl |
Infinity |
How long completed cached lookups remain fresh, in ms. |
includeLocal |
false |
Include host/local candidates. Modern browsers may expose mDNS names instead of private IPs. |
signal |
undefined |
AbortSignal for cancellation. |
import { getIPInfo } from "webrtc-ip";
const info = await getIPInfo();
console.log(info.ip); // "203.0.113.42"
console.log(info.type); // "srflx", "host", "relay", ...
console.log(info.elapsedMs); // lookup duration, not cached-read timeimport { getIPOrNull } from "webrtc-ip";
const ip = await getIPOrNull(); // string | nullconst controller = new AbortController();
const promise = getIP({ signal: controller.signal, timeout: 5000 });
controller.abort();
await promise; // rejects with WebRTCIPError code "ABORTED"getIP(options?: string | string[] | GetIPOptions): Promise<string>
getIPInfo(options?: string | string[] | GetIPOptions): Promise<GetIPInfoResult>
getIPOrNull(options?: string | string[] | GetIPOptions): Promise<string | null>
prefetchIP(options?: string | string[] | GetIPOptions): Promise<string>
clearIPCache(): void
isSupported(): boolean
isWebRTCIPError(error: unknown): error is WebRTCIPErrorFailures reject with WebRTCIPError:
import { getIP, isWebRTCIPError } from "webrtc-ip";
try {
await getIP();
} catch (error) {
if (isWebRTCIPError(error)) {
console.log(error.code);
}
}Error codes:
UNSUPPORTEDβRTCPeerConnectionis unavailable, such as SSR/Node.js.TIMEOUTβ no accepted candidate arrived beforetimeout.NO_PUBLIC_CANDIDATEβ ICE completed without a public/server-reflexive candidate.WEBRTC_BLOCKEDβ browser policy, permissions, or networking blocked WebRTC setup.INVALID_STUN_URLβ a STUN URL is malformed.INVALID_OPTIONSβtimeoutorcacheTtlis invalid.ABORTEDβ the suppliedAbortSignalwas aborted.
prefetchIP()starts the same cached lookup thatgetIP()reads later. This is what makes later calls fast.- If you fire-and-forget
prefetchIP(), attach a.catch()handler because unsupported or blocked environments reject. - WebRTC/STUN still performs a network round trip and can fail on restrictive networks, VPNs, hardened browsers, or when WebRTC/UDP is disabled.
- Local candidates may be masked as
.localmDNS names by modern browsers.
- Unsupported environments now reject with
WebRTCIPErrorcodeUNSUPPORTEDinstead of resolving"". - Package exports now provide both CommonJS and ESM entry points.
- New APIs:
isSupported,getIPInfo,getIPOrNull,isWebRTCIPError,cacheTtl, andAbortSignalsupport.
The author of webrtc-ip is Joey Malvinni.