When you are running a car brokerage in Azerbaijan and importing salvage vehicles from the United States, your software problems look different from those of a Silicon Valley startup. You are not optimizing for a million concurrent users. You are optimizing for clarity, uptime, and the ability to fix things yourself at midnight while coordinating with an auction house twelve time zones away. That is exactly the situation I found myself in when I built AutoMakler. The platform handles everything from live auction scraping and Carfax lookups to delivery estimates and payment processing. It is a real production system serving real customers, and it runs on what most developers would call an aggressively boring stack.
The Stack That Nobody Wants to Pitch
There is no React. No Vue. No Redis, no Celery, and no WebSocket server. The backend is FastAPI with plain Python. The database is PostgreSQL. The frontend is server-rendered HTML using Jinja2 templates, Bootstrap, and a pinch of vanilla JavaScript. For scraping, I use Playwright. Everything runs as a single Python process that serves HTML directly.
There is no build step. There are no node_modules folders to audit, no transpilers to configure, and no frontend framework churn to keep up with. When I deploy, I am moving Python files and templates, not orchestrating a pipeline of bundlers. That simplicity is not a compromise. It is the entire point.
How to Queue Jobs Without a Message Broker
Scraping a live car auction cannot happen synchronously. A single scrape can take several seconds as Playwright loads the page, executes JavaScript, and extracts the data. Blocking the user while this happens is not an option. The standard playbook says to install Redis, configure Celery, and spin up a worker pool. I skipped all of it.
Instead, AutoMakler uses Postgres as its own job queue. When a user triggers a scrape, the application writes a new row into a tasks table with a status of pending. An asyncio background task picks up that row and launches the browser scrape. Meanwhile, the browser polls a lightweight endpoint every three seconds to check the status. When the row updates to completed, the page refreshes and displays the results.
This pattern works because the polling interval is short enough to feel responsive but long enough to avoid crushing the server. Three seconds is an eternity for a computer and barely noticeable for a human waiting on an external auction site. The database handles concurrency natively, and because the jobs are just rows in Postgres, I can inspect the queue with a simple SQL query instead of digging through Celery logs or Redis keys.
Keeping the Server Alive Without a Worker Pool
Browser automation is memory-hungry. Launch too many Playwright instances at once and your server will collapse. The conventional fix is a managed worker pool with concurrency limits, often backed by that same Redis and Celery combination. I use one line of Python: an asyncio.Semaphore.
The semaphore caps how many simultaneous browser instances can run. When a new scrape request comes in, it either grabs a slot immediately or waits until one opens up. All of this happens inside the same process. There is no external orchestrator to fail, no worker process to silently die, and no additional infrastructure to monitor. My memory stays predictable, and the code that protects the server is right next to the code that uses it, not hidden in a deployment manifest.
Routing Money with One Callback URL
Payment processing introduced a constraint I could not change. My payment gateway allows exactly one callback URL per merchant account, but I needed to process transactions for two separate projects through that single account. Building a second merchant profile would have meant extra fees, extra compliance, and extra paperwork that a small brokerage does not have time for.
The fix was to encode the project name directly into the order ID string before sending the customer to the gateway. When the callback hits my server, AutoMakler decodes that ID, identifies which project the payment belongs to, and routes the notification to the correct internal handler. The existing logic stayed untouched. This is additive design: I did not rewrite the payment flow, I just made the identifier carry a little more context. It is the kind of hack that looks obvious in hindsight but saves hours of architectural gymnastics.
Chat That Works Without WebSockets
Sembang sokongan pelanggan biasanya merupakan tempat jurutera menyerah kalah dan menambah WebSockets. Saya memerlukan mesej dalam aplikasi, tetapi saya juga perlu memastikan jejak infrastruktur kekal kecil. Jadi, saya menggunakan semula strategi polling yang sama yang menggerakkan pengisapan lelongan.
Mesej disimpan dalam Postgres. Apabila pengguna menghantar mesej, ia ditulis ke dalam jadual. Klien melakukan polling untuk kemas kini, dan UI memaparkan mesej baharu serta resit bacaan dalam keadaan hampir masa nyata. Untuk mengekalkan kepantasan ini walaupun jadual perbualan semakin besar, saya menambah indeks separa Postgres yang hanya merangkumi mesej yang belum dibaca untuk perbualan aktif. Pangkalan data tidak membazir kitaran mengimbas sejarah lama, dan perancang pertanyaan boleh memenuhi kebanyakan carian sembang dengan imbasan julat indeks yang padat.
Untuk sembang sokongan di mana kependaman beberapa saat boleh diterima, ini adalah sangat mencukupi. Pengguna mendapat maklum balas yang mereka perlukan, dan saya tidak pernah perlu menyahpepijat sambungan WebSocket yang usang atau menguruskan pelayan soket yang berasingan.
Kelemahan yang Sebenar
Seni bina ini melibatkan pertukaran (trade-offs) yang nyata, dan berpura-pura sebaliknya adalah tidak jujur. Polling bersifat 'chatty'. Setiap tiga saat, setiap klien aktif akan menghubungi pelayan. Jalur lebar dan beban pertanyaan adalah lebih tinggi daripada yang diperlukan oleh sambungan soket yang berterusan. Jika proses Python dimulakan semula, sebarang tugas latar belakang yang sedang berjalan akan mati serta-merta kerana tiada pekerja luaran untuk mengambilnya semula. Saya menerima perkara ini kerana tugas-tugas tersebut adalah kecil dan kos untuk mencuba semula adalah rendah. Pengisapan pelayar yang gagal boleh dicetuskan semula dengan mudah oleh pengguna.
Terdapat juga had kepada pendekatan ini. Jika AutoMakler perlu melayani beribu-ribu pengisapan serentak, model proses tunggal dengan polling akan terbeban. Tetapi itu bukan perniagaan yang saya ceburi. Saya memerlukan kebolehpercayaan untuk berpuluh-puluh pengguna serentak, bukan
