Tu App Necesita Workers Silenciosos: El Patrón que Usa Amazon para No Hacerte Esperar
Comprende el teje y maneje de los servicios en segundo plano

Hola dev, tema importante eh... en este artículo te hablaré de la teoría que debes saber, en el siguiente lo implementaré, así que arranquemos!

Cuando envías un mensaje por WhatsApp, cuando Uber te confirma tu viaje antes de encontrar conductor, cuando Netflix te recomienda esa serie que terminaste en una noche, ninguno de esos sistemas te hizo esperar mientras procesaban tu solicitud. La respuesta llegó instantáneamente, pero el trabajo real ocurrió en segundo plano. ¿Cómo lo logran? Con background services.

El problema que ningún usuario quiere ver

Imagina que estás pidiendo comida por Rappi. Confirmas el pedido y la pantalla se queda ahí, cargando. Mientras tanto, la app está enviando emails al restaurante, calculando rutas, procesando el pago, actualizando inventarios. Tu percepción: la app es lenta.

Ahora imagina que la app te dice inmediatamente "¡Pedido confirmado!" y luego te envía updates conforme el domiciliario se acerca. Lo que pasó es que tu solicitud se registró al instante, pero toda la complejidad posterior se delegó en procesos en segundo plano, invisibles para ti pero absolutamente críticos.

La Magia Detrás de las Apps que No Hacen Esperar

Cada vez que una aplicación te da feedback instantáneo pero hace trabajo pesado después, está usando el mismo patrón: registro inmediato, ejecución diferida. Tu email de bienvenida de Amazon no se envía cuando tú te registras, sino que entra a una cola y se procesa después. Tu búsqueda en Slack no busca en tiempo real, consulta índices construidos por procesos nocturnos. Tu viaje en Uber se confirma antes de encontrar conductor porque el matching corre en background.

El error más común en equipos desarrollando sus primeras apps es intentar hacer todo síncronamente. Un usuario registra una cuenta, el sistema le envía un email de bienvenida, guarda el perfil, crea una sesión, genera un token de confirmación. Todo en una petición HTTP que puede tomar varios segundos.

Los estudios de Nielsen Norman Group (1993) establecen que: 0.1 segundos es el límite para que el usuario sienta respuesta instantánea, 1 segundo es el límite para que su flujo de pensamiento no se interrumpa, y 10 segundos es el límite para mantener su atención en la tarea.Además, un estudio de Google de 2019 encontró que el 53% de los usuarios abandonan los sitios que tardan más de 3 segundos en cargar. En la práctica, incluso operaciones de 1-2 segundos generan esa micro-duda: ¿está funcionando? ¿Se quedó pegado? ¿Hago clic de nuevo? jaja seguro también te ha pasado crack.

Tres casos reales que puedes copiar mañana

Amazon para confirmaciones de pedido. Cuando realizas una compra, el proceso de confirmación dura milisegundos. Pero detrás hay servicios que verifican tu forma de pago, reservan inventario, notifican al almacén, calculan rutas de entrega, actualizan sistemas de logística. Si Amazon esperara a que todo terminara antes de mostrar "pedido confirmado", la experiencia sería insoportablemente lenta.

Slack para búsqueda ultrarrápida. Cuando buscas un mensaje antiguo, los resultados aparecen inmediatamente porque Slack no busca en tiempo real. Cada mensaje se indexa automáticamente por procesos en segundo plano. Cuando ejecutas una búsqueda, consultas un índice ya construido, lo cual es órdenes de magnitud más rápido.

Spotify para recomendaciones personalizadas. El análisis de qué canciones podrían gustarte basado en tus hábitos es computacionalmente intenso. No pueden hacerlo cuando estás navegando. Tu actividad se registra constantemente, se procesa durante horas de baja actividad y los resultados se guardan para que cuando abras la app, las recomendaciones estén ahí esperando.

La Arquitectura: Colas, Workers y el Patrón de Reintentos

La arquitectura más común usa colas de mensajes: cuando tu API recibe una solicitud que requiere trabajo asíncrono, la coloca en una cola y responde inmediatamente.

Conceptualmente, el flujo es simple: cuando subes una foto a una red social, la app guarda la imagen temporalmente, te responde "publicado" al instante y en segundo plano hace todo lo demás. Sube la imagen a servidores de almacenamiento, genera diferentes versiones para diferentes tamaños de pantalla, extrae metadata como ubicación si existe, analiza contenido para moderación, actualiza índices de búsqueda. Si algo falla, el sistema reintenta automáticamente sin que el usuario note nada.

El patrón de reintentos con backoff exponencial es estándar: cuando un worker intenta procesar una tarea y falla, programa un reintento para unos segundos después. Si falla otra vez, espera el doble. Este comportamiento evita sobrecargar sistemas que ya están teniendo problemas y les da tiempo a recuperarse.

.NET tiene esto integrado: IHostedService y BackgroundService

Aquí haré un poco más prácticos los conceptos: .NET no necesita librerías externas para esto. El framework tiene todo lo que necesitas con IHostedService y la clase abstracta BackgroundService.

Un hosted service es simplemente una clase que se ejecuta en segundo plano cuando tu aplicación está corriendo. No responde a requests HTTP, no interactúa con usuarios directamente. Solo existe, hace su trabajo y se apaga limpiamente cuando la aplicación se detiene.

La implementación básica es sencilla. Heredas de BackgroundService, sobrescribes ExecuteAsync con tu lógica, y .NET se encarga del ciclo de vida, las dependencias y el apagado controlado.

public class EmailWorker : BackgroundService
{
    private readonly ILogger<EmailWorker> _logger;

    public EmailWorker(ILogger<EmailWorker> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Worker iniciado");

        while (!stoppingToken.IsCancellationRequested)
        {
            await ProcessNextEmailAsync();
            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }
}

Registras el worker en Program.cs junto con cualquier servicio que necesites inyectar:

builder.Services.AddHostedService<EmailWorker>();
builder.Services.AddSingleton<IEmailService, EmailService>();

EmailService se registra como Singleton porque mantiene estado en memoria (la cola de emails). Si usara persistencia externa como base de datos o Redis, podría registrarse como Scoped. El Singleton existe porque el BackgroundService necesita acceder al mismo estado que los controllers.

Lo elegante de este patrón es que el worker puede usar la misma inyección de dependencias que tus controllers. Si necesita un servicio de base de datos, una cola de mensajes o cualquier otra dependencia, simplemente la pide en el constructor como cualquier otra clase.

El Flujo Completo en Código

La separación típica es así: el controller recibe el request, valida rápidamente y encola la tarea. El worker constantemente hace polling, obtiene tareas pendientes, las procesa y marca el resultado. Esto evita que el usuario espere por operaciones que pueden tomar segundos.

Con un estado enumerado (PendingProcessingProcessedFailed) puedes hacer seguimiento de cada tarea. El worker marca como Processing antes de empezar y solo marca Processed cuando termina exitosamente. Si falla, puede incrementar un contador de reintentos y volver a poner la tarea como Pending para reintentarla en el siguiente ciclo.

Esta idempotencia es crítica. Un worker puede caer a mitad de una operación y cuando se reinicie continuará desde donde quedó sin duplicar trabajo. Tu sistema es resiliente a fallos porque ninguna tarea se pierde, solo se retrasa.

Cuándo usar esto y cuándo no

IHostedService es ideal para envío de emails, procesamiento de archivos, tareas programadas como reportes o backups, sincronización con sistemas externos y cualquier operación que no necesite responder inmediatamente al usuario.

No es ideal para tareas que requieren exactly-once guarantees a nivel de mensajes (para eso existen message brokers como RabbitMQ o Azure Service Bus), para procesamiento de millones de items que requieren paralelismo masivo, o para operaciones que el usuario necesita ver el resultado en tiempo real.

La regla simple: si el usuario no necesita el resultado ahora, es candidato para un background service. Si necesita esperar el resultado para continuar, probablemente pertenece al request path.

Lo Que Puedes Implementar Mañana

Empieza con algo pequeño. Toma una operación que tus controllers hacen actualmente y que toma más de medio segundo. Un envío de email, una generación de reporte, una llamada a una API externa. Mueve esa operación a un background service.

El cambio más impactante en la percepción de velocidad es inmediato. Tu API responderá más rápido, tu código será más mantenible y cuando esa operación falle, no romperá el request del usuario. Es una victoria en múltiples frentes.

La próxima vez que alguien diga que su app es lenta, pregúntele cuánto trabajo síncrono están haciendo que podría correr en segundo plano. La respuesta casi siempre es: demasiado.

En el siguiente artículo implementaré todo lo aprendido aquí, échale un vistazo, crack.

Si esta entrada te ha gustado 😉 ya sabes qué hacer...

Créditos de imagen de portada: Foto de Christina Hawkins en Unsplash

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *