Why I rebuilt India Post tracking in Rust and WebAssembly
From an internal firm tool to a high-ranking public web utility with async connection pooling.
The Trigger & Underlying Problem
The Trigger: Watched our legal ops team spend 45 minutes every morning copy-pasting tracking numbers one by one into the India Post website.
The Core Problem: India Post is the only legally accepted postal carrier for court notice delivery verification in India. But their website is painfully slow, times out frequently, has no bulk tracking mode, and requires solving arithmetic CAPTCHAs for every query.
Why Standard Solutions Were Insufficient: No open API existed for bulk parcel tracking without signing enterprise postal contracts that took months to negotiate.
What It Solves in Practice
Allows single tracking in one click and bulk CSV tracking of 200 consignment numbers simultaneously in seconds.
Legal firms mailing arbitration summons, e-commerce sellers, and ordinary citizens checking consignments for free at trackmypost.in.
Paralegals manually queried numbers one by one, took screenshots of delivery receipts, and saved them to Word files.
Architecture & Key Engineering Decisions
A dual-engine application: frontend written in Rust with the Yew framework compiled to WebAssembly (WASM), paired with an asynchronous FastAPI backend that fans out concurrent HTTPX requests to upstream endpoints.
- Used Rust compiled to WASM for the frontend to eliminate client runtime exceptions and process large CSV tables directly in-browser with zero VDOM lag.
- Used FastAPI with asyncio.gather() and HTTPX connection pooling to fan out 200 requests concurrently without blocking the server thread.
- Containerized stack with Docker Compose and Nginx, where Nginx serves static WASM MIME types and reverse-proxies `/api` to Uvicorn.
import asyncio
import httpx
async def track_single(client: httpx.AsyncClient, tracking_id: str) -> dict:
try:
response = await client.get(f"/upstream/track/{tracking_id}", timeout=8.0)
return parse_tracking_payload(response.text)
except Exception as exc:
return {"id": tracking_id, "status": "RETRY_REQUIRED", "error": str(exc)}
async def track_batch(tracking_ids: list[str]) -> list[dict]:
# Connection pooling with strict limits to prevent socket exhaustion
limits = httpx.Limits(max_keepalive_connections=20, max_connections=50)
async with httpx.AsyncClient(limits=limits) as client:
tasks = [track_single(client, tid) for tid in tracking_ids]
return await asyncio.gather(*tasks)What Failed in Practice & How It Was Addressed
- Upstream rate-limiting: firing 200 requests simultaneously from a single VPS IP address caused India Post to drop connections with HTTP 504 gateway timeouts.
- WebAssembly MIME types: Nginx was serving `.wasm` files as `application/octet-stream` instead of `application/wasm`, causing browsers to refuse WebAssembly.instantiateStreaming.
- Upstream postal scraping service response latency and intermittent timeouts during peak tracking hours.
- Added a token-bucket rate limiter with staggered micro-delays between concurrent HTTPX batches, wrapping each request in an exponential backoff retry loop.
- Configured explicit MIME types in `nginx.conf`: `types { application/wasm wasm; }`, allowing fast streaming compilation in modern browsers.
- Implemented two-phase adaptive polling (15s polling for initial 48s, shifting to 4s polling up to 95s) with timeout resilience against the internal scraping service.
Takeaways & Future Iterations
- WebAssembly is genuinely practical for web apps that do data manipulation in-browser. Zero JavaScript runtime errors is not a gimmick; it saves real debugging time.
- When depending on brittle third-party government services, resilience (exponential backoff, pooling, timeouts) matters far more than raw speed.
Today, I would add a Redis cache layer for recently completed deliveries (e.g. delivered packages whose state will never change again) to eliminate redundant upstream calls completely.