Kiedy prowadzisz brokerską firmę samochodową w Azerbejdżanie i importujesz uszkodzone pojazdy ze Stanów Zjednoczonych, Twoje problemy z oprogramowaniem wyglądają inaczej niż w startupie z Doliny Krzemowej. Nie optymalizujesz pod kątem miliona jednoczesnych użytkowników. Optymalizujesz pod kątem przejrzystości, czasu pracy (uptime) i możliwości samodzielnego naprawiania rzeczy o północy, koordynując działania z domem aukcyjnym znajdującym się w innej strefie czasowej (dwanaście godzin różnicy). Dokładnie w takiej sytuacji się znalazłem, budując AutoMakler. Platforma obsługuje wszystko: od scrapingu aukcji na żywo i sprawdzania Carfax, po szacowanie kosztów dostawy i przetwarzanie płatności. To prawdziwy system produkcyjny obsługujący realnych klientów, działający na stosie technologicznym, który większość programistów nazwałaby agresywnie nudnym.

Stos technologiczny, którego nikt nie chce promować

Nie ma Reacta. Nie ma Vue. Nie ma Redis, Celery ani serwera WebSocket. Backend to FastAPI z czystym Pythonem. Bazą danych jest PostgreSQL. Frontend to renderowany po stronie serwera HTML przy użyciu szablonów Jinja2, Bootstrapa i szczypty vanilla JavaScript. Do scrapingu używam Playwright. Wszystko działa jako pojedynczy proces Python, który bezpośrednio serwuje HTML.

Nie ma etapu budowania (build step). Nie ma folderów node_modules do audytowania, żadnych transpilatorów do konfigurowania ani ciągłej pogoni za nowościami w frameworkach frontendowych. Kiedy wdrażam (deploy), przenoszę pliki Python i szablony, a nie zarządzam potokiem (pipeline) bundlerów. Ta prostota nie jest kompromisem. To jest właśnie cel.

Jak kolejkować zadania bez brokera wiadomości

Scraping na żywej aukcji samochodowej nie może odbywać się synchronicznie. Pojedynczy scraping może trwać kilka sekund, podczas gdy Playwright ładuje stronę, wykonuje JavaScript i wyodrębnia dane. Blokowanie użytkownika w tym czasie nie wchodzi w grę. Standardowy podręcznik mówi: zainstaluj Redis, skonfiguruj Celery i uruchom pulę workerów. Ja pominąłem to wszystko.

Zamiast tego AutoMakler wykorzystuje Postgres jako własną kolejkę zadań. Gdy użytkownik uruchamia scraping, aplikacja zapisuje nowy wiersz w tabeli zadań (tasks) ze statusem pending. Zadanie w tle typu asyncio pobiera ten wiersz i uruchamia scraping w przeglądarce. W międzyczasie przeglądarka co trzy sekundy odpytuje (polls) lekki endpoint, aby sprawdzić status. Gdy status wiersza zmieni się na completed, strona odświeża się i wyświetla wyniki.

Ten wzorzec działa, ponieważ interwał odpytywania jest wystarczająco krótki, by zapewnić responsywność, ale wystarczająco długi, by nie przeciążyć serwera. Trzy sekundy to wieczność dla komputera, a niemal nieodczuwalny moment dla człowieka czekającego na zewnętrzną stronę aukcyjną. Baza danych natywnie obsługuje współbieżność, a ponieważ zadania to po prostu wiersze w Postgres, mogę sprawdzić kolejkę prostym zapytaniem SQL, zamiast przeszukiwać logi Celery lub klucze Redis.

Jak utrzymać serwer przy życiu bez puli workerów

Automatyzacja przeglądarki pożera mnóstwo pamięci. Uruchomisz zbyt wiele instancji Playwright naraz, a Twój serwer padnie. Konwencjonalnym rozwiązaniem jest zarządzana pula workerów z limitami współbieżności, często oparta na tej samej kombinacji Redis i Celery. Ja używam jednej linii w Pythonie: asyncio.Semaphore.

Semafory ograniczają liczbę jednoczesnych instancji przeglądarki, które mogą działać. Gdy wpada nowe żądanie scrapingu, albo natychmiast zajmuje wolne miejsce, albo czeka, aż jedno się zwolni. Wszystko to dzieje się wewnątrz tego samego procesu. Nie ma zewnętrznego orkiestratora, który mógłby zawieść, żadnego procesu worker, który mógłby po cichu zginąć, ani dodatkowej infrastruktury do monitorowania. Zużycie pamięci pozostaje przewidywalne, a kod chroniący serwer znajduje się tuż obok kodu, który go używa, a nie jest ukryty w manifeście wdrożeniowym.

Kierowanie pieniędzy za pomocą jednego adresu URL callback

Przetwarzanie płatności wprowadziło ograniczenie, którego nie mogłem zmienić. Moja bramka płatnicza pozwala na dokładnie jeden adres URL callback na konto sprzedawcy (merchant account), ale musiałem przetwarzać transakcje dla dwóch oddzielnych projektów przez to samo konto. Stworzenie drugiego profilu sprzedawcy oznaczałoby dodatkowe opłaty, dodatkową zgodność (compliance) i dodatkową biurokrację, na którą mała firma brokerska nie ma czasu.

Rozwiązaniem było zakodowanie nazwy projektu bezpośrednio w ciągu identyfikatora zamówienia (order ID) przed przekierowaniem klienta do bramki. Gdy callback trafia na mój serwer, AutoMakler dekoduje to ID, identyfikuje, do którego projektu należy płatność, i kieruje powiadomienie do odpowiedniego wewnętrznego handlera. Istniejąca logika pozostała nienaruszona. To projektowanie addytywne: nie przepisywałem przepływu płatności, po prostu sprawiłem, że identyfikator niesie ze sobą nieco więcej kontekstu. To tego rodzaju „hack”, który z perspektywy czasu wydaje się oczywisty, ale oszczędza godziny gimnastyki architektonicznej.

Czat, który działa bez WebSocketów

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