YC

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.

System: TrackMyPost
Category: Systems · Rust / WASM & FastAPI
Reading Time: 6 min read
Engineering: Rust · WebAssembly (Yew) · FastAPI · Python · Docker Compose · Nginx · Linux
01. WHY I BUILT IT

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.

02. PRACTICAL IMPACT

What It Solves in Practice

PRACTICAL PROBLEM

Allows single tracking in one click and bulk CSV tracking of 200 consignment numbers simultaneously in seconds.

BENEFICIARIES

Legal firms mailing arbitration summons, e-commerce sellers, and ordinary citizens checking consignments for free at trackmypost.in.

BEFORE THIS SYSTEM

Paralegals manually queried numbers one by one, took screenshots of delivery receipts, and saved them to Word files.

03. IMPLEMENTATION

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.

Key Decisions:
  • 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.
File: concurrency_fanout.py
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)
04. CHALLENGES & SOLUTIONS

What Failed in Practice & How It Was Addressed

WHAT WENT WRONG
  • 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.
HOW I SOLVED IT
  • 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.
05. REFLECTION & LESSONS

Takeaways & Future Iterations

What I Learned:
  • 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.
WHAT I WOULD CHANGE NOW:

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.