Geo-IP user region detection for website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Geo-IP user region detection for website
Medium
from 1 business day to 3 business days
FAQ

Our competencies:

Development stages

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1171
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    831
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    879
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    453

Setting Up Geo-IP Region Detection on Website

Region detection by IP is foundation for content personalization, regional pricing and analytics. Looks simple but has nuances: database accuracy, IPv6, proxies and VPN, caching, data updates.

Choosing GeoIP Database

MaxMind GeoLite2 — free base, requires registration and license key. Updates every Tuesday. City accuracy for Russia around 80%, countries 98%+.

MaxMind GeoIP2 City — paid version with higher accuracy and ISP data included. Justified for e-commerce with regional pricing.

ip-api.com / ipinfo.io — HTTP API, no local db. Suits small load (thousands requests/day). Adds ~50–200 ms delay on each first visit.

For production site with significant load — only local MaxMind database.

Installing MaxMind GeoLite2

# Install geoipupdate for auto-updates
apt-get install geoipupdate

# /etc/GeoIP.conf
AccountID 123456
LicenseKey your_key_from_account
EditionIDs GeoLite2-City GeoLite2-Country

# Initial download
geoipupdate

# Cron: Wednesday 3:00 (base updates Tuesdays)
0 3 * * 3 /usr/bin/geoipupdate

Database saved to /var/lib/GeoIP/GeoLite2-City.mmdb.

PHP: geoip2/geoip2 Package

composer require geoip2/geoip2
// app/Services/GeoIpService.php
use GeoIp2\Database\Reader;
use GeoIp2\Exception\AddressNotFoundException;

class GeoIpService
{
    private Reader $reader;

    public function __construct()
    {
        $this->reader = new Reader(config('geoip.database_path'));
    }

    public function lookup(string $ip): array
    {
        // Don't lookup RFC1918 addresses — intranet
        if ($this->isPrivateIp($ip)) {
            return $this->defaultResult();
        }

        try {
            $record = $this->reader->city($ip);
            return [
                'country_code' => $record->country->isoCode,         // 'RU'
                'country_name' => $record->country->name,             // 'Russia'
                'region_code'  => $record->subdivisions[0]?->isoCode, // 'MOW'
                'region_name'  => $record->subdivisions[0]?->name,    // 'Moscow'
                'city'         => $record->city->name,                 // 'Moscow'
                'latitude'     => $record->location->latitude,
                'longitude'    => $record->location->longitude,
                'timezone'     => $record->location->timeZone,        // 'Europe/Moscow'
                'is_vpn'       => false, // GeoLite2 doesn't detect VPN
            ];
        } catch (AddressNotFoundException) {
            return $this->defaultResult();
        }
    }

    private function isPrivateIp(string $ip): bool
    {
        return filter_var($ip, FILTER_VALIDATE_IP,
            FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
    }

    private function defaultResult(): array
    {
        return [
            'country_code' => config('geoip.default_country'),
            'region_name'  => null,
            'city'         => null,
            'timezone'     => config('app.timezone'),
        ];
    }
}

Getting Real IP Behind Proxy/Load Balancer

Nginx and cloud load balancers send original IP through headers. Only trust known balancer IPs:

// config/trustedproxies.php or bootstrap/app.php (Laravel 11)
->withMiddleware(function (Middleware $middleware) {
    $middleware->trustProxies(
        proxies: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'],
        headers: Request::HEADER_X_FORWARDED_FOR
    );
})

Cloudflare sends real IP in CF-Connecting-IP header — handle separately:

public function getClientIp(Request $request): string
{
    // Cloudflare
    if ($cf = $request->header('CF-Connecting-IP')) {
        return $cf;
    }
    return $request->ip();
}

Caching Results

MMDB lookup is fast (~0.1 ms), but for high-load sites cache in Redis by IP:

public function lookupCached(string $ip): array
{
    return Cache::remember(
        "geoip:{$ip}",
        86400, // 24 hours
        fn() => $this->lookup($ip)
    );
}

For apps with thousands unique IPs/hour — Redis cache with 24h TTL gives noticeable boost. For typical sites — enough to store in session.

Storing in Session

// app/Http/Middleware/DetectUserRegion.php
public function handle(Request $request, Closure $next): Response
{
    if (!$request->session()->has('geo')) {
        $ip  = app(GeoIpService::class)->getClientIp($request);
        $geo = app(GeoIpService::class)->lookupCached($ip);
        $request->session()->put('geo', $geo);
    }

    View::share('userGeo', $request->session()->get('geo'));

    return $next($request);
}

After first request data in session — GEOIP database not queried again.

IPv6

MaxMind GeoLite2 supports IPv6. Only nuance — PHP filter_var handles IPv6 correctly, but isPrivateIp needs IPv6 ranges:

private function isPrivateIp(string $ip): bool
{
    // Local IPv6 addresses
    if (str_starts_with($ip, '::1') || str_starts_with($ip, 'fc') || str_starts_with($ip, 'fd')) {
        return true;
    }
    return filter_var($ip, FILTER_VALIDATE_IP,
        FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
}

Accuracy and Limitations

GeoLite2 determines city in ~75–85% for Russia users. Larger cities higher accuracy. VPN and corporate proxy users determined by exit node IP — not real location.

For critical scenarios (regional prices) consider manual region selection with cookie — user can correct detection. Solves VPN problem and matches user expectations.

Database Updates

GeoLite2 updates Tuesdays. geoipupdate in cron ensures freshness without manual intervention. On update, flush Redis cache (Cache::tags(['geoip'])->flush() if using tagged cache).