I Built an IP Geo Phone Server — 5 Things I Actually Used
python, #api, #sideprojects, #security
The cheapest server I own isn't a Pi or a refurbished ThinkPad. It's a 2021 Samsung Galaxy A32 with a cracked screen, propped against a windowsill, sucking power from a 5W charger. That phone answers about 4,000 IP geolocation requests every day. My only cost is the trickle of electricity through the cable.
My last side-project VPS cost $38 a month, sat at 3% CPU, ran a cron job I'd swear I'd fix, and spammed me with disk-usage alerts I ignored. That $38 didn't buy compute—it bought guilt. The phone bought me a story.
The whole thing is thirty-eight lines. No geo database, no monthly bill. The phone asks an API, caches the answer for an hour, and serves JSON. Redis keeps my daily API calls under a thousand. The setup uses less RAM than one Chrome tab on my laptop. Code is in the GitHub repo.
from flask import Flask, request, jsonify
import os, requests, redis, logging
app = Flask(__name__)
cache = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
RAPIDAPI_KEY = os.environ["RAPIDAPI_KEY"]
API_HOST = "ip-geolocation44.p.rapidapi.com"
API_URL = f"https://{API_HOST}/v1/geoip"
@app.route("/geo/<ip>")
def geo(ip):
cached = cache.get(ip)
if cached:
return jsonify({"ip": ip, "cached": True, "data": cached})
try:
resp = requests.get(
API_URL,
headers={"X-RapidAPI-Key": RAPIDAPI_KEY, "X-RapidAPI-Host": API_HOST},
params={"ip": ip},
timeout=8,
)
resp.raise_for_status()
data = resp.json()
cache.setex(ip, 3600, str(data))
return jsonify({"ip": ip, "cached": False, "data": data})
except requests.exceptions.Timeout:
return jsonify({"error": "upstream timeout"}), 504
except requests.exceptions.HTTPError as e:
return jsonify({"error": f"upstream {e.response.status_code}"}), 502
except Exception:
logging.exception("geo lookup failed")
return jsonify({"error": "lookup failed"}), 500
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Why a phone? Because I was tired of paying rent on idle silicon
I run a tiny SaaS for sharing invite links—nothing huge, a few hundred sign-ups a week. Fraud was eating my free tier, though: duplicate accounts, VPN sign-ups from the same IP block, people pretending to be in countries they weren't. I needed IP intelligence badly.
Commercial geo services wanted $50+ a month for their smallest plan. My app barely cleared $90. That math stung. The cracked A32 was already in a drawer. So I asked the obvious, stupid question: can this thing be the server?
Phones make terrible servers: Android kills background apps, CGNAT steals your public IP, storage is flash, thermal throttling is real, and a plugged-in battery is a swelling risk. I knew all of that. My workload was also read-only, stateless, and forgiving enough that a dropped request wouldn't kill anyone, so I tried it.
Termux turned that stupid question into a real experiment.
1. Termux + Python: the laziest server stack
I installed Termux from F-Droid, ran pkg install python redis, and had a working environment in ten minutes. No rooting, no custom ROM—just a Linux-ish shell on a phone I'd already paid for.
I picked Flask over FastAPI, not because Flask wins on merit, but because I already knew it and didn't want to debug async event loops while lying on my couch. Laziness is a feature when your server fits in a pocket.
Gunicorn runs four workers. That sounds silly on a phone, but each worker is mostly waiting on the network, and CPU usage peaks at 12%. The phone gets warm, not hot. Redis sits beside it on the same device. Everything lives in /data/data/com.termux/files/home.
Deployment means ssh over Tailscale, git pull, pkill gunicorn, gunicorn app:app. Not elegant, but it stays up. The real surprise was uptime: once I disabled battery optimization, the process stayed alive for three weeks.
2. Ngrok: public URL without router pain
The phone sits behind my ISP's CGNAT, so I don't have a public IPv4 address. Port forwarding is a myth in my apartment building anyway, and I don't feel like fighting carrier-grade NAT just to expose a hobby endpoint.
Ngrok gives me a stable *.ngrok-free.app URL and HTTPS termination. I point my main app at that URL. When the tunnel restarts, the URL changes on the free plan, so I wrote a tiny webhook updater that texts me the new URL. It's dirty, but it works.
The tunnel adds about 80 ms to each request—cheaper than a VPS invoice. The API itself returns in 60 ms, so end-to-end p95 latency is 160 ms. My old VPS did the same work in 180 ms because it sat on a different continent than most of my users. Geography won.
3. Redis on the same phone: caching is non-negotiable
Without caching, 4,000 daily lookups would burn through API quota and mobile data fast. I checked the logs: eighty percent of requests were repeat IPs—bots, returning users, the same VPN exit nodes hammering the sign-up page.
Redis keeps those hits in memory with a one-hour TTL. RAM usage sits around 18 MB, and the cache hit ratio is 82%. That 82% means I can sleep through a data outage. It dropped my API calls from roughly 4,000 a day to about 720—data savings alone justify the cache.
The cache also saves me when mobile data hiccups. My carrier reshapes traffic during peak hours, and a stale geo answer is better than a 504. I learned that the hard way after a 10-minute outage that returned nothing but timeouts.
4. The API: one endpoint, no database
I wanted one HTTP call that returns country, city, lat/lon, timezone, ISP, ASN, VPN/proxy/Tor flags, and reverse-IP domains. I didn't want to ship a MaxMind DB, update it weekly, and pretend I enjoyed maintaining infrastructure. The API I wired in does exactly that.
I use the VPN/Tor flag the most. My app blocks sign-ups from known VPNs during free-trial campaigns. The reverse-IP endpoint is fun too: punch in a suspicious IP and see what other domains share the host. Sometimes it's a cheap shared host. Sometimes it's a bulletproof provider with 400 parked domains.
That reverse-IP feature once burned me. I flagged an IP hosting 300 domains. Every single one was a parked GoDaddy page. I burned an hour chasing a ghost. Now I filter parked pages before alerting.
Batch lookup is the feature I didn't know I needed. I can throw 100 IPs at it in one request. My nightly fraud report now runs in seconds instead of firing 100 sequential calls. That single endpoint replaced three separate tools I used to duct-tape together.
You can grab the hosted endpoints on RapidAPI and the code samples on GitHub.
5. A $5 fan and a wake lock: thermal throttling is real
During the first week, the process died every three days. Android's Doze mode kept putting Termux to sleep, and I thought I was clever until I woke up to 500 failed lookups. termux-wake-lock fixed it—one command, no more dead processes.
Heat was the next enemy. After sustained load the phone throttled. I bought a tiny 40mm USB fan for five dollars and aimed it at the back. CPU frequency stopped bouncing. Response times stabilized. It looks ridiculous, and it absolutely is.
The battery stays at 100% because it's always plugged in. I know that's a fire-risk cliché. Metal tray. Window. If it swells, I'll retire it. I'm still not sure whether this is engineering or just a weird hobby.
The numbers that matter
Before the phone, I paid $38 a month for a VPS that idled at 3% CPU. My p95 latency to users was 180 ms. Now I pay $0 for compute, $0 for the device, and $5 for a fan; p95 latency is 160 ms. API costs scale with usage, not idle time.
The phone uses 180 MB of RAM total and serves 4,000 requests a day. It has no static IP, no RAID, no redundant power, and no dignity. I wouldn't run a payment gateway on it. For a side-project fraud signal, though, it's oddly perfect.
The real win is mental. I stopped maintaining a server I resented and started maintaining a phone I can literally hold in one hand. That shift made the whole project feel smaller, and smaller projects actually ship.
How to use IP Geolocation API
Sign up on RapidAPI, subscribe, and copy your key. Single-IP lookup is a GET request. The batch endpoint accepts up to 100 IPs in one POST. Check the RapidAPI listing for the exact path names; the ones I used look like this.
Single IP with curl:
curl --request GET \
--url 'https://ip-geolocation44.p.rapidapi.com/v1/geoip?ip=8.8.8.8' \
--header 'X-RapidAPI-Key: YOUR_KEY_HERE' \
--header 'X-RapidAPI-Host: ip-geolocation44.p.rapidapi.com'
Single IP with Python:
import os, requests
def lookup_ip(ip: str) -> dict:
url = "https://ip-geolocation44.p.rapidapi.com/v1/geoip"
headers = {
"X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"],
"X-RapidAPI-Host": "ip-geolocation44.p.rapidapi.com",
}
try:
r = requests.get(url, headers=headers, params={"ip": ip}, timeout=8)
r.raise_for_status()
return r.json()
except requests.exceptions.Timeout:
return {"error": "timeout"}
except requests.exceptions.HTTPError as e:
return {"error": f"upstream {e.response.status_code}"}
print(lookup_ip("1.1.1.1"))
Batch lookup with Python:
import os, requests
def lookup_batch(ips: list[str]) -> list[dict]:
url = "https://ip-geolocation44.p.rapidapi.com/v1/batch"
headers = {
"X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"],
"X-RapidAPI-Host": "ip-geolocation44.p.rapidapi.com",
}
try:
r = requests.post(url, headers=headers, json={"ips": ips}, timeout=15)
r.raise_for_status()
return r.json()
except requests.exceptions.Timeout:
return [{"error": "timeout"}]
except requests.exceptions.HTTPError as e:
return [{"error": f"upstream {e.response.status_code}"}]
print(lookup_batch(["1.1.1.1", "8.8.8.8"]))
For batch work, pass a JSON list of IPs. The response is a list of objects you can feed straight into a pandas DataFrame or your fraud pipeline. I run mine nightly and dump the results into SQLite.
Docs and code samples are on GitHub. The hosted endpoints are on RapidAPI.
What I learned (and what still scares me)
Cloud VPS is overrated for low-traffic side projects. Most of us rent a server because that's what we're told to do, not because the workload actually needs it. A phone is worse as a server in almost every measurable way, but for a stateless, cached, low-traffic API proxy it's not just acceptable—it's cheaper, closer to my users, and more fun.
The scariest part isn't hardware failure. It's trust. I don't fully trust Android not to update and break Termux. I don't trust the battery, and I don't trust my ISP not to change CGNAT behavior. Every month I ask myself if I should just move it to a $5 VPS. Every month the phone wins because the bill is zero.
I also learned that IP intelligence is more than a country flag. The VPN/Tor signal catches abuse I used to miss, reverse IP catches shared infrastructure, and batch lookup turns a nightly script into a coffee break. I should've made all three default years ago.
If you want to build the same backend, grab the IP Geolocation API from RapidAPI and the sample code from GitHub. And tell me which IP check you always skip on signups and regret later.
Top comments (0)