Wenn man eine Autohandelsagentur in Aserbaidschan betreibt und Restwertfahrzeuge aus den USA importiert, sehen die Softwareprobleme anders aus als bei einem Silicon-Valley-Startup. Man optimiert nicht für eine Million gleichzeitiger Nutzer. Man optimiert auf Klarheit, Uptime und die Fähigkeit, Dinge um Mitternacht selbst zu reparieren, während man mit einem Auktionshaus koordiniert, das zwölf Zeitzonen entfernt ist. Genau in dieser Situation befand ich mich, als ich AutoMakler entwickelte. Die Plattform übernimmt alles – vom Live-Auction-Scraping und Carfax-Abfragen bis hin zu Lieferprognosen und der Zahlungsabwicklung. Es ist ein echtes Produktionssystem für echte Kunden, das auf einem Stack läuft, den die meisten Entwickler als „aggressiv langweilig“ bezeichnen würden.
Der Stack, den niemand bewerben möchte
Es gibt kein React. Kein Vue. Kein Redis, kein Celery und keinen WebSocket-Server. Das Backend besteht aus FastAPI mit reinem Python. Die Datenbank ist PostgreSQL. Das Frontend ist servergerendertes HTML unter Verwendung von Jinja2-Templates, Bootstrap und einer Prise Vanilla JavaScript. Für das Scraping verwende ich Playwright. Alles läuft als ein einziger Python-Prozess, der HTML direkt ausliefert.
Es gibt keinen Build-Schritt. Es gibt keine node_modules-Ordner, die man prüfen muss, keine Transpiler, die man konfigurieren muss, und keinen ständigen Wechsel von Frontend-Frameworks, mit dem man Schritt halten muss. Wenn ich deploye, verschiebe ich Python-Dateien und Templates, anstatt eine Pipeline von Bundlern zu orchestrieren. Diese Einfachheit ist kein Kompromiss. Sie ist der eigentliche Punkt.
Wie man Jobs ohne Message Broker in eine Warteschlange stellt
Das Scraping einer Live-Autoauktion kann nicht synchron erfolgen. Ein einziger Scraping-Vorgang kann mehrere Sekunden dauern, da Playwright die Seite lädt, JavaScript ausführt und die Daten extrahiert. Den Nutzer währenddessen zu blockieren, ist keine Option. Das Standardvorgehen sieht vor, Redis zu installieren, Celery zu konfigurieren und einen Worker-Pool zu starten. Ich habe das alles übersprungen.
Stattdessen nutzt AutoMakler Postgres als eigene Job-Queue. Wenn ein Nutzer ein Scraping auslöst, schreibt die Anwendung eine neue Zeile in eine tasks-Tabelle mit dem Status pending. Ein asyncio-Hintergrundtask übernimmt diese Zeile und startet das Browser-Scraping. Währenddessen fragt der Browser alle drei Sekunden über einen leichtgewichtigen Endpoint den Status ab (Polling). Sobald die Zeile auf completed aktualisiert wird, lädt die Seite neu und zeigt die Ergebnisse an.
Dieses Muster funktioniert, weil das Polling-Intervall kurz genug ist, um reaktionsschnell zu wirken, aber lang genug, um den Server nicht zu überlasten. Drei Sekunden sind eine Ewigkeit für einen Computer und kaum spürbar für einen Menschen, der auf eine externe Auktionsseite wartet. Die Datenbank verwaltet Nebenläufigkeit nativ, und da die Jobs einfach nur Zeilen in Postgres sind, kann ich die Warteschlange mit einer einfachen SQL-Abfrage einsehen, anstatt mich durch Celery-Logs oder Redis-Keys zu wühlen.
Den Server ohne Worker-Pool am Laufen halten
Browser-Automatisierung ist speicherhungrig. Startet man zu viele Playwright-Instanzen gleichzeitig, bricht der Server zusammen. Die konventionelle Lösung ist ein verwalteter Worker-Pool mit Nebenläufigkeitsbeschränkungen, oft gestützt durch dieselbe Redis- und Celery-Kombination. Ich verwende eine einzige Zeile Python: ein asyncio.Semaphore.
Das Semaphor begrenzt die Anzahl der gleichzeitig laufenden Browser-Instanzen. Wenn eine neue Scraping-Anfrage eingeht, belegt sie entweder sofort einen Slot oder wartet, bis einer frei wird. All dies geschieht innerhalb desselben Prozesses. Es gibt keinen externen Orchestrator, der ausfallen könnte, keinen Worker-Prozess, der stillschweigend stirbt, und keine zusätzliche Infrastruktur, die überwacht werden muss. Mein Speicherverbrauch bleibt vorhersehbar, und der Code, der den Server schützt, befindet sich direkt neben dem Code, der ihn nutzt, anstatt in einem Deployment-Manifest versteckt zu sein.
Geldtransfers mit nur einer Callback-URL steuern
Die Zahlungsabwicklung brachte eine Einschränkung mit sich, die ich nicht ändern konnte. Mein Payment-Gateway erlaubt genau eine Callback-URL pro Händlerkonto, aber ich musste Transaktionen für zwei separate Projekte über dieses eine Konto abwickeln. Der Aufbau eines zweiten Händlerprofils hätte zusätzliche Gebühren, zusätzlichen Compliance-Aufwand und zusätzlichen Papierkram bedeutet, für den eine kleine Agentur keine Zeit hat.
Die Lösung bestand darin, den Projektnamen direkt in den Order-ID-String zu kodieren, bevor der Kunde zum Gateway weitergeleitet wird. Wenn der Callback meinen Server erreicht, dekodiert AutoMakler diese ID, identifiziert, zu welchem Projekt die Zahlung gehört, und leitet die Benachrichtigung an den korrekten internen Handler weiter. Die bestehende Logik blieb unberührt. Das ist additives Design: Ich habe den Zahlungsfluss nicht neu geschrieben, sondern lediglich den Identifikator mit etwas mehr Kontext versehen. Es ist die Art von Hack, die im Nachhinein offensichtlich erscheint, aber Stunden an architektonischer Akrobatik spart.
Chat, der ohne WebSockets funktioniert
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
