Detect data-centre and hosting IPs with ASN data and cloud range files

Real customers rarely browse from a server rack, so knowing that a request comes from a cloud network is a useful signal. There's no magic flag for it. Here is how to build the check yourself from ASN data and the providers' own published ranges, and where it goes wrong.

On this page ASN vs ISP vs organization

Sign-ups from cloud servers, checkout attempts from rented machines, scrapers rotating through a hosting provider's addresses: a lot of unwanted automation comes from data centres, and most real people don't. "Is this IP in a data centre?" is a useful question.

Let's be clear about one thing first. IPAPI.GR has no "data centre" or "hosting" flag. What every lookup gives you is the network data you need to build the check yourself: the autonomous system number (asn), the isp and the organization. On any paid plan, the risk object also flags verified AI crawlers, search crawlers and internet scanners. Those flags identify specific operators, not data centres in general.

This guide shows how to combine those fields with the IP range files that cloud providers publish, so you get a check you understand and control.

ASN vs ISP vs organization

The three network fields answer slightly different questions:

FieldAnswersExample
asnWhich network announces this address on the internet?"16509"
ispWhich provider is the address associated with?"Amazon.com, Inc."
organizationWho holds or uses the address block?A hosting customer's name, or the same as isp

The ASN is the most stable key: a number, the same for all of the network's addresses, and it doesn't depend on how a name is spelled. It's a string of digits without the AS prefix. isp and organization are best for display and human review, and for spotting addresses a provider has reassigned to a customer. More on looking up ASNs in ASN lookup by IP.

Like every field, these can be the string "No Data". Treat that as unknown, not as "not a data centre".

Three signals, from strongest to weakest

1. The provider's own published ranges

Several large clouds publish machine-readable lists of their IP ranges. When an address is in one of these, you know it's that provider's infrastructure, from the provider itself:

ProviderFileNotes
AWSip-ranges.amazonaws.com/ip-ranges.jsonprefixes[].ip_prefix, ipv6_prefixes[].ipv6_prefix, with region and service. AWS notes that it doesn't publish ranges for every service, and that customers' own addresses brought to AWS (BYOIP) are not included.
Google Cloudwww.gstatic.com/ipranges/cloud.jsonCustomer-usable external ranges, prefixes[].ipv4Prefix / ipv6Prefix with scope (region)
Microsoft AzureAzure IP Ranges and Service TagsA JSON download, updated weekly, grouped by service tag and region
Oracle Cloudpublic_ip_ranges.jsonregions[].cidrs[].cidr. Oracle asks you to poll at most every 24 hours
Cloudflarecloudflare.com/ipsText lists for IPv4 and IPv6

These files cover a handful of large providers. There are thousands of hosting networks that publish nothing comparable.

2. An ASN list you maintain

Most hosting providers run their own ASNs, so a list of hosting ASNs catches far more providers than the range files do. A few widely documented examples:

text
16509, 14618   Amazon (AWS)
15169, 396982  Google
8075           Microsoft
14061          DigitalOcean
24940          Hetzner
16276          OVH
63949          Akamai Connected Cloud (Linode)
20473          Vultr (The Constant Company)

Confirm each one with whois -h whois.cymru.com " -v AS<number>" or the registry's RDAP before you rely on it. Some of these ASNs also carry traffic you don't want to treat as hosting. Google's and Microsoft's networks carry their crawlers and corporate traffic, for example. That's why the next sections exempt verified crawlers and recommend scoring rather than blocking.

The best way to build the list is from your own traffic. Group a week of requests by ASN, sort by volume, and classify the top networks by hand. The Python script in ASN lookup by IP does the grouping. Hosting networks show up quickly: high volume, few successful logins or purchases, odd user agents.

3. Keywords in the organization name

Organization names containing hosting, cloud, server, data center, VPS or colo are often hosting providers. This is the weakest signal: names are free text, some ISPs are called "Something Cloud", and many hosting companies have names that say nothing. Use it to add candidates to your review queue, not to make decisions.

What goes wrong: false positives

A data-centre address doesn't mean a bot or a fraudster. Expect these legitimate cases:

  • Privacy relays. iCloud Private Relay sends Safari traffic through egress nodes on large CDN networks. Apple publishes those egress ranges. Real users on iPhones and Macs can look like CDN traffic.
  • Corporate VPNs and secure web gateways. Many companies send employee traffic out through cloud-hosted security services.
  • Consumer VPNs. Mostly hosted in data centres. IPAPI.GR doesn't detect VPNs, but a VPN user isn't necessarily doing anything wrong.
  • Cloud desktops and remote workers browsing from virtual machines.
  • Good bots. Search engines, uptime monitors and your own integrations run in data centres. On a paid plan, check risk.is_crawler and risk.is_ai first. They tell you whether an address belongs to a known AI crawler or crawler, so you don't block the bots you want. To check crawlers on your own, see verifying AI crawler IPs.
  • Shared cloud ranges. One abusive customer on a big cloud doesn't make all of its addresses abusive.

So the right response is usually friction, not a block: an email verification at sign-up, a challenge before checkout, a stricter rate limit, manual review above a basket size.

Node.js: ranges + ASN list + verified bots

This loads the AWS and Google Cloud range files at startup and refreshes them daily, then combines them with an ASN list and the API lookup:

js
import net from 'node:net';

const HOSTING_ASNS = new Set(['16509', '14618', '15169', '396982', '8075', '14061', '24940', '16276', '63949', '20473']);
const KEYWORDS = /\b(hosting|cloud|server|data ?cent(er|re)|vps|colo)\b/i;
const known = (v) => (v === undefined || v === null || v === '' || v === 'No Data' ? null : v); // missing fields are "No Data"

let ranges = new net.BlockList();

async function loadRanges() {
  const next = new net.BlockList();
  const add = (cidr) => {
    const [addr, bits] = cidr.split('/');
    next.addSubnet(addr, Number(bits), addr.includes(':') ? 'ipv6' : 'ipv4');
  };
  const aws = await (await fetch('https://ip-ranges.amazonaws.com/ip-ranges.json')).json();
  aws.prefixes.forEach((p) => add(p.ip_prefix));
  aws.ipv6_prefixes.forEach((p) => add(p.ipv6_prefix));
  const gcp = await (await fetch('https://www.gstatic.com/ipranges/cloud.json')).json();
  gcp.prefixes.forEach((p) => add(p.ipv4Prefix || p.ipv6Prefix));
  ranges = next;                                   // swap only after a complete load
}
await loadRanges().catch((e) => console.warn('range load failed:', e.message));
setInterval(() => loadRanges().catch((e) => console.warn('range refresh failed:', e.message)), 24 * 3600 * 1000).unref();

export async function networkSignals(ip) {
  const family = net.isIP(ip) === 6 ? 'ipv6' : 'ipv4';
  const signals = { inPublishedCloudRange: ranges.check(ip, family), hostingAsn: false, orgKeyword: false, verifiedBot: false, asn: null, org: null };
  try {
    const res = await fetch(`https://ipapi.gr/api/lookup/${encodeURIComponent(ip)}?key=${process.env.IPAPI_KEY}`,
      { signal: AbortSignal.timeout(2000) });
    if (res.ok) {
      const d = await res.json();
      signals.asn = known(d.asn);
      signals.org = known(d.organization) ?? known(d.isp);
      signals.hostingAsn = signals.asn !== null && HOSTING_ASNS.has(signals.asn);
      signals.orgKeyword = signals.org !== null && KEYWORDS.test(signals.org);
      signals.verifiedBot = d.risk?.is_crawler === true || d.risk?.is_ai === true;   // paid plans only
    }
  } catch { /* lookup failed: keep the range signal only */ }
  return signals;
}

// Example policy for a sign-up form
export function signupFriction(s) {
  if (s.verifiedBot) return 'none';                            // real crawlers shouldn't be signing up, but don't challenge them on public pages
  if (s.inPublishedCloudRange || s.hostingAsn) return 'challenge';
  if (s.orgKeyword) return 'review';
  return 'none';
}

Two things to note. The ranges are swapped in only after a complete load, so a failed refresh keeps the old list instead of an empty one. And the policy returns levels of friction, so you can log the decision before you start enforcing it.

Python: the same with ipaddress

python
import ipaddress
import os

import requests

session = requests.Session()
HOSTING_ASNS = {"16509", "14618", "15169", "396982", "8075", "14061", "24940", "16276", "63949", "20473"}


def known(v):
    return None if v in (None, "", "No Data") else v  # missing fields are "No Data"


def load_ranges():
    nets = []
    aws = session.get("https://ip-ranges.amazonaws.com/ip-ranges.json", timeout=10).json()
    nets += [p["ip_prefix"] for p in aws["prefixes"]] + [p["ipv6_prefix"] for p in aws["ipv6_prefixes"]]
    gcp = session.get("https://www.gstatic.com/ipranges/cloud.json", timeout=10).json()
    nets += [p.get("ipv4Prefix") or p.get("ipv6Prefix") for p in gcp["prefixes"]]
    collapsed = {4: [], 6: []}
    for n in map(ipaddress.ip_network, nets):
        collapsed[n.version].append(n)
    return {v: list(ipaddress.collapse_addresses(ns)) for v, ns in collapsed.items()}


RANGES = load_ranges()


def network_signals(ip: str) -> dict:
    addr = ipaddress.ip_address(ip)
    s = {"in_cloud_range": any(addr in n for n in RANGES[addr.version]),
         "hosting_asn": False, "asn": None, "org": None, "verified_bot": False}
    try:
        r = session.get(f"https://ipapi.gr/api/lookup/{ip}", params={"key": os.environ["IPAPI_KEY"]}, timeout=2)
        if r.status_code == 200:
            d = r.json()
            s["asn"] = known(d.get("asn"))
            s["org"] = known(d.get("organization")) or known(d.get("isp"))
            s["hosting_asn"] = s["asn"] in HOSTING_ASNS
            risk = d.get("risk") or {}
            s["verified_bot"] = risk.get("is_crawler") is True or risk.get("is_ai") is True
    except requests.RequestException:
        pass
    return s

The any(...) scan is fine for occasional checks. For high request rates, sort the networks and use bisect, or a radix-tree library.

PHP: ASN check at sign-up

In PHP, the per-request ASN check is the practical part. Load range files in a cron job if you need them.

php
<?php
const HOSTING_ASNS = ['16509', '14618', '15169', '396982', '8075', '14061', '24940', '16276', '63949', '20473'];

function known($v) {
    return ($v === null || $v === '' || $v === 'No Data') ? null : $v; // missing fields are "No Data"
}

function network_signals(string $ip): array {
    $ch = curl_init('https://ipapi.gr/api/lookup/' . rawurlencode($ip) . '?key=' . urlencode(getenv('IPAPI_KEY')));
    curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($body === false || $status !== 200) return ['known' => false];

    $d = json_decode($body, true);
    $asn = known($d['asn'] ?? null);
    $org = known($d['organization'] ?? null) ?? known($d['isp'] ?? null);
    return [
        'known'        => true,
        'asn'          => $asn,
        'org'          => $org,
        'hosting_asn'  => $asn !== null && in_array($asn, HOSTING_ASNS, true),
        'verified_bot' => ($d['risk']['is_crawler'] ?? false) === true || ($d['risk']['is_ai'] ?? false) === true,
    ];
}

$s = network_signals($_SERVER['REMOTE_ADDR']);
$requireEmailVerification = ($s['hosting_asn'] ?? false) && !($s['verified_bot'] ?? false);

Go: netip prefixes

go
package netsig

import (
    "encoding/json"
    "net/http"
    "net/netip"
    "net/url"
    "os"
    "time"
)

var (
    client       = &http.Client{Timeout: 2 * time.Second}
    hostingASNs  = map[string]bool{"16509": true, "14618": true, "15169": true, "396982": true, "8075": true, "14061": true, "24940": true, "16276": true, "63949": true, "20473": true}
    cloudPrefixes []netip.Prefix // loaded from the range files at startup
)

func known(s string) string {
    if s == "No Data" { // missing fields are "No Data"
        return ""
    }
    return s
}

func LoadAWS() error {
    resp, err := client.Get("https://ip-ranges.amazonaws.com/ip-ranges.json")
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    var f struct {
        Prefixes     []struct{ IPPrefix string `json:"ip_prefix"` }     `json:"prefixes"`
        IPv6Prefixes []struct{ IPv6Prefix string `json:"ipv6_prefix"` } `json:"ipv6_prefixes"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&f); err != nil {
        return err
    }
    var out []netip.Prefix
    for _, p := range f.Prefixes {
        if pp, err := netip.ParsePrefix(p.IPPrefix); err == nil {
            out = append(out, pp)
        }
    }
    for _, p := range f.IPv6Prefixes {
        if pp, err := netip.ParsePrefix(p.IPv6Prefix); err == nil {
            out = append(out, pp)
        }
    }
    cloudPrefixes = out // guard with a mutex or atomic.Pointer if you refresh while serving
    return nil
}

type Signals struct {
    InCloudRange, HostingASN, VerifiedBot bool
    ASN, Org                              string
}

func For(ip netip.Addr) Signals {
    var s Signals
    for _, p := range cloudPrefixes {
        if p.Contains(ip) {
            s.InCloudRange = true
            break
        }
    }
    resp, err := client.Get("https://ipapi.gr/api/lookup/" + url.PathEscape(ip.String()) + "?key=" + url.QueryEscape(os.Getenv("IPAPI_KEY")))
    if err != nil {
        return s
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
        return s
    }
    var d struct {
        ASN          string `json:"asn"`
        ISP          string `json:"isp"`
        Organization string `json:"organization"`
        Risk         *struct {
            IsAI      bool `json:"is_ai"`
            IsCrawler bool `json:"is_crawler"`
        } `json:"risk"`
    }
    if json.NewDecoder(resp.Body).Decode(&d) != nil {
        return s
    }
    s.ASN = known(d.ASN)
    s.Org = known(d.Organization)
    if s.Org == "" {
        s.Org = known(d.ISP)
    }
    s.HostingASN = hostingASNs[s.ASN]
    s.VerifiedBot = d.Risk != nil && (d.Risk.IsAI || d.Risk.IsCrawler)
    return s
}

risk is a pointer, so it stays nil on plans without bot flags. The struct deliberately leaves out latitude and longitude, which can be "No Data" instead of numbers.

At the edge: friction on sensitive routes only

Apply the check where it matters, such as sign-up, login and checkout, and leave the rest of the site alone:

js
// Cloudflare Worker: challenge hosting ASNs on POST /signup only.
const HOSTING_ASNS = new Set(['16509', '14618', '15169', '396982', '8075', '14061', '24940', '16276', '63949', '20473']);
const known = (v) => (v === undefined || v === null || v === '' || v === 'No Data' ? null : v); // missing fields are "No Data"

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (request.method !== 'POST' || url.pathname !== '/signup') return fetch(request);

    let hosting = false;
    try {
      const ip = request.headers.get('CF-Connecting-IP');
      const res = await fetch(`https://ipapi.gr/api/lookup/${encodeURIComponent(ip)}?key=${env.IPAPI_KEY}`,
        { signal: AbortSignal.timeout(1500) });
      if (res.ok) {
        const d = await res.json();
        const asn = known(d.asn);
        hosting = asn !== null && HOSTING_ASNS.has(asn) && d.risk?.is_crawler !== true;
      }
    } catch { /* unknown: no extra friction */ }

    const upstream = new Request(request);
    upstream.headers.set('X-Network-Hosting', hosting ? '1' : '0');   // origin decides: extra verification step
    return fetch(upstream);
  }
};

Workers also expose request.cf.asn directly, which you can use instead of the lookup if you only need the ASN. The API adds organization, isp and, on paid plans, the verified bot flags.

Roll it out safely

  1. Log first. Record the signals and the decision you would have made for a couple of weeks, without enforcing anything.
  2. Look at what you'd have caught. Check the share of real customers among flagged requests. If many paying customers come from one "hosting" ASN, it's probably a privacy relay or a corporate gateway.
  3. Enforce friction, not blocks, on the routes where abuse happens.
  4. Review the list monthly. Add new hosting ASNs from your logs. Remove ones that cause false positives.
  5. Keep the range files fresh. Refresh daily at most, and subscribe to AWS's change notifications if you depend on its ranges.

FAQ

Does IPAPI.GR have a datacenter or hosting flag?

No. You get asn, isp and organization on every plan, and the verified AI, crawler and scanner flags on every paid plan. The data-centre check in this article is something you build and maintain yourself from those fields and the providers' published ranges.

Can I just block all cloud ASNs?

You can, but you'll block privacy-relay users, corporate VPN users, monitoring services and some good bots. Most sites are better off adding friction on sensitive routes.

Is a residential IP proof of a real person?

No. Residential proxy networks route traffic through real home connections, and compromised devices exist. A residential address lowers suspicion. It doesn't prove anything.

How often do ASNs change?

An address block can move between networks when it's sold or reassigned, but most allocations are stable for long periods. Caching the ASN per IP for hours, or a day, is reasonable.

Related: IP geolocation for fraud prevention · internet scanner IP list.

Look up an IP address now

Free IP lookup: location, network and bot flags, no account needed.