Cuando diriges una correduría de vehículos en Azerbaiyán e importas vehículos siniestrados desde Estados Unidos, tus problemas de software se ven diferentes a los de una startup de Silicon Valley. No estás optimizando para un millón de usuarios concurrentes. Estás optimizando para la claridad, el tiempo de actividad y la capacidad de arreglar las cosas tú mismo a medianoche mientras coordinas con una casa de subastas en una zona horaria con doce horas de diferencia. Esa es exactamente la situación en la que me encontraba cuando construí AutoMakler. La plataforma gestiona todo, desde el scraping de subastas en vivo y las consultas de Carfax hasta las estimaciones de entrega y el procesamiento de pagos. Es un sistema de producción real que sirve a clientes reales, y funciona con lo que la mayoría de los desarrolladores llamarían un stack agresivamente aburrido.

El stack que nadie quiere promocionar

No hay React. No hay Vue. No hay Redis, ni Celery, ni un servidor WebSocket. El backend es FastAPI con Python puro. La base de datos es PostgreSQL. El frontend es HTML renderizado en el servidor utilizando plantillas Jinja2, Bootstrap y una pizca de vanilla JavaScript. Para el scraping, utilizo Playwright. Todo se ejecuta como un único proceso de Python que sirve HTML directamente.

No hay un paso de compilación (build step). No hay carpetas node_modules que auditar, ni transpiladores que configurar, ni el constante cambio de frameworks de frontend al que hay que seguir el ritmo. Cuando despliego, estoy moviendo archivos de Python y plantillas, no orquestando un pipeline de bundlers. Esa simplicidad no es una concesión. Es el objetivo principal.

Cómo encolar tareas sin un message broker

El scraping de una subasta de coches en vivo no puede realizarse de forma sincrónica. Un solo scraping puede tardar varios segundos mientras Playwright carga la página, ejecuta JavaScript y extrae los datos. Bloquear al usuario mientras esto sucede no es una opción. El manual estándar dice que instales Redis, configures Celery y levantes un pool de workers. Yo me salté todo eso.

En su lugar, AutoMakler utiliza Postgres como su propia cola de tareas. Cuando un usuario activa un scraping, la aplicación escribe una nueva fila en una tabla de tareas con un estado de "pending" (pendiente). Una tarea en segundo plano de asyncio toma esa fila y lanza el scraping del navegador. Mientras tanto, el navegador realiza un polling a un endpoint ligero cada tres segundos para comprobar el estado. Cuando la fila se actualiza a "completed" (completado), la página se refresca y muestra los resultados.

Este patrón funciona porque el intervalo de polling es lo suficientemente corto como para sentirse fluido, pero lo suficientemente largo como para evitar saturar el servidor. Tres segundos es una eternidad para una computadora y apenas perceptible para un humano que espera en un sitio de subastas externo. La base de datos gestiona la concurrencia de forma nativa y, como las tareas son simplemente filas en Postgres, puedo inspeccionar la cola con una simple consulta SQL en lugar de tener que buscar entre los logs de Celery o las claves de Redis.

Mantener el servidor vivo sin un worker pool

La automatización del navegador consume mucha memoria. Si lanzas demasiadas instancias de Playwright a la vez, tu servidor colapsará. La solución convencional es un pool de workers gestionado con límites de concurrencia, a menudo respaldado por esa misma combinación de Redis y Celery. Yo utilizo una sola línea de Python: un asyncio.Semaphore.

El semáforo limita cuántas instancias simultáneas del navegador pueden ejecutarse. Cuando llega una nueva solicitud de scraping, esta ocupa un espacio inmediatamente o espera hasta que uno se libere. Todo esto sucede dentro del mismo proceso. No hay un orquestador externo que pueda fallar, no hay procesos de worker que mueran silenciosamente y no hay infraestructura adicional que monitorear. Mi memoria se mantiene predecible, y el código que protege al servidor está justo al lado del código que lo utiliza, no escondido en un manifiesto de despliegue.

Enrutando dinero con una única URL de callback

El procesamiento de pagos introdujo una restricción que no pude cambiar. Mi pasarela de pago permite exactamente una URL de callback por cuenta de comerciante, pero yo necesitaba procesar transacciones para dos proyectos distintos a través de esa misma cuenta. Crear un segundo perfil de comerciante habría significado comisiones extra, más cumplimiento normativo y más papeleo para el que una pequeña correduría no tiene tiempo.

La solución fue codificar el nombre del proyecto directamente en la cadena del ID del pedido antes de enviar al cliente a la pasarela. Cuando el callback llega a mi servidor, AutoMakler decodifica ese ID, identifica a qué proyecto pertenece el pago y enruta la notificación al manejador interno correcto. La lógica existente permaneció intacta. Esto es diseño aditivo: no reescribí el flujo de pago, simplemente hice que el identificador llevara un poco más de contexto. Es el tipo de truco que parece obvio en retrospectiva, pero que ahorra horas de gimnasia arquitectónica.

Chat que funciona sin WebSockets

El chat de atención al cliente es usualmente el punto donde los ingenieros ceden y añaden WebSockets. Necesitaba mensajería dentro de la aplicación, pero también necesitaba mantener la huella de infraestructura al mínimo. Así que reutilicé la misma estrategia de polling que impulsa los scrapes de las subastas.

Los mensajes se almacenan en Postgres. Cuando un usuario envía un mensaje, este se escribe en la tabla. El cliente realiza polling para buscar actualizaciones, y la interfaz de usuario refleja los nuevos mensajes y las confirmaciones de lectura casi en tiempo real. Para mantener esto rápido incluso a medida que la tabla de conversaciones crece, añadí un índice parcial de Postgres que solo cubre los mensajes no leídos de las conversaciones activas. La base de datos no desperdicia ciclos escaneando el historial antiguo, y el planificador de consultas puede satisfacer la mayoría de las búsquedas de chat con un escaneo de rango de índice ajustado.

Para un chat de soporte donde unos pocos segundos de latencia son aceptables, esto es perfectamente adecuado. Los usuarios obtienen la respuesta que necesitan y yo nunca tuve que depurar una conexión WebSocket obsoleta ni gestionar un servidor de sockets independiente.

Las desventajas honestas

Esta arquitectura implica concesiones reales, y pretender lo contrario sería deshonesto. El polling es ruidoso. Cada tres segundos, cada cliente activo realiza peticiones al servidor. El ancho de banda y la carga de consultas son mayores de lo que exigiría una conexión de socket persistente. Si el proceso de Python se reinicia, cualquier tarea en segundo plano en curso muere inmediatamente porque no hay un worker externo que la retome. Acepto esto porque las tareas son pequeñas y el coste de un reintento es bajo. Un scrape de navegador fallido simplemente puede ser reactivado por el usuario.

También hay un límite para este enfoque. Si AutoMakler alguna vez necesita servir miles de scrapes simultáneos, el modelo de proceso único con polling se verá forzado. Pero ese no es el negocio al que me dedico. Necesito fiabilidad para docenas de usuarios concurrentes, no