जब आप अज़रबैजान में कार ब्रोकरेज चला रहे होते हैं और संयुक्त राज्य अमेरिका से साल्वेज वाहन (salvage vehicles) आयात कर रहे होते हैं, तो आपकी सॉफ़्टवेयर समस्याएँ सिलिकॉन वैली स्टार्टअप की समस्याओं से अलग होती हैं। आप दस लाख समवर्ती उपयोगकर्ताओं (concurrent users) के लिए ऑप्टिमाइज़ नहीं कर रहे होते हैं। आप स्पष्टता, अपटाइम और आधी रात को बारह टाइम ज़ोन दूर स्थित एक ऑक्शन हाउस के साथ तालमेल बिठाते हुए चीज़ों को खुद ठीक करने की क्षमता के लिए ऑप्टिमाइज़ कर रहे होते हैं। AutoMakler बनाते समय मैं बिल्कुल इसी स्थिति में था। यह प्लेटफ़ॉर्म लाइव ऑक्शन स्क्रैपिंग और Carfax लुकअप से लेकर डिलीवरी अनुमान और पेमेंट प्रोसेसिंग तक सब कुछ संभालता है। यह वास्तविक ग्राहकों की सेवा करने वाला एक वास्तविक प्रोडक्शन सिस्टम है, और यह उस स्टैक पर चलता है जिसे अधिकांश डेवलपर्स "आक्रामक रूप से बोरिंग स्टैक" (aggressively boring stack) कहेंगे।
वह स्टैक जिसे कोई पिच नहीं करना चाहता
यहाँ कोई React नहीं है। कोई Vue नहीं है। कोई Redis नहीं, कोई Celery नहीं, और कोई WebSocket सर्वर नहीं है। बैकएंड plain Python के साथ FastAPI है। डेटाबेस PostgreSQL है। फ्रंटएंड Jinja2 templates, Bootstrap और थोड़े से vanilla JavaScript का उपयोग करके server-rendered HTML है। स्क्रैपिंग के लिए, मैं Playwright का उपयोग करता हूँ। सब कुछ एक सिंगल Python प्रोसेस के रूप में चलता है जो सीधे HTML सर्व करता है।
यहाँ कोई बिल्ड स्टेप नहीं है। ऑडिट करने के लिए कोई node_modules फोल्डर नहीं है, कॉन्फ़िगर करने के लिए कोई ट्रांसपाइलर नहीं है, और तालमेल बिठाने के लिए कोई फ्रंटएंड फ्रेमवर्क का बदलाव (churn) नहीं है। जब मैं डिप्लॉय करता हूँ, तो मैं Python फ़ाइलें और टेम्पलेट्स मूव कर रहा होता हूँ, न कि बंडलरों की एक पाइपलाइन को ऑर्केस्ट्रेट कर रहा होता हूँ। वह सादगी कोई समझौता नहीं है। वही तो मुख्य उद्देश्य है।
मैसेज ब्रोकर के बिना जॉब्स को कैसे क्यू (Queue) करें
लाइव कार ऑक्शन की स्क्रैपिंग सिंक्रोनस (synchronously) नहीं हो सकती। एक सिंगल स्क्रैप में कई सेकंड लग सकते हैं क्योंकि Playwright पेज लोड करता है, JavaScript निष्पादित करता है, और डेटा निकालता है। इस दौरान उपयोगकर्ता को ब्लॉक करना कोई विकल्प नहीं है। स्टैंडर्ड प्लेबुक कहती है कि Redis इंस्टॉल करें, Celery कॉन्फ़िगर करें और एक वर्कर पूल (worker pool) शुरू करें। मैंने यह सब छोड़ दिया।
इसके बजाय, AutoMakler अपने स्वयं के जॉब क्यू के रूप में Postgres का उपयोग करता है। जब कोई उपयोगकर्ता स्क्रैप ट्रिगर करता है, तो एप्लिकेशन 'pending' स्टेटस के साथ एक नई रो (row) को tasks टेबल में लिख देता है। एक asyncio बैकग्राउंड टास्क उस रो को उठाता है और ब्राउज़र स्क्रैप शुरू कर देता है। इस बीच, ब्राउज़र स्टेटस चेक करने के लिए हर तीन सेकंड में एक लाइटवेट एंडपॉइंट को पोल (poll) करता है। जब रो अपडेट होकर 'completed' हो जाती है, तो पेज रिफ्रेश होता है और परिणाम प्रदर्शित करता है।
यह पैटर्न इसलिए काम करता है क्योंकि पोलिंग इंटरवल इतना छोटा है कि रिस्पॉन्सिव महसूस हो, लेकिन इतना लंबा है कि सर्वर पर दबाव न पड़े। एक कंप्यूटर के लिए तीन सेकंड एक अनंत काल है और किसी बाहरी ऑक्शन साइट का इंतज़ार कर रहे इंसान के लिए यह मुश्किल से ही महसूस होता है। डेटाबेस नेटिवली कंकरेंसी (concurrency) को संभालता है, और क्योंकि जॉब्स केवल Postgres में रो हैं, मैं Celery लॉग्स या Redis कीज़ को खोजने के बजाय एक साधारण SQL क्वेरी के साथ क्यू का निरीक्षण कर सकता हूँ।
वर्कर पूल के बिना सर्वर को चालू कैसे रखें
ब्राउज़र ऑटोमेशन मेमोरी-हंग्री (memory-hungry) होता है। यदि आप एक साथ बहुत अधिक Playwright इंस्टेंस लॉन्च करते हैं, तो आपका सर्वर क्रैश हो जाएगा। पारंपरिक समाधान कंकरेंसी लिमिट के साथ एक मैनेज्ड वर्कर पूल है, जो अक्सर उसी Redis और Celery कॉम्बिनेशन द्वारा समर्थित होता है। मैं Python की एक लाइन का उपयोग करता हूँ: एक asyncio.Semaphore।
सेमाफोर (semaphore) यह तय करता है कि एक साथ कितने ब्राउज़र इंस्टेंस चल सकते हैं। जब कोई नया स्क्रैप अनुरोध आता है, तो यह या तो तुरंत एक स्लॉट ले लेता है या तब तक प्रतीक्षा करता है जब तक कि एक स्लॉट खाली न हो जाए। यह सब एक ही प्रोसेस के अंदर होता है। यहाँ विफल होने के लिए कोई बाहरी ऑर्केस्ट्रेटर नहीं है, कोई वर्कर प्रोसेस नहीं है जो चुपचाप मर जाए, और निगरानी के लिए कोई अतिरिक्त इंफ्रास्ट्रक्चर नहीं है। मेरी मेमोरी प्रेडिक्टेबल रहती है, और सर्वर की रक्षा करने वाला कोड ठीक उसी कोड के बगल में होता है जो इसका उपयोग करता है, न कि किसी डिप्लॉयमेंट मैनिफेस्ट में छिपा होता है।
एक ही कॉलबैक URL के साथ पैसे रूट करना
पेमेंट प्रोसेसिंग ने एक ऐसी बाधा (constraint) पेश की जिसे मैं बदल नहीं सकता था। मेरा पेमेंट गेटवे प्रति मर्चेंट अकाउंट ठीक एक कॉलबैक URL की अनुमति देता है, लेकिन मुझे उसी सिंगल अकाउंट के माध्यम से दो अलग-अलग प्रोजेक्ट्स के लिए ट्रांजेक्शन प्रोसेस करने की आवश्यकता थी। दूसरा मर्चेंट प्रोफाइल बनाने का मतलब होता अतिरिक्त शुल्क, अतिरिक्त अनुपालन (compliance) और अतिरिक्त कागजी कार्रवाई, जिसके लिए एक छोटे ब्रोकरेज के पास समय नहीं होता।
इसका समाधान ग्राहक को गेटवे पर भेजने से पहले ऑर्डर ID स्ट्रिंग में सीधे प्रोजेक्ट का नाम एनकोड करना था। जब कॉलबैक मेरे सर्वर पर पहुँचता है, तो AutoMakler उस ID को डिकोड करता है, पहचानता है कि भुगतान किस प्रोजेक्ट का है, और नोटिफिकेशन को सही इंटरनल हैंडलर को रूट कर देता है। मौजूदा लॉजिक को बिना छेड़े ही यह काम हो गया। यह एक एडिटिव डिज़ाइन (additive design) है: मैंने पेमेंट फ्लो को फिर से नहीं लिखा, मैंने बस आइडेंटिफायर को थोड़ा अधिक संदर्भ (context) देने के लिए बनाया। यह उस तरह का हैक है जो बाद में देखने पर स्पष्ट लगता है लेकिन आर्किटेक्चरल जिम्नास्टिक्स (architectural gymnastics) के घंटों बचा लेता है।
बिना WebSockets के काम करने वाला चैट
Customer support chat is usually where engineers cave and add WebSockets. I needed in-app messaging, but I also needed to keep the infrastructure footprint tiny. So I reused the same polling strategy that powers the auction scrapes.
Messages are stored in Postgres. When a user sends a message, it writes to the table. The client polls for updates, and the UI reflects new messages and read receipts in near real-time. To keep this fast even as the conversation table grows, I added a Postgres partial index that only covers unread messages for active conversations. The database does not waste cycles scanning old history, and the query planner can satisfy most chat lookups with a tight index range scan.
For a support chat where a few seconds of latency is acceptable, this is perfectly adequate. The users get the feedback they need, and I never had to debug a stale WebSocket connection or manage a separate socket server.
The Honest Downsides
This architecture makes real trade-offs, and pretending otherwise would be dishonest. Polling is chatty. Every three seconds, every active client hits the server. The bandwidth and query load are higher than a persistent socket connection would demand. If the Python process restarts, any in-flight background task dies immediately because there is no external worker to pick it back up. I accept this because the tasks are small and the cost of a retry is low. A failed browser scrape can simply be re-triggered by the user.
There is also a ceiling to this approach. If AutoMakler ever needs to serve thousands of simultaneous scrapes, the single-process model with polling will strain. But that is not the business I am in. I need reliability for dozens of concurrent users, not
