<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Silviu Technology</title>
    <description>The latest articles on DEV Community by Silviu Technology (@silviutech).</description>
    <link>https://dev.to/silviutech</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4013497%2F7d1c82f4-cd78-412d-908d-981280990b57.png</url>
      <title>DEV Community: Silviu Technology</title>
      <link>https://dev.to/silviutech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/silviutech"/>
    <language>en</language>
    <item>
      <title>FastAPI: prueba emails sin romper tu entorno</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Fri, 24 Jul 2026 05:24:23 +0000</pubDate>
      <link>https://dev.to/silviutech/fastapi-prueba-emails-sin-romper-tu-entorno-4n3a</link>
      <guid>https://dev.to/silviutech/fastapi-prueba-emails-sin-romper-tu-entorno-4n3a</guid>
      <description>&lt;p&gt;Cuando un flujo de registro falla, mucha gente revisa solo si el proveedor "envio algo". En mi experiencia con &lt;code&gt;FastAPI&lt;/code&gt;, eso casi nunca alcanza. El problema de verdad suele estar en la frontera entre API, cola, plantilla y entorno. Si usas datos reales o inboxes mezclados, depurar se vuelve lento y medio confuso.&lt;/p&gt;

&lt;p&gt;Por eso me gusta separar la prueba funcional de email con una regla bien simple: cada ejecucion de staging recibe su propia identidad, su propia traza y su propia inbox temporal. Si necesitas &lt;code&gt;generar correo desechable&lt;/code&gt; para un caso puntual, que sea un recurso de prueba y no una dependencia rara pegada al sistema.&lt;/p&gt;

&lt;h2&gt;
  
  
  El error comun: mezclar pruebas de email y entorno real
&lt;/h2&gt;

&lt;p&gt;He visto equipos guardar direcciones manuales en variables compartidas, reutilizar la misma inbox por dias y luego preguntarse por que el test "a veces" pasa. Tambien aparecen notas como &lt;code&gt;temp mailid&lt;/code&gt; en tickets o mensajes de QA, y eso ya te avisa que el proceso depende demasiado de memoria humana.&lt;/p&gt;

&lt;p&gt;El otro error es mirar solo el correo recibido y no el camino completo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;la API acepta la solicitud&lt;/li&gt;
&lt;li&gt;la cola crea el trabajo&lt;/li&gt;
&lt;li&gt;el worker renderiza la plantilla&lt;/li&gt;
&lt;li&gt;el proveedor intenta la entrega&lt;/li&gt;
&lt;li&gt;la inbox de prueba confirma el resultado&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si solo validas el paso 5, te pierdes casi todo lo util. Para eso me han servido articulos sobre &lt;a href="https://dev.to/hannahdev56/como-revisar-emails-de-reactivacion-trial-7d"&gt;revisar emails de reactivacion con contexto&lt;/a&gt;, porque muestran bien que un email aislado dice poco sin el evento que lo produjo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patron simple para aislar inboxes en FastAPI
&lt;/h2&gt;

&lt;p&gt;Lo que mejor me ha funcionado es crear un &lt;code&gt;run_id&lt;/code&gt; por ejecucion y propagarlo desde la request hasta el envio. No hace falta una arquitectura enorme, de hecho algo pequeño suele durar mas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BackgroundTasks&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid4&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;queue_signup_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inbox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signup_verify&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/signup-test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;signup_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;background_tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BackgroundTasks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;inbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@example.test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;background_tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queue_signup_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inbox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La idea no es el &lt;code&gt;print&lt;/code&gt;, claro. La idea es que el mismo &lt;code&gt;run_id&lt;/code&gt; viva en logs, eventos y revisiones. Asi sabes que inbox corresponde a que intento, y no terminas cazando un &lt;code&gt;tempail mail&lt;/code&gt; viejo de otra corrida. Parece obvio, pero cuando no existe esa relacion, el debugging se pone feo muy rapido.&lt;/p&gt;

&lt;h2&gt;
  
  
  Codigo minimo para crear trazas utiles
&lt;/h2&gt;

&lt;p&gt;Despues del &lt;code&gt;run_id&lt;/code&gt;, yo intento guardar solo tres cosas que casi siempre alcanzan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;estado normalizado: &lt;code&gt;queued&lt;/code&gt;, &lt;code&gt;sent&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;identificador del proveedor&lt;/li&gt;
&lt;li&gt;marca de tiempo del intento&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Con eso puedes contestar preguntas operativas sin exponer contenido sensible ni abrir cinco paneles. Si ademas tu worker registra una razon corta de fallo, ya tienes una base bastante decente.&lt;/p&gt;

&lt;p&gt;Tambien conviene que la automatizacion revise el resultado desde dos angulos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;la API devolvio el &lt;code&gt;run_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;la inbox asociada recibio el mensaje esperado&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ese segundo paso se puede conectar con una guia como &lt;a href="https://dev.to/hannahdev56/como-probar-emails-de-referidos-en-tu-saas-60c"&gt;probar emails de referidos en flujos reales&lt;/a&gt;, sobre todo si en tu producto hay varias plantillas y no quieres validar todo con una sola direccion reciclada.&lt;/p&gt;

&lt;p&gt;Si tu suite crece, una mejora bastante util es persistir un manifiesto corto por ejecucion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b51d7c3e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"template"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup_verify"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Confirma tu cuenta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b51d7c3e@example.test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sent"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No es glamoroso, pero funciona. Ese archivito evita muchas discusiones tontas sobre cual email estabamos mirando, y hace que soporte o QA puedan revisar fallos sin entrar al codigo cada vez.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que validar antes de automatizar mas
&lt;/h2&gt;

&lt;p&gt;Antes de meter mas scripts, yo revisaria esto:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cada entorno usa inboxes distintas o nombres claramente aislados.&lt;/li&gt;
&lt;li&gt;Las pruebas limpian sus artefactos despues de un tiempo razonable.&lt;/li&gt;
&lt;li&gt;El worker puede reintentarse sin duplicar evidencia confusa.&lt;/li&gt;
&lt;li&gt;Los logs no guardan cuerpos completos de emails por defecto.&lt;/li&gt;
&lt;li&gt;Existe una manera corta de unir request, job e inbox.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si esas cinco piezas estan, la automatizacion mejora mucho. Si no estan, cualquier herramienta que agregues solo tapa el ruido por un rato. Es mejor resolver primero el contrato minimo entre API y entrega, aun si al principio se siente un poco boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h2&gt;
  
  
  ¿Necesito una inbox temporal para todas las pruebas?
&lt;/h2&gt;

&lt;p&gt;No. Para unit tests y varias integraciones basta con mocks o con una salida a archivo. La inbox temporal sirve mas cuando quieres validar el mensaje final que veria una persona.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Conviene revisar el HTML completo del email en cada corrida?
&lt;/h2&gt;

&lt;p&gt;Solo en pruebas bien elegidas. Hacerlo siempre suele volver la suite mas fragil de lo necesario. Yo prefiero revisar asunto, CTA principal y uno o dos fragmentos criticos.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Que cambia cuando trabajo con varios entornos?
&lt;/h2&gt;

&lt;p&gt;Que la disciplina importa mas. Si staging, QA y demos comparten destino, tarde o temprano vas a leer el email correcto en el momento equivocado. Y ese tipo de fallo te roba tiempo, aunque paresca pequeño.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>backend</category>
      <category>testing</category>
    </item>
    <item>
      <title>React: evita dobles envíos al reenviar email</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Thu, 23 Jul 2026 23:25:14 +0000</pubDate>
      <link>https://dev.to/silviutech/react-evita-dobles-envios-al-reenviar-email-56m5</link>
      <guid>https://dev.to/silviutech/react-evita-dobles-envios-al-reenviar-email-56m5</guid>
      <description>&lt;p&gt;En muchos productos el botón de reenviar verificación parece una tarea menor, pero acaba tocando UX, métricas y soporte a la vez. Si la persona pulsa dos o tres veces porque el estado tarda en reflejarse, el frontend crea ruido innecesario aunque la API esté bien. En equipos de React esto pasa bastante: el evento sale, pero la interfaz no deja claro que ya está trabajando.&lt;/p&gt;

&lt;p&gt;Lo he visto sobre todo en flujos donde QA repite pruebas con un correo de usar y tirar o una direccion de correo desechable. Cuando la bandeja tarda unos segundos, la tentación natural es volver a pulsar. Ahí se mezclan eventos, se inflan contadores y luego cuesta saber si el fallo era de entrega, de UI o simplemente de impaciencia humana.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por que el boton de reenviar suele romper la experiencia
&lt;/h2&gt;

&lt;p&gt;El problema no suele ser un bug enorme. Suele ser una suma de detalles pequeños:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;el botón cambia de texto tarde&lt;/li&gt;
&lt;li&gt;el spinner aparece sin reservar espacio&lt;/li&gt;
&lt;li&gt;el cooldown se muestra en un lugar distinto al foco&lt;/li&gt;
&lt;li&gt;la telemetría cuenta cada clic aunque el backend ya haya ignorado el duplicado&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eso deja una sensación rara. La persona piensa "no hizo nada", prueba otra vez, y el flujo se vuelve menos confiable de lo que realmente es. En móvil se nota mas, porque el pulgar ya está encima del CTA y el feedback visual llega un poco despues.&lt;/p&gt;

&lt;p&gt;También hay un coste interno. Si tu equipo intenta &lt;a href="https://dev.to/alexcarteruk/checks-de-email-para-ventanas-de-mantenimiento-k8s-5ap8"&gt;probar correos transaccionales sin mezclar eventos&lt;/a&gt;, un botón poco claro arruina esa señal limpia. El frontend no debería fabricar duplicados que luego obligan a explicar dashboards raros.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que estados merece el CTA desde el primer commit
&lt;/h2&gt;

&lt;p&gt;Para mi, el error es tratar "reenviar email" como un único estado booleano. En realidad hay, como mínimo, cuatro estados útiles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;idle&lt;/code&gt;, cuando la acción está disponible.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sending&lt;/code&gt;, cuando ya salió la petición.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cooldown&lt;/code&gt;, cuando el backend aceptó el reenvío pero quieres frenar intentos inmediatos.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;error&lt;/code&gt;, cuando toca explicar qué puede hacer la persona sin sonar brusco.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Separar esos estados hace dos cosas buenas. Primero, la interfaz deja de improvisar. Segundo, la medición mejora porque cada transición tiene significado real. Si sólo alternas entre "activo" y "desactivado", terminas leyendo datos medio opacos.&lt;/p&gt;

&lt;p&gt;También ayuda a accesibilidad. Un mensaje corto en &lt;code&gt;aria-live&lt;/code&gt; puede anunciar "email reenviado, espera 30 s" sin mover el foco ni esconder el botón. Parece un ajuste pequeño, pero hace la interacción bastante mas honesta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patron de React para enfriar el reenvio sin castigar accesibilidad
&lt;/h2&gt;

&lt;p&gt;Prefiero un patrón simple: el botón conserva tamaño, el texto cambia dentro del mismo bloque y el cooldown usa tiempo restante visible. Nada muy fancy, pero funciona bien.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ResendState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cooldown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ResendEmailButton&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ResendState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;secondsLeft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSecondsLeft&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cooldown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;secondsLeft&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setSecondsLeft&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secondsLeft&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cooldown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;secondsLeft&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secondsLeft&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleResend&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;resendVerificationEmail&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;setSecondsLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cooldown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enviando...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cooldown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`Reenviar en &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;secondsLeft&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s`&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Reenviar email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"resend-block"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleResend&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"resend-status"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cooldown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ya enviamos un nuevo enlace.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.resend-block&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.resend-block&lt;/span&gt; &lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;min-inline-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.resend-status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;min-block-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No es un patrón revolucionario, pero evita dos problemas comunes: el doble clic y el layout nervioso. Si además el backend usa idempotencia o rate limiting, mucho mejor. El frontend no reemplaza esa capa; la hace entendible.&lt;/p&gt;

&lt;p&gt;Aquí también intento evitar textos ambiguos. "Inténtalo más tarde" suena seco y no enseña nada. "Ya enviamos un nuevo enlace, espera 30 s" da contexto y baja ansiedad. Es una diferencia chica, pero muy util.&lt;/p&gt;

&lt;h2&gt;
  
  
  Como medir si el cambio realmente mejora el flujo
&lt;/h2&gt;

&lt;p&gt;Yo miraría cuatro señales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tasa de clic duplicado dentro de 10 segundos&lt;/li&gt;
&lt;li&gt;tiempo hasta el siguiente paso exitoso&lt;/li&gt;
&lt;li&gt;errores de reenvío por persona única&lt;/li&gt;
&lt;li&gt;sesiones donde hubo foco perdido o navegación frustrada&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si puedes separar los tests con bandejas dedicadas, todavía mejor. Un equipo que ya sabe &lt;a href="https://dev.to/silviutech/contratos-de-inbox-para-agentes-llm-26i3"&gt;validar cohortes sin contaminar bandejas&lt;/a&gt; tiene medio camino hecho para leer estas métricas con menos ruido. El truco está en distinguir duplicado real de simple repetición manual durante pruebas.&lt;/p&gt;

&lt;p&gt;Cuando QA usa temp gamil com en una nota rápida, no pasa nada. El punto es no dejar que ese tipo de shorthand acabe mezclado con criterios de observabilidad o con nombres de escenarios. Cuanto mas nítido sea el estado del CTA, menos dependes de interpretar capturas a ojo.&lt;/p&gt;

&lt;p&gt;Una referencia útil aquí es el patrón de &lt;a href="https://web.dev/articles/optimistic-ui" rel="noopener noreferrer"&gt;optimistic UI de web.dev&lt;/a&gt;. No porque haya que aplicar optimismo puro en un flujo de verificación, sino porque recuerda algo importante: el feedback de la interfaz debe llegar en el momento correcto, no varios beats despues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Desactivar el botón no perjudica accesibilidad?
&lt;/h3&gt;

&lt;p&gt;No, si mantienes el contexto visible y anuncias el resultado con una región viva. Lo que perjudica mas es dejar un botón activo que parece disponible cuando en realidad no lo está.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Hace falta mostrar el contador?
&lt;/h3&gt;

&lt;p&gt;Yo diría que sí en la mayoría de casos. Reduce incertidumbre y evita la sensación de que el sistema se quedó pensando forever. Además, el tiempo restante ayuda a soporte y QA a entender el comportamiento esperado.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Esto mejora performance o solo UX?
&lt;/h3&gt;

&lt;p&gt;Las dos cosas, aunque sea de forma indirecta. Menos clics duplicados significa menos trabajo redundante, menos ruido de analítica y una lectura mas limpia de la salud del flujo.&lt;/p&gt;

</description>
      <category>react</category>
      <category>performance</category>
      <category>a11y</category>
      <category>webdev</category>
    </item>
    <item>
      <title>FastAPI: logs utiles para colas de email</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Thu, 23 Jul 2026 14:24:34 +0000</pubDate>
      <link>https://dev.to/silviutech/fastapi-logs-utiles-para-colas-de-email-4f6f</link>
      <guid>https://dev.to/silviutech/fastapi-logs-utiles-para-colas-de-email-4f6f</guid>
      <description>&lt;p&gt;Cuando una app en FastAPI manda emails desde una cola, el fallo casi nunca está en &lt;code&gt;send_email()&lt;/code&gt;. El problema suele ser que no sabes qué pasó entre el request original, el worker, el reintento y la confirmación final. En equipos pequeños eso se nota tarde, y en equipos grandes se vuelve un dolor diario.&lt;/p&gt;

&lt;p&gt;En varios backends he visto el mismo patrón: la cola "funciona", pero investigar un email perdido toma demasiado tiempo. Si quieres depurar rapido, necesitas trazabilidad suficiente desde el inicio, no más prints sueltos al final.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por que una cola de email se vuelve opaca tan rapido
&lt;/h2&gt;

&lt;p&gt;Una tarea de email normalmente cruza varias capas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;endpoint HTTP&lt;/li&gt;
&lt;li&gt;validación de payload&lt;/li&gt;
&lt;li&gt;enqueue del job&lt;/li&gt;
&lt;li&gt;worker async&lt;/li&gt;
&lt;li&gt;proveedor externo&lt;/li&gt;
&lt;li&gt;actualización de estado&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si cada capa escribe logs distintos, sin una clave común, buscar el origen del problema se vuelve bastante lento. Realesmente no hace falta una plataforma enorme para arreglarlo; hace falta disciplina en los eventos y nombres consistentes.&lt;/p&gt;

&lt;p&gt;También conviene diferenciar dos preguntas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿el job fue aceptado?&lt;/li&gt;
&lt;li&gt;¿el email fue entregado o falló?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mezclar esas dos respuestas en un solo log complica mucho el soporte. Este enfoque se parece a cómo se diseñan los &lt;a href="https://dev.to/silviutech/contratos-de-inbox-para-agentes-llm-26i3"&gt;contratos de inbox para automatización&lt;/a&gt;: primero dejas claro qué evento ocurrió, luego validas la siguiente transición.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que guardar en cada evento del worker
&lt;/h2&gt;

&lt;p&gt;Mi regla simple es: cada intento debe poder entenderse sin abrir cinco dashboards. Para eso, cada evento de log debería incluir como mínimo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;job_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;request_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user_id&lt;/code&gt; o un identificador seguro del destinatario&lt;/li&gt;
&lt;li&gt;&lt;code&gt;template_name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attempt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;provider&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;duration_ms&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si usas un &lt;code&gt;correlation_id&lt;/code&gt; estable desde FastAPI hasta el worker, ya ganaste bastante. No resuelve todo, pero reduce mucho el tiempo de diagnóstico.&lt;/p&gt;

&lt;p&gt;Tambien vale la pena registrar el motivo de reintento con una categoría corta. Algo como &lt;code&gt;timeout&lt;/code&gt;, &lt;code&gt;provider_5xx&lt;/code&gt; o &lt;code&gt;invalid_recipient&lt;/code&gt; es más útil que una excepción gigante pegada en una sola línea. La excepción completa puede vivir aparte.&lt;/p&gt;

&lt;p&gt;Un detalle que evita bugs tontos: normaliza entradas dudosas antes de encolar. He visto datos de prueba como &lt;code&gt;temp gamil com&lt;/code&gt; o &lt;code&gt;tempail mail&lt;/code&gt; colarse en staging y contaminar métricas. No es grave por si mismo, pero ensucia el analisis si nadie marca que era un dato artificial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patron simple en FastAPI para seguir cada intento
&lt;/h2&gt;

&lt;p&gt;Un patrón muy usable es separar el evento de negocio del evento de entrega. El endpoint solo registra que se pidió enviar un email. El worker registra cada intento real.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BackgroundTasks&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid4&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_email_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_attempt_started&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attempt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;started&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# aqui llamarias a tu proveedor
&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_attempt_finished&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attempt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/emails&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;queue_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EmailRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BackgroundTasks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;job_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;request_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_job_queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;send_email_job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No es una arquitectura exotica, pero funciona muy bien para equipos que todavía no quieren meter otra capa compleja. Si luego migras a Celery, RQ o Dramatiq, la idea base sigue siendo la misma: un identificador por solicitud y eventos cortos, comparables, y faciles de buscar.&lt;/p&gt;

&lt;p&gt;Cuando el flujo crece, me gusta combinar esto con runbooks sencillos. Ese enfoque encaja con estos &lt;a href="https://dev.to/silviutech/llms-con-runbooks-de-email-que-si-escalan-5cje"&gt;runbooks de email que escalan&lt;/a&gt;, porque obligan a documentar qué estados importan de verdad y cuál es la acción esperada en cada uno.&lt;/p&gt;

&lt;h2&gt;
  
  
  Como depurar reintentos sin duplicar ruido
&lt;/h2&gt;

&lt;p&gt;Los reintentos ayudan, pero si no se diseñan bien esconden el fallo en vez de aclararlo. Un error comun es registrar el mismo mensaje completo en cada vuelta. Eso infla los logs y hace más dificil detectar la causa original.&lt;/p&gt;

&lt;p&gt;Prefiero esta secuencia:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;un evento corto cuando se agenda el reintento&lt;/li&gt;
&lt;li&gt;un evento corto cuando empieza el nuevo intento&lt;/li&gt;
&lt;li&gt;un evento final con resultado y duración&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Con eso puedes responder preguntas utiles muy rapido:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿cuántos jobs terminan en el segundo intento?&lt;/li&gt;
&lt;li&gt;¿qué plantilla falla más?&lt;/li&gt;
&lt;li&gt;¿el proveedor tarda más en ciertas horas?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si además guardas un resumen final por job, el soporte puede revisar un caso sin leer todo el stream. No necesitas perfección; necesitas señales consistentes. Esa pequeña diferencia hace que una cola de email pase de "mas o menos estable" a algo que el equipo puede operar con calma.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h2&gt;
  
  
  ¿Necesito tracing distribuido para esto?
&lt;/h2&gt;

&lt;p&gt;No al principio. Con &lt;code&gt;request_id&lt;/code&gt;, &lt;code&gt;job_id&lt;/code&gt; y eventos bien nombrados ya puedes resolver una parte grande del problema.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Cuándo separar logs de negocio y logs técnicos?
&lt;/h2&gt;

&lt;p&gt;Lo antes posible. Los logs de negocio explican qué quería hacer el sistema; los técnicos explican cómo salió ese intento. Juntarlos suele crear confusión.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué revisaría primero si faltan emails?
&lt;/h2&gt;

&lt;p&gt;Buscaría jobs aceptados sin evento final, luego tiempos altos por proveedor y después errores repetidos por plantilla. Ese orden normalmente te lleva al problema real bastante mas rapido.&lt;/p&gt;

&lt;p&gt;Si tu cola ya envia emails pero nadie confía en sus señales, empezar por los logs correctos suele dar más retorno que cambiar de librería. No es glamoroso, pero si muy efectivo.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>backend</category>
      <category>automation</category>
    </item>
    <item>
      <title>Make Playwright Email Tests Less Flaky</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Thu, 23 Jul 2026 05:24:49 +0000</pubDate>
      <link>https://dev.to/silviutech/make-playwright-email-tests-less-flaky-49df</link>
      <guid>https://dev.to/silviutech/make-playwright-email-tests-less-flaky-49df</guid>
      <description>&lt;p&gt;I like email verification tests when they prove one narrow thing: a real user can finish signup without a hidden messaging bug. I do not like them when they become a slot machine. If your Playwright suite sometimes sees the email, sometimes grabs the wrong one, and sometimes times out for no obvious reason, the test is usually missing structure rather than missing retries.&lt;/p&gt;

&lt;p&gt;This is the workflow I keep coming back to for Playwright and Automation teams. It is not fancy, but it is dependable and a bit easier to explain during failure review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these tests fail in different ways
&lt;/h2&gt;

&lt;p&gt;Most flaky email tests fail for one of three reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;two runs read from the same inbox&lt;/li&gt;
&lt;li&gt;the test matches the first email instead of the correct email&lt;/li&gt;
&lt;li&gt;the failure leaves almost no evidence behind&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sounds simple, yet it creates very different symptoms. One build looks green locally and red in CI. Another run passes but verifies the wrong account. A third one times out and gives you zero clue where the delay realy happened.&lt;/p&gt;

&lt;p&gt;Before changing the locator or the timeout again, I try to make the test answer a boring question first: "what exact inbox and message was this run supposed to use?"&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Give each run its own inbox identity
&lt;/h2&gt;

&lt;p&gt;Shared inboxes are the fastest way to create noisy failures. If your suite points every signup test at the same temp mailbox, you are testing message arrival and accidental cross-talk at the same time. That is not a fair test.&lt;/p&gt;

&lt;p&gt;I prefer an address pattern that carries run identity inside it, even if the provider is just a temporary inbox used for QA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;parallelIndex&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`signup-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@example.test`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact address format does not matter much. What matters is that the run id appears in the inbox name, your test logs, and ideally the application-side metadata too. The same idea shows up in &lt;a href="https://dev.to/silviutech/name-test-inboxes-in-playwright-2og5"&gt;naming inboxes per test run&lt;/a&gt;: once every run has a distinct identity, the wierd failures get much less mysterious.&lt;/p&gt;

&lt;p&gt;If your team uses a temp mail so service or a temp mailbox provider for staging checks, keep it scoped to one test run and expire it quickly. I have also seen people type temp org mail into setup notes when they mean "some throwaway inbox here"; that is fine as shorthand, but the test itself should still record the actual mailbox it created.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Wait for the right email, not any email
&lt;/h2&gt;

&lt;p&gt;The next problem is matching. A lot of tests poll an inbox and click the first message with "Verify" in the subject. That is a brittle shortcut. In busy environments, that approach drifts fast.&lt;/p&gt;

&lt;p&gt;Instead, wait on a small contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;expected recipient&lt;/li&gt;
&lt;li&gt;expected subject pattern&lt;/li&gt;
&lt;li&gt;expected creation window&lt;/li&gt;
&lt;li&gt;expected text unique to this flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That contract can stay small and still be strong. For example, if the UI shows the email address back to the user, assert against that exact value before you ever open the inbox. Then look for a message created after the signup action and containing the app or tenant name. This avoids a bunch of fake greens.&lt;/p&gt;

&lt;p&gt;Here is the shape I use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subjectIncludes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verify your account&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;receivedAfter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;bodyIncludes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Finish creating your workspace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does two helpful things. First, it narrows the search space. Second, it tells you what the test believed should happen, which makes failure analysis way more usefull than "timed out after 30s".&lt;/p&gt;

&lt;p&gt;If you run email checks in CI, the operational pattern from &lt;a href="https://dev.to/jasonmills94/bluegreen-release-emails-for-kubernetes-ops-3bff"&gt;staging inbox smoke tests&lt;/a&gt; is worth borrowing too: keep the inbox logic narrow, observable, and tied to one deploy or one run, not to the whole environment forever. Keeping those concerns seperate helps a lot when a flaky failure only appears under load.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Save evidence when the test fails
&lt;/h2&gt;

&lt;p&gt;This is the step teams skip because the happy path already works. Then a red build shows up, and nobody knows if the app failed to send, the inbox API lagged, or the test matched the wrong message.&lt;/p&gt;

&lt;p&gt;When a Playwright email test fails, save three bits of evidence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the generated email address&lt;/li&gt;
&lt;li&gt;the poll criteria&lt;/li&gt;
&lt;li&gt;the latest inbox snapshot or message list&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need to dump everything. A short structured artifact is usualy enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expectedStatus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email-debug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;criteria&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verify your account&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;bodyIncludes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Finish creating your workspace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;inboxSnapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listRecent&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the failure review starts from evidence instead of guesswork. That is also why I like storing the inbox name in the same run metadata you use for the rest of the suite. The principle is pretty close to &lt;a href="https://dev.to/jasonmills94/docker-build-emails-need-run-ids-in-aws-n96"&gt;naming inboxes per test run&lt;/a&gt;: once artifacts carry the run identity, retries stop erasing the story.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short checklist I reuse
&lt;/h2&gt;

&lt;p&gt;Before I call an email test "stable enough", I want this checklist to be true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each test run gets a unique inbox or alias&lt;/li&gt;
&lt;li&gt;the app logs the email target for the run&lt;/li&gt;
&lt;li&gt;the inbox poll filters by recipient and time window&lt;/li&gt;
&lt;li&gt;the assertion checks a flow-specific string, not just any email&lt;/li&gt;
&lt;li&gt;failures attach enough evidence for another engineer to debug it&lt;/li&gt;
&lt;li&gt;the suite keeps mailbox lifetime short so old mail does not bleed into new runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not a huge list, but it catches most of the avoidable flake I see in QA suites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Should I just increase the timeout?
&lt;/h2&gt;

&lt;p&gt;Only after the matching rules are good. Longer waits can hide shared-inbox bugs and make the suite slower without making it safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is a temporary inbox okay for real projects?
&lt;/h2&gt;

&lt;p&gt;Yes, for staging and test flows. Just keep the scope narrow, avoid real user data, and do not confuse a passing inbox check with full delivery validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if multiple emails are expected in one flow?
&lt;/h2&gt;

&lt;p&gt;Give each message its own assertion contract. If you collapse "verification", "welcome", and "team invite" into one loose poll, the test will get confusing prety fast.&lt;/p&gt;

&lt;p&gt;Reliable email tests are mostly about reducing ambiguity. Once the run has one inbox identity, one message contract, and one evidence trail, Playwright failures become much more fixable and much less annoying.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>FastAPI: aisla pruebas de email por entorno</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Wed, 22 Jul 2026 20:24:21 +0000</pubDate>
      <link>https://dev.to/silviutech/fastapi-aisla-pruebas-de-email-por-entorno-3e2l</link>
      <guid>https://dev.to/silviutech/fastapi-aisla-pruebas-de-email-por-entorno-3e2l</guid>
      <description>&lt;p&gt;Cuando un flujo de registro manda correos desde desarrollo, staging y producción usando casi la misma infraestructura, los fallos se vuelven muy confusos. El email llega, pero nadie sabe desde qué entorno salió, qué worker lo disparó ni si el enlace corresponde al escenario que estabas probando. En equipos backend esto pasa mas de lo que admitimos.&lt;/p&gt;

&lt;p&gt;En varios proyectos con FastAPI, lo que mejor me funcionó fue tratar cada entorno como un carril separado: su propia etiqueta, su propia bandeja temporal y su propia traza. Mucha gente termina buscando algo tipo crear correo temporal o generador de correos falsos porque nota el síntoma, pero el problema real casi siempre es falta de aislamiento.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué mezclar entornos rompe pruebas útiles
&lt;/h2&gt;

&lt;p&gt;Si una misma bandeja recibe mensajes de staging y de local, una verificación simple deja de ser confiable. Puedes ver el asunto correcto y aun asi estar leyendo el correo equivocado. Eso rompe pruebas manuales, scripts y hasta decisiones de producto.&lt;/p&gt;

&lt;p&gt;El daño no solo está en QA. También afecta a backend cuando revisa reintentos, deduplicación o expiración de tokens. Si el equipo quiere &lt;a href="https://dev.to/hannahdev56/mide-fallos-de-verificacion-sin-frenar-tu-saas-22o3"&gt;medir fallos de verificación&lt;/a&gt;, primero necesita datos limpios. Sin eso, el dashboard queda bonito pero dice poco.&lt;/p&gt;

&lt;p&gt;Por eso prefiero definir tres reglas sencillas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Un entorno, una bandeja o alias.&lt;/li&gt;
&lt;li&gt;Un request id visible desde la API hasta el correo entregado.&lt;/li&gt;
&lt;li&gt;Un nombre de remitente o prefijo que deje claro el origen.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No es glamoroso, pero funciona muy bien y evita bastantes discusiones raras.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patrón simple para aislar bandejas en FastAPI
&lt;/h2&gt;

&lt;p&gt;La idea es guardar contexto del entorno antes de encolar el envío. No hace falta un sistema enorme. Con FastAPI, una cola y una convención de nombres ya puedes empezar.&lt;/p&gt;

&lt;p&gt;Este patrón pequeño suele alcanzar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid4&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/signup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;signup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;env_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-env-name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;env_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;env_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify-signup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# envía el job al worker o broker que uses
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Luego, en el worker, uso &lt;code&gt;env_name&lt;/code&gt; para decidir una etiqueta visible y también para elegir la bandeja de prueba. En staging, por ejemplo, conviene que cada suite tenga su propio destino. Si el equipo necesita crear cuentas rápidas, una bandeja de &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;tempmailso&lt;/a&gt; puede servir como buzón aislado sin mezclar pruebas reales.&lt;/p&gt;

&lt;p&gt;Lo importante acá no es la herramienta. Es que el correo deje pistas claras: entorno, hora, tipo de evento y run id. Si una persona de producto abre el mensaje, debería entenderlo sin pedir ayuda al backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué conviene registrar en la API y en el worker
&lt;/h2&gt;

&lt;p&gt;Cuando una prueba falla, yo intento responder tres preguntas muy rapido:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;¿La API aceptó el evento correcto?&lt;/li&gt;
&lt;li&gt;¿El worker procesó ese evento una sola vez?&lt;/li&gt;
&lt;li&gt;¿El correo observado pertenece a ese intento y no a otro?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Para eso, estos campos ayudan bastante:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;run_id&lt;/code&gt; para unir logs y pruebas.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;env_name&lt;/code&gt; para separar staging, local y preview.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;template&lt;/code&gt; para saber qué copy debía salir.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;recipient_kind&lt;/code&gt; para distinguir usuario real, seed interno o bandeja temporal.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;queued_at&lt;/code&gt; y &lt;code&gt;sent_at&lt;/code&gt; para detectar atrasos o retries duplicados.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si ya estás haciendo &lt;a href="https://dev.to/alexcarteruk/como-validar-correos-de-rollback-en-kubernetes-p2b"&gt;pruebas de correo en entornos reales&lt;/a&gt;, este registro adicional te da la pieza que suele faltar: saber por qué llegó ese mensaje y no solo confirmar que llegó.&lt;/p&gt;

&lt;p&gt;También me gusta dejar el keyword &lt;code&gt;tempail&lt;/code&gt; en notas internas o fixtures si el equipo ya lo usa como referencia informal, pero no lo usaría como anchor ni como nombre oficial. Sirve para capturar búsquedas o costumbres viejas, nada más.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores pequeños que suelen colarse
&lt;/h2&gt;

&lt;p&gt;Hay varios fallos repetidos que veo en automatización de correos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reutilizar la misma bandeja para varias suites paralelas.&lt;/li&gt;
&lt;li&gt;Validar solo el asunto y no el enlace final.&lt;/li&gt;
&lt;li&gt;Reintentar jobs sin registrar si el primer envío ya salió.&lt;/li&gt;
&lt;li&gt;Probar con cuentas que comparten estado entre local y staging.&lt;/li&gt;
&lt;li&gt;Esconder el entorno en logs pero no en el propio email.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Otro error muy comun es poner toda la lógica de aislamiento en tests y nada en la app. Eso hace que la prueba sepa demasiado del sistema y el sistema muy poco de sí mismo. Prefiero que FastAPI y el worker publiquen contexto útil por defecto, aunque el test sea sencillo.&lt;/p&gt;

&lt;p&gt;Un detalle practico: si tienes varios templates, no metas todas las verificaciones en una sola bandeja y luego filtres por texto. Eso parece rapido al principio, pero se vuelve frágil en cuanto cambian copys o llegan reintentos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Necesito una bandeja distinta por cada entorno?
&lt;/h3&gt;

&lt;p&gt;Sí, si quieres depurar con calma. Incluso cuando el volumen es bajo, separar local, staging y preview te ahorra tiempo luego.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Esto aplica solo a FastAPI?
&lt;/h3&gt;

&lt;p&gt;No. El patrón vale para cualquier backend con colas o envíos asíncronos. FastAPI simplemente lo hace facil de integrar porque el request y el tipado quedan bastante claros.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué reviso primero si veo correos duplicados?
&lt;/h3&gt;

&lt;p&gt;Primero revisa el &lt;code&gt;run_id&lt;/code&gt; y el registro del worker. Si dos mensajes comparten destinatario pero no comparten el mismo origen lógico, casi seguro el problema está en reintentos o en falta de idempotencia.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Hace falta una gran plataforma de observabilidad?
&lt;/h3&gt;

&lt;p&gt;No necesariamente. Con buenos nombres, campos mínimos y una bandeja separada por entorno ya mejoras mucho. Luego, si el sistema crece, agregas métricas y alertas sin rehacer todo.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>backend</category>
      <category>automation</category>
    </item>
    <item>
      <title>React: reenvíos de verificación sin ansiedad</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Tue, 21 Jul 2026 17:24:37 +0000</pubDate>
      <link>https://dev.to/silviutech/react-reenvios-de-verificacion-sin-ansiedad-4km0</link>
      <guid>https://dev.to/silviutech/react-reenvios-de-verificacion-sin-ansiedad-4km0</guid>
      <description>&lt;p&gt;En muchos productos el botón de "reenviar verificación" parece un detalle menor, pero suele decidir si el usuario termina el alta o abandona. Cuando la interfaz no explica si el correo ya salió, cuánto falta para volver a intentarlo o por qué el botón sigue desactivado, la sensación es rara. No parece rota del todo, pero tampoco transmite confianza.&lt;/p&gt;

&lt;p&gt;En frontend me encuentro este problema bastante seguido. El backend responde bien, la cola manda el email, pero la UI deja al usuario adivinando. Algunos equipos incluso prueban con cuentas de tem email o con inboxes temporales para ver si "más o menos" funciona, y justo ahí aparecen los bordes raros: loaders que parpadean, temporizadores que cambian de ancho y mensajes que desaparecen demasiado pronto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué el botón de reenviar crea desconfianza
&lt;/h2&gt;

&lt;p&gt;El usuario no piensa en estados finitos ni en throttling. Piensa algo mucho más simple: "¿ya envié el correo o lo vuelvo a tocar?". Si la interfaz no responde eso rápido, empieza el martilleo del botón, llegan solicitudes duplicadas y soporte recibe tickets que eran evitables.&lt;/p&gt;

&lt;p&gt;También hay un tema claro de accesibilidad. Un temporizador visual sin anuncio comprensible deja fuera a usuarios con lector de pantalla, y un mensaje efímero puede perderse si el foco sigue en otro sitio. La guía de WAI sobre mensajes de estado explica por qué conviene anunciar cambios relevantes sin romper la navegación: &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html" rel="noopener noreferrer"&gt;https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Desde rendimiento percibido, el problema tampoco es pequeño. Nielsen Norman Group lleva años señalando que el feedback visible dentro de los primeros segundos reduce ansiedad y mejora la sensación de control: &lt;a href="https://www.nngroup.com/articles/response-times-3-important-limits/" rel="noopener noreferrer"&gt;https://www.nngroup.com/articles/response-times-3-important-limits/&lt;/a&gt;. No hace milagros, pero si baja bastante la fricción cuando el correo tarda un poco más de lo esperado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué estados necesita un reenvío usable
&lt;/h2&gt;

&lt;p&gt;Para mí el patrón mínimo tiene cuatro estados: &lt;code&gt;idle&lt;/code&gt;, &lt;code&gt;sending&lt;/code&gt;, &lt;code&gt;cooldown&lt;/code&gt; y &lt;code&gt;sent-with-help&lt;/code&gt;. El primero permite enviar. El segundo confirma que ya se disparó la acción. El tercero evita reintentos impulsivos y explica cuánto falta. El cuarto da una salida si el correo no aparece: revisar spam, corregir dirección o usar otra bandeja de prueba.&lt;/p&gt;

&lt;p&gt;Lo importante es que esos estados no cambien la estructura principal. El mismo botón debe seguir ocupando el mismo espacio. El mensaje de ayuda necesita una altura estable. Y el contador no debería empujar el layout cada segundo, por que ese tipo de micro salto se siente mas de lo que parece.&lt;/p&gt;

&lt;p&gt;Aquí también entra la parte pragmática: si tu equipo usa una bandeja temporal para QA, conviene separar el flujo de usuario real del flujo de prueba. A veces basta con documentar cuándo usar una cuenta normal y cuándo una herramienta como &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;temp mail so&lt;/a&gt;. No necesita ser protagonista del artículo ni del producto, solo un recurso contextual cuando el equipo valida entregas o tiempos de llegada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patrón simple en React
&lt;/h2&gt;

&lt;p&gt;Este ejemplo mantiene el ancho del CTA, anuncia cambios útiles y evita que el contador rehaga media interfaz a cada tick. No es el único enfoque, pero sale bastante bien en productos con onboarding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;COOLDOWN_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ResendVerification&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;secondsLeft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSecondsLeft&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timerRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cooldown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;timerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setSecondsLeft&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ya puedes reenviar el correo otra vez.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleResend&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enviando otro correo de verificación...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/email/resend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No pudimos reenviar el correo. Revisa la dirección e inténtalo de nuevo.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cooldown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setSecondsLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;COOLDOWN_SECONDS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Correo reenviado. Espera un momento antes de repetir la acción.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
        &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleResend&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cooldown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;220&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cooldown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`Reenviar en &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;secondsLeft&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s`&lt;/span&gt;
          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enviando...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Reenviar verificación&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hay tres cosas pequeñas que me gustan aquí. Una: el texto de estado vive en un contenedor estable. Dos: el botón mantiene ancho mínimo, así que el cambio entre "Enviando..." y el contador no provoca brincos feos. Tres: el usuario entiende qué pasa sin tener que deducirlo, que suena básico, pero no siempre pasa.&lt;/p&gt;

&lt;p&gt;Si quieres ir un poco más lejos, separa el countdown visual del anuncio accesible. Por ejemplo, puedes mantener &lt;code&gt;aria-live&lt;/code&gt; solo para eventos grandes y no para cada segundo del contador. Eso evita ruido en lector de pantalla, que a veces se vuelve medio insoportable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo medir si la experiencia mejora
&lt;/h2&gt;

&lt;p&gt;No me quedaría solo con "se siente mejor". Hay métricas concretas que ayudan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ratio de clicks repetidos sobre reenviar en menos de 10 segundos&lt;/li&gt;
&lt;li&gt;tiempo hasta completar verificación después del primer resend&lt;/li&gt;
&lt;li&gt;tickets de soporte ligados a "no me llegó el correo"&lt;/li&gt;
&lt;li&gt;abandono del onboarding tras ver el estado de cooldown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si el ratio de clicks repetidos baja, normalmente la UI está explicando mejor lo que ya hacía el sistema. Y si además reduces abandono, mejor todavía. Para equipos que conectan estas pruebas con operaciones o automatización, me parecen utiles estos ejemplos sobre &lt;a href="https://dev.to/alexcarteruk/checks-de-email-para-ventanas-de-mantenimiento-k8s-5ap8"&gt;checks de email en ventanas de mantenimiento reales&lt;/a&gt; y &lt;a href="https://dev.to/silviutech/llms-con-runbooks-de-email-que-si-escalan-5cje"&gt;runbooks de email que si escalan&lt;/a&gt;. Muestran bien cómo una interacción pequeña puede terminar afectando observabilidad y soporte.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas rápidas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿El cooldown no frustra más al usuario?
&lt;/h3&gt;

&lt;p&gt;Solo si aparece sin contexto. Si explicas que el correo ya salió y muestras cuánto falta, suele bajar la ansiedad en vez de subirla. Lo confuso no es esperar; lo confuso es esperar sin saber por qué.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Debo ocultar el botón por completo durante el cooldown?
&lt;/h3&gt;

&lt;p&gt;Yo no lo haría. Prefiero dejar el mismo botón desactivado con texto útil. Cuando desaparece del DOM, el flujo se siente inconsistente y aveces rompe foco o layout.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué pasa si el correo tarda mucho?
&lt;/h3&gt;

&lt;p&gt;Añade una vía de escape después de cierto tiempo: revisar spam, editar dirección o pedir ayuda. El peor caso no es la latencia; es la sensación de que la interfaz te dejó solo.&lt;/p&gt;

&lt;p&gt;Un buen flujo de reenvío en React no necesita más animación ni más complejidad. Necesita estados honestos, espacio estable y mensajes que bajen la duda en vez de amplificarla. Cuando eso está cuidado, la experiencia se nota más rapida, más clara y bastante más humana.&lt;/p&gt;

</description>
      <category>react</category>
      <category>performance</category>
      <category>a11y</category>
      <category>webdev</category>
    </item>
    <item>
      <title>React: validación sin saltos ni foco roto</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Tue, 21 Jul 2026 14:24:09 +0000</pubDate>
      <link>https://dev.to/silviutech/react-validacion-sin-saltos-ni-foco-roto-neg</link>
      <guid>https://dev.to/silviutech/react-validacion-sin-saltos-ni-foco-roto-neg</guid>
      <description>&lt;p&gt;Los flujos de verificación por correo suelen romperse por detalles de interfaz, no por la API. Cuando el usuario pega una direccion de correo falsa para probar, o compara un mejor correo desechable con su email real, espera una pantalla estable, foco claro y mensajes que no cambien de sitio cada dos segundos. Si eso falla, la sensación de calidad cae bastante rapido.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué el flujo falla aunque la API responda bien
&lt;/h2&gt;

&lt;p&gt;En equipos frontend veo el mismo patrón una y otra vez: el backend entrega estados correctos, pero la UI empuja el contenido hacia abajo, cambia el botón por un spinner enorme o borra el foco al llegar un error. Eso afecta accesibilidad y tambien rendimiento percibido.&lt;/p&gt;

&lt;p&gt;No es un detalle menor. Google considera bueno un &lt;code&gt;CLS&lt;/code&gt; de &lt;code&gt;0.1&lt;/code&gt; o menos, y explica por qué los saltos de layout dañan la experiencia en tareas sensibles como formularios y checkouts: &lt;a href="https://web.dev/cls/" rel="noopener noreferrer"&gt;https://web.dev/cls/&lt;/a&gt;. En verificación por correo el problema se nota mas, porque el usuario ya está esperando una respuesta externa.&lt;/p&gt;

&lt;p&gt;También hay un tema de consistencia. Si el formulario muestra "Revisa tu inbox" y luego reemplaza todo por un bloque distinto, lectores de pantalla y usuarios de teclado pierden contexto. A veces no es un bug "grave", pero si es cansado, y termina subiendo abandono.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tres decisiones de UI que estabilizan el formulario
&lt;/h2&gt;

&lt;p&gt;La primera es reservar espacio para mensajes de estado. No esperes a que aparezca el error para crear el contenedor. Un bloque con &lt;code&gt;min-height&lt;/code&gt; evita que el botón y el campo salten. Es simple, medio aburrido, pero funciona de verdad.&lt;/p&gt;

&lt;p&gt;La segunda es mantener el foco donde aporta valor. Si la petición falla, devuelve el foco al campo o al resumen de error. Si la petición sale bien, muévelo a la confirmación solo cuando esa confirmación sea una región clara con &lt;code&gt;aria-live&lt;/code&gt;. Hacer foco "porque sí" suele empeorar las cosas.&lt;/p&gt;

&lt;p&gt;La tercera es no mezclar carga con desaparición. Puedes desactivar el botón y cambiar su etiqueta a "Enviando..." sin desmontarlo. Cuando reemplazas un botón entero por otro nodo, es facil perder estilos, foco y métricas. He visto este problema muchisimas veces en formularios React que parecían ya terminados.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un ejemplo simple en React
&lt;/h2&gt;

&lt;p&gt;Este patrón me gusta porque mantiene la estructura estable y hace visibles los estados para teclado y lector de pantalla, aunque sea un ejemplo chico:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;VerifyEmailForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statusId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enviando enlace de verificación...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/verify-email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No pudimos enviar el correo. Revisa el campo e inténtalo otra vez.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;inputRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Listo. Revisa tu bandeja de entrada para continuar.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Correo&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;inputRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-describedby&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;statusId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;statusId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enviando...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enviar verificación&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No resuelve todo, claro. Pero deja dos ventajas utiles: el layout no brinca, y el usuario entiende qué pasó sin pelear con la interfaz. Si además mides CLS en laboratorio y en campo, verás cambios pequeños pero bastante consistentes.&lt;/p&gt;

&lt;p&gt;Cuando pruebo esto con cuentas de test, incluso usando cadenas raras como temp org mail dentro de casos internos, prefiero observar dos cosas: si el mensaje aparece en el mismo lugar y si el foco vuelve adonde debe. Parece obvio, pero muchas regresiones entran por ahí.&lt;/p&gt;

&lt;p&gt;Si tu equipo además necesita coordinar pruebas de correo entre producto y operaciones, me gustó cómo otros posts explican &lt;a href="https://dev.to/alexcarteruk/como-probar-correos-de-mantenimiento-en-kubernetes-4gif"&gt;probar correos de mantenimiento en equipos reales&lt;/a&gt; y &lt;a href="https://dev.to/hannahdev56/mide-fallos-de-verificacion-sin-frenar-tu-saas-22o3"&gt;medir fallos de verificacion sin friccion&lt;/a&gt;. Son buenos ejemplos de cómo conectar la UI con el resto del sistema, no solo con el componente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist rapido antes de publicar
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Reserva espacio fijo para mensajes de estado.&lt;/li&gt;
&lt;li&gt;Mantén el mismo botón durante la carga.&lt;/li&gt;
&lt;li&gt;Usa &lt;code&gt;aria-live="polite"&lt;/code&gt; para confirmaciones cortas.&lt;/li&gt;
&lt;li&gt;Devuelve el foco al campo cuando el error bloquea la tarea.&lt;/li&gt;
&lt;li&gt;Comprueba el flujo con teclado completo, no solo con mouse.&lt;/li&gt;
&lt;li&gt;Revisa que el primer párrafo del estado sea corto y entendible, por que luego DEVs y QA lo leen muy rápido.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Preguntas que salen siempre
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Debo mover el foco al mensaje de éxito?
&lt;/h3&gt;

&lt;p&gt;Solo si el siguiente paso depende de leerlo enseguida. En muchos formularios basta con anunciar el cambio por &lt;code&gt;aria-live&lt;/code&gt; y dejar el foco quieto. Menos movimiento suele ser mejor.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Y si el spinner cambia el ancho del botón?
&lt;/h3&gt;

&lt;p&gt;Define un ancho mínimo o usa una etiqueta parecida en longitud. Otra opción es renderizar el spinner dentro del botón sin quitar el texto. No es elegante siempre, pero evita brincos feos.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Vale la pena medir esto si el backend ya va bien?
&lt;/h3&gt;

&lt;p&gt;Sí. Si la interfaz pierde foco o desplaza el contenido, la tarea se siente lenta aunque la red responda rapido. En frontend, esa percepción pesa un monton.&lt;/p&gt;

&lt;p&gt;Pequeños detalles de React, CSS y Accesibilidad suelen decidir si una verificación se siente confiable o improvisada. No hace falta una arquitectura enorme; hace falta una interfaz estable, clara y un poco menos ansiosa.&lt;/p&gt;

</description>
      <category>react</category>
      <category>css</category>
      <category>a11y</category>
      <category>performance</category>
    </item>
    <item>
      <title>React: evita saltos en flujos de verificación</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Tue, 21 Jul 2026 08:24:21 +0000</pubDate>
      <link>https://dev.to/silviutech/react-evita-saltos-en-flujos-de-verificacion-473d</link>
      <guid>https://dev.to/silviutech/react-evita-saltos-en-flujos-de-verificacion-473d</guid>
      <description>&lt;p&gt;Cuando una pantalla de verificación de email se siente torpe, casi siempre pensamos en el backend primero. Pero muchas veces el problema real está en el frontend: el mensaje aparece tarde, el botón salta de sitio y el foco queda raro justo cuando la persona necesita claridad. En equipos de producto esto se nota enseguida, porque una UI que cambia demasiado durante la espera da una sensación medio rota aunque la API responda bien.&lt;/p&gt;

&lt;p&gt;En flujos de registro o login, ese detalle importa mas de lo que parece. &lt;a href="https://web.dev/articles/cls" rel="noopener noreferrer"&gt;Web.dev explica que el Cumulative Layout Shift&lt;/a&gt; mide cambios inesperados en el layout que afectan percepción y usabilidad, y eso pega de lleno en formularios, banners de error y mensajes de confirmación. Si encima tu QA usa una direccion de correo desechable o un correo de usar y tirar para repetir pruebas rápido, cualquier salto visual hace mas dificil distinguir si falló la entrega o falló la interfaz.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por que el layout shift rompe mas de lo que parece
&lt;/h2&gt;

&lt;p&gt;He visto este patrón varias veces: el usuario envía el formulario, React muestra un spinner minimo, luego entra un mensaje de éxito de dos líneas, y por ultimo aparece un CTA adicional. Todo funciona, pero el bloque cambia de altura tres veces. En desktop molesta; en móvil se siente peor, porque el dedo ya iba hacia un lugar que dejó de existir.&lt;/p&gt;

&lt;p&gt;Ese tipo de salto no solo se ve feo. También complica accesibilidad. Si el contenido principal cambia de posición mientras un lector de pantalla anuncia una región viva, la experiencia queda un poco confusa. No es un bug enorme ni glamuroso, pero sí una fuente muy real de friccion.&lt;/p&gt;

&lt;p&gt;Una pista util: si soporte recibe capturas de una pantalla "que parpadea" o QA dice que el estado "se mueve solo", yo reviso layout antes que lógica. A veces nos vamos directo a depurar fetches, cuando el problema era muchisimo más terrenal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que medir antes de tocar el CSS
&lt;/h2&gt;

&lt;p&gt;Antes de cambiar componentes, intento observar tres cosas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cuánto cambia la altura del contenedor entre &lt;code&gt;idle&lt;/code&gt;, &lt;code&gt;loading&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt; y &lt;code&gt;error&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Si el foco termina en un encabezado o mensaje estable.&lt;/li&gt;
&lt;li&gt;Si el CTA principal conserva posición aproximada entre estados.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No hace falta montar una auditoría gigante. Un perfil básico con Lighthouse ya deja ver si la pantalla castiga CLS, y el propio equipo de Chrome recomienda reservar espacio para contenido dinámico cuando sabes que va a llegar después. En auth flows eso aplica a mensajes, ayudas inline y bloques de acciones secundarias.&lt;/p&gt;

&lt;p&gt;También conviene separar el problema de entrega del problema visual. Si tu equipo ya tiene un proceso para &lt;a href="https://dev.to/hannahdev56/como-revisar-emails-de-reactivacion-trial-7d"&gt;revisar emails de reactivacion trial sin mezclar señales&lt;/a&gt;, úsalo como capa de validación externa. Así sabes si el correo llegó bien antes de discutir si React se desordenó durante la espera.&lt;/p&gt;

&lt;p&gt;Y aquí aparece algo muy cotidiano: en notas internas alguien pone tempail o fake e mail com porque va deprisa. No pasa nada. Lo que sí conviene evitar es que el procedimiento de prueba dependa de nombres improvisados y no de estados verificables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patron simple de React para reservar espacio
&lt;/h2&gt;

&lt;p&gt;La idea mas útil que me ha funcionado es aburrida, y justo por eso sirve: reservar espacio desde el inicio. En vez de dejar que cada estado crezca libremente, defino un bloque con altura mínima razonable, encabezado persistente y zona de acciones estable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;VerifyState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;VerifyState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;idle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revisa tu email para continuar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Validando enlace...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email verificado, ya puedes seguir&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No pudimos validar el enlace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;VerifyPanel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VerifyState&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"verify-panel"&lt;/span&gt; &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;tabIndex&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Verificación de cuenta&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"verify-copy"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"verify-actions"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Volver al acceso&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.verify-panel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;min-block-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;align-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.verify-copy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;min-block-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.verify-actions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;min-block-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No es una receta magica, pero reduce sorpresas. El mensaje puede cambiar, el layout no tanto. Esto además facilita testing visual, porque la diff entre estados deja de ser un terremoto y pasa a ser un cambio legible.&lt;/p&gt;

&lt;p&gt;Si el flujo se integra con automatizaciones o pruebas repetidas, tener entradas y salidas predecibles también ayuda a diseñar mejores &lt;a href="https://dev.to/silviutech/contratos-de-inbox-para-agentes-llm-26i3"&gt;contratos de inbox para automatizaciones mas estables&lt;/a&gt;. Aunque ese artículo va por otra capa del sistema, comparte una idea que me gusta mucho: menos ambigüedad en cada tramo del flujo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Como probar el flujo completo con bandejas aisladas
&lt;/h2&gt;

&lt;p&gt;Cuando QA revisa verificación de email en frontend, yo no me quedo solo con "llegó el correo". Prefiero este mini checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enviar el formulario desde una viewport móvil y otra desktop.&lt;/li&gt;
&lt;li&gt;Abrir el enlace de verificación y comprobar que el título principal no salta de sitio.&lt;/li&gt;
&lt;li&gt;Revisar con teclado que el foco sigue una ruta entendible.&lt;/li&gt;
&lt;li&gt;Comparar estados &lt;code&gt;loading&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt; y &lt;code&gt;error&lt;/code&gt; con capturas o snapshots.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si la prueba usa una bandeja temporal para repetir escenarios, mejor todavía. Ahí es donde una direccion de correo desechable ahorra tiempo y hace mas simple aislar señales. Pero el objetivo no es el inbox en sí; el objetivo es ver si la transición completa se siente estable, clara y rapida.&lt;/p&gt;

&lt;p&gt;Otra cosa que conviene mirar: errores largos. Muchos equipos escriben un mensaje corto para éxito y un párrafo entero para error. Resultado: el estado que peor se siente también es el que más mueve la interfaz. Yo intento mantener una caja estable y poner detalles extendidos detrás de un enlace o bloque secundario. Es una decisión simple, pero suele arreglar bastante UX con poco codigo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Hace falta medir CLS en una pantalla tan pequeña?
&lt;/h3&gt;

&lt;p&gt;Sí. Justo en estos flujos pequeños es donde un salto de layout cambia la percepción del producto completo. Parece un detalle menor, pero no lo es.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Reservar espacio no deja huecos feos?
&lt;/h3&gt;

&lt;p&gt;Un poco, a veces. Pero prefiero un layout quieto a una interfaz nerviosa. Con buen ritmo visual y copy corto, el compromiso suele valer la pena.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Esto mejora también accesibilidad?
&lt;/h3&gt;

&lt;p&gt;Sí, porque combina estabilidad visual con mensajes más previsibles y rutas de foco mas limpias. No resuelve todo, pero deja una base bastante sana para seguir iterando.&lt;/p&gt;

</description>
      <category>react</category>
      <category>css</category>
      <category>a11y</category>
      <category>performance</category>
    </item>
    <item>
      <title>Cypress Email Retries Need Inbox Leases</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Mon, 20 Jul 2026 23:23:57 +0000</pubDate>
      <link>https://dev.to/silviutech/cypress-email-retries-need-inbox-leases-24hb</link>
      <guid>https://dev.to/silviutech/cypress-email-retries-need-inbox-leases-24hb</guid>
      <description>&lt;p&gt;I have seen plenty of end-to-end suites look stable in the dashboard while the email step was quietly cheating. A retry fires, the test passes on the second attempt, and everyone moves on. Later, a real bug slips through because the passing run actually read a message from the first attempt. That kind of flake is annoyng because it feels random until you inspect the timeline closely.&lt;/p&gt;

&lt;p&gt;For QA work, I now treat mailbox ownership as part of the test contract. If a Cypress spec can retry, parallelize, or resume after a failed setup step, it needs an inbox lease tied to that attempt. That does not mean building a huge system. It means one test run claims one inbox for a short window, tags every message with the same trace value, and cleans up when the assertion is done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why retries create false confidence
&lt;/h2&gt;

&lt;p&gt;Email assertions fail in a few predictable ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a second attempt reads the first attempt's message&lt;/li&gt;
&lt;li&gt;two workers poll the same mailbox during parallel Automation runs&lt;/li&gt;
&lt;li&gt;the app sends a valid email, but later than the test expected&lt;/li&gt;
&lt;li&gt;cleanup happens too slowly, so yesterday's mail still looks fresh enough&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where a use and throw email setup helps, but only if it is wrapped in rules. The disposable mailbox is not the strategy by itself. The strategy is ownership, expiration, and traceability. That is also why I like articles about &lt;a href="https://dev.to/jasonmills94/docker-checks-for-aws-config-drift-emails-156e"&gt;traceable email verification&lt;/a&gt;: they focus on proving which run produced which message, not just "something arrived."&lt;/p&gt;

&lt;p&gt;When teams skip that piece, they start saying weird stuff like "the retry probably picked the right email, looks fine." It might be fine. It might also be silently reading the wrong artifact and teaching the suite to lie.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an inbox lease actually solves
&lt;/h2&gt;

&lt;p&gt;An inbox lease is just a short claim:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;lease_id&lt;/code&gt; maps to the test attempt&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;owner&lt;/code&gt; maps to the spec name or worker&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expires_at&lt;/code&gt; prevents stale reuse&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;trace_id&lt;/code&gt; appears in the app payload and assertion logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That tiny bit of metadata does a lot. If a retry starts, it requests a new lease instead of inheriting the old mailbox. If the app is slow, the waiting logic can say "this message belongs to lease B, ignore it for lease C." If a test crashes mid-run, the cleanup job can sweep expired leases later. It is not fancy, but it is very, very practical.&lt;/p&gt;

&lt;p&gt;For teams that need a quick mailbox source, I have used a &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;disposable email address&lt;/a&gt; provider in test environments, but the helpful part was always the lease wrapper around it. The provider gives you the inbox. Your test harness decides who owns it and for how long.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Cypress workflow that survives reruns
&lt;/h2&gt;

&lt;p&gt;The workflow below is small enough to keep in a real repo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Before the spec starts, request an inbox lease from a helper service.&lt;/li&gt;
&lt;li&gt;Store &lt;code&gt;leaseId&lt;/code&gt;, &lt;code&gt;inboxAddress&lt;/code&gt;, and &lt;code&gt;traceId&lt;/code&gt; in Cypress env.&lt;/li&gt;
&lt;li&gt;Pass &lt;code&gt;traceId&lt;/code&gt; through the signup or reset flow.&lt;/li&gt;
&lt;li&gt;Poll only messages attached to the active lease.&lt;/li&gt;
&lt;li&gt;Release the lease in &lt;code&gt;afterEach&lt;/code&gt;, even if the assertion fails.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the rough shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;leaseInbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;lease&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;leaseId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inboxAddress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;traceId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verifies the reset email for this attempt only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/test/reset&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inboxAddress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;traceId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waitForLeaseMessage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;leaseId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;leaseId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;traceId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Reset your password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;traceId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;releaseInbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;leaseId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;leaseId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is that &lt;code&gt;waitForLeaseMessage&lt;/code&gt; should filter on both lease and trace ID. If you only match on recipient address, reruns can still collide. That bug is more common than teams admit, and it makes the suite feel haunted for no good reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the temp inbox fits without taking over the article
&lt;/h2&gt;

&lt;p&gt;I do not think every QA guide needs to become a pitch for inbox tools. Most of the value comes from test design. Still, if you need a short-lived mailbox pool for CI, a &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;disposable email account&lt;/a&gt; can be useful as plumbing behind the lease service. The important thing is keeping the mailbox lifecycle small and boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create one inbox per attempt&lt;/li&gt;
&lt;li&gt;tag the app request with a trace value&lt;/li&gt;
&lt;li&gt;reject messages outside the lease window&lt;/li&gt;
&lt;li&gt;archive logs with lease IDs when a test fails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That pattern also lines up with general flake-reduction guidance. Cypress itself recommends controlling external side effects and avoiding shared mutable state in end-to-end tests because nondeterminism is where false passes and false failures begin (&lt;a href="https://docs.cypress.io/app/core-concepts/best-practices" rel="noopener noreferrer"&gt;Cypress best practices&lt;/a&gt;). Different stack, same lesson basicly.&lt;/p&gt;

&lt;p&gt;One extra note: if somebody still writes "send it to the dummy e mail inbox" in the runbook, update the runbook. Name the mailbox source, state the retention window, and document who clears expired leases. Tiny docs fixes save suprising amounts of debug time.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for less flaky email tests
&lt;/h2&gt;

&lt;p&gt;Before I trust a Cypress email check, I want these boxes ticked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each retry gets a fresh lease, not a reused address&lt;/li&gt;
&lt;li&gt;the app payload includes a trace ID or attempt ID&lt;/li&gt;
&lt;li&gt;polling filters by lease and trace, not just by subject&lt;/li&gt;
&lt;li&gt;expired inboxes are cleaned automatically&lt;/li&gt;
&lt;li&gt;failure logs show the lease ID, worker, and timestamps&lt;/li&gt;
&lt;li&gt;parallel jobs never share the same mailbox pool record&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only change one thing this week, make it the lease boundary. That one fix removes a lot of spooky behavior from test reruns, and it gives QA engineers a cleaner story when a failure is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Do I need a lease system for every product email test?
&lt;/h2&gt;

&lt;p&gt;Not always. If the flow is single-threaded and never retries, a lighter setup may be enough. But once retries or parallel workers enter the picture, leases pay for themself pretty fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just delete all messages before each test?
&lt;/h2&gt;

&lt;p&gt;Because deletion is a weak ownership model. Slow delivery, duplicate sends, and parallel specs can still race each other. A fresh lease is more explicit and easier to audit later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What usually breaks first?
&lt;/h2&gt;

&lt;p&gt;In my experiance, teams either forget to pass a trace ID through the app flow or they reuse the same mailbox between attempts. Both issues make failures harder to trust, even when the test looks green.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>cypress</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>Contratos de inbox para agentes LLM</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Mon, 20 Jul 2026 17:24:12 +0000</pubDate>
      <link>https://dev.to/silviutech/contratos-de-inbox-para-agentes-llm-26i3</link>
      <guid>https://dev.to/silviutech/contratos-de-inbox-para-agentes-llm-26i3</guid>
      <description>&lt;p&gt;Cuando un agente LLM dispara correo, el bug molesto no suele estar en el texto final. Suele estar en la evidencia: una inbox compartida, un assert flojo o una corrida que pisa a otra. En varios equipos he visto la misma escena, mas o menos: el agente "parece" correcto, pero nadie puede probar qué acción produjo qué mensaje. Sin ese borde claro, la automatización se vuelve cara de operar.&lt;/p&gt;

&lt;p&gt;Mi forma de pensarlo es bastante simple. El sistema no termina en el prompt ni en la tool call. Termina cuando puedes mirar una corrida concreta, abrir su evidencia y decidir en pocos minutos si el flujo está sano o no. Ahí es donde un contrato de inbox bien definido hace diferencia real para LLMs y Automatización.&lt;/p&gt;

&lt;h2&gt;
  
  
  El contrato correcto empieza en la inbox
&lt;/h2&gt;

&lt;p&gt;Si la herramienta de correo solo recibe &lt;code&gt;to&lt;/code&gt;, &lt;code&gt;subject&lt;/code&gt; y &lt;code&gt;body&lt;/code&gt;, el agente queda con demasiada libertad y el equipo con muy poca trazabilidad. Prefiero modelar una inbox de prueba como una pieza de arquitectura, no como un detalle de QA.&lt;/p&gt;

&lt;p&gt;Ese contrato mínimo debería incluir:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;run_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scenario_key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;recipient_alias&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;expected_template&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;assertion_window&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;evidence_retention&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eso evita una clase entera de fallos raros. Dos corridas pueden usar el mismo backend, incluso la misma feature flag, pero nunca deberían competir por la misma bandeja lógica. Si ya estás trabajando en &lt;a href="https://dev.to/silviutech/versiona-acciones-de-correo-en-agentes-llm-63d"&gt;versionar acciones de correo en agentes&lt;/a&gt;, este paso completa la frontera: acción cerrada por un lado, evidencia aislada por el otro.&lt;/p&gt;

&lt;p&gt;También ordena el vocabulario del equipo. Mucha gente mezcla términos como inbox temporal, correo de usar y tirar o incluso cosas medio rotas que quedan en tickets, como tepm mail com. No es grave por sí mismo, pero muestra que el flujo aún depende más del contexto humano que de un contrato reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué campos hacen la evidencia reutilizable
&lt;/h2&gt;

&lt;p&gt;Para mí, una prueba de correo útil necesita responder cuatro preguntas sin leer veinte logs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;¿Qué acción pidió el agente?&lt;/li&gt;
&lt;li&gt;¿Qué inbox quedó reservada para esa corrida?&lt;/li&gt;
&lt;li&gt;¿Qué mensaje terminó llegando?&lt;/li&gt;
&lt;li&gt;¿Qué aserción de negocio se validó al final?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Un payload pequeño puede cubrirlo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eval_2026_07_21_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scenario_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup_magic_link"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient_alias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup-eval-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_template"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"magic_link_v3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"assertion_window"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"120s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evidence_retention"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"24h"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con algo así, el ejecutor deja de "interpretar" y pasa a validar. Además, puedes separar mejor qué vive en la capa del agente y qué vive en la capa de la tool. El agente decide el caso; la tool garantiza aislamiento, consulta y retención. Ese reparto suele bajar bastante el tiempo de debugging, y va en la misma línea que reportes de entrega como DORA, que asocian feedback claro y ciclos cortos con mejor desempeño operativo &lt;a href="https://services.google.com/fh/files/misc/dora-accelerate-state-of-devops-2024.pdf" rel="noopener noreferrer"&gt;source&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un flujo simple para equipos pequeños
&lt;/h2&gt;

&lt;p&gt;No hace falta montar una plataforma gigante. Para un equipo chico, yo empezaría así:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;La corrida crea un &lt;code&gt;run_id&lt;/code&gt; y reserva una inbox lógica única.&lt;/li&gt;
&lt;li&gt;El agente elige una acción cerrada y una ventana de espera explícita.&lt;/li&gt;
&lt;li&gt;El ejecutor persiste request, metadata y resultado en el mismo sobre de evidencia.&lt;/li&gt;
&lt;li&gt;La prueba consulta solo esa inbox y valida contenido, destinatario y efecto final.&lt;/li&gt;
&lt;li&gt;La limpieza expira la evidencia sin tocar corridas activas.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ese patrón también viene bien cuando necesitas &lt;a href="https://dev.to/silviutech/fastapi-emails-aislados-por-branch-3i51-temp-slug-9747599?preview=15d0f24ab23156ff7add433341db5b88816d63c95c3ecff5b0582256604f6e1410fd3181238aaa64757da0be78c0c8b8b797801df629e31b80e4e55a"&gt;aislar emails por branch en FastAPI&lt;/a&gt;. Branches, retries y escenarios manuales tienden a contaminarse entre sí si comparten naming o retention policy.&lt;/p&gt;

&lt;p&gt;Cuando necesito un enlace contextual para explicar el tipo de proveedor que encaja aquí, suelo hablar de servicios tipo &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;temp mail com&lt;/a&gt;. No por el marketing, sino porque la idea de fondo importa: inbox efímera, consulta determinista y eliminación predecible. Si el proveedor no te da eso, la herramienta puede verse comoda al inicio, pero luego castiga cada suite paralela.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs y checkpoints antes de publicarlo
&lt;/h2&gt;

&lt;p&gt;Este diseño mete más estructura, sí. Pero casi siempre paga solo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Más ids y metadata, pero menos ambigüedad.&lt;/li&gt;
&lt;li&gt;Un setup un poco más estricto, pero corridas mucho mas auditables.&lt;/li&gt;
&lt;li&gt;Menos magia en demos, pero mejor operación diaria.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mis checkpoints antes de darlo por bueno son estos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cada corrida reserva una inbox única o un alias exclusivo.&lt;/li&gt;
&lt;li&gt;La tool rechaza lecturas sin &lt;code&gt;run_id&lt;/code&gt; o &lt;code&gt;scenario_key&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;La evidencia tiene TTL y dueño claro.&lt;/li&gt;
&lt;li&gt;La aserción final valida negocio, no solo "llegó un correo".&lt;/li&gt;
&lt;li&gt;El historial permite reconstruir fallos sin reejecutar todo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si uno de esos puntos falta, el sistema aún depende demasiado de memoria humana. Y eso no escala, especialmente cuando varios agentes o pipelines corren a la vez.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Cuándo conviene una inbox por escenario y no por usuario?
&lt;/h3&gt;

&lt;p&gt;Cuando estás probando concurrencia, retries o ramas paralelas. Por usuario suele ser demasiado amplio y genera cruces dificiles de explicar después.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Hace falta guardar el prompt completo?
&lt;/h3&gt;

&lt;p&gt;No siempre. Normalmente basta con la acción elegida, los ids de contexto y el resultado normalizado de la tool. Guardar más ruido no necesariamente ayuda.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Esto aplica fuera de LLMs?
&lt;/h3&gt;

&lt;p&gt;Sí. La diferencia es que con agentes el beneficio aparece antes, porque la frontera entre decisión y ejecución necesita ser aburrida, estable y muy legible.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>automation</category>
      <category>devtools</category>
    </item>
    <item>
      <title>FastAPI: evita correos duplicados al reintentar</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sat, 18 Jul 2026 23:24:17 +0000</pubDate>
      <link>https://dev.to/silviutech/fastapi-evita-correos-duplicados-al-reintentar-49dp</link>
      <guid>https://dev.to/silviutech/fastapi-evita-correos-duplicados-al-reintentar-49dp</guid>
      <description>&lt;p&gt;Cuando una API de registro o verificación falla a medias, el equipo suele reintentar rapido. El problema es que ese segundo intento muchas veces manda un correo extra aunque el primero ya estaba en cola. En FastAPI esto pasa bastante cuando el endpoint responde antes de que el worker termine y nadie define una clave idempotente para el envio.&lt;/p&gt;

&lt;p&gt;En proyectos pequeños no hace falta montar una arquitectura enorme para corregirlo. Lo que sí conviene hacer es separar dos decisiones: "la app debe enviar este correo" y "el proveedor ya recibió este correo". Esa distinción evita varios sustos, sobre todo cuando soporte empieza a ver usuarios con dos links de verificación distintos y no sabe cual abrió primero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por que los reintentos duplican correos
&lt;/h2&gt;

&lt;p&gt;El patrón que veo más seguido es este:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El cliente llama &lt;code&gt;POST /verify-email&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;La API crea un token y encola el envio.&lt;/li&gt;
&lt;li&gt;La conexión se corta o el worker tarda más de lo esperado.&lt;/li&gt;
&lt;li&gt;El cliente reintenta.&lt;/li&gt;
&lt;li&gt;El backend vuelve a crear otro envio porque no sabe que el primero ya existía.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Eso genera ruido para producto, para soporte y para métricas. También rompe pruebas de staging, porque una bandeja compartida ya no deja claro qué mensaje corresponde al escenario actual. En temas de trazabilidad, me gusta bastante esta idea de &lt;a href="https://dev.to/alexcarteruk/como-probar-correos-de-mantenimiento-en-kubernetes-4gif"&gt;automatizacion de correos entre servicios&lt;/a&gt;: si el flujo cruza varios componentes, hay que dejar un identificador estable y visible entre ellos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patron simple para volver idempotente el envio
&lt;/h2&gt;

&lt;p&gt;La versión más simple que me ha funcionado usa una tabla &lt;code&gt;email_jobs&lt;/code&gt; con una clave única por intención de negocio. Por ejemplo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;user_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;template&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;intent_key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;provider_message_id&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;intent_key&lt;/code&gt; puede ser algo como &lt;code&gt;verify-email:&amp;lt;user_id&amp;gt;:&amp;lt;version&amp;gt;&lt;/code&gt;. Si entra el mismo request otra vez, la API no crea un nuevo job. Solo devuelve el existente o confirma que el envío ya está programado. Ese pequeño detalle suele bajar muchisimo los duplicados.&lt;/p&gt;

&lt;p&gt;Si además quieres una bandeja temporal para validar el flujo completo de extremo a extremo, un &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;correo burner&lt;/a&gt; puede servir para aislar pruebas manuales o smoke tests. No lo volvería el centro del sistema, pero ayuda cuando necesitas comprobar que solo salió un mensaje real y no tres variaciones casi iguales.&lt;/p&gt;

&lt;p&gt;Lo importante es que la idempotencia viva en tu backend, no en la paciencia del equipo. Si dependes de "ojalá el cliente no reintente" o "ojalá el proveedor ignore duplicados", tarde o temprano pagas ese atajo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ejemplo pequeno con FastAPI y PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Una implementación minima puede verse así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_intent_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify-email:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/verify-email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_verification_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;intent_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_intent_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_email_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intent_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;intent_key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_email_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify-email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;intent_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;intent_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_conflict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;enqueue_email_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intent_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;intent_key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No es un sistema completo, claro, pero muestra la idea central: primero registras la intención, luego encolas el trabajo. Si el request vuelve, respondes usando el mismo &lt;code&gt;intent_key&lt;/code&gt;. En PostgreSQL esto suele quedar bien con un índice único y un &lt;code&gt;INSERT ... ON CONFLICT DO NOTHING&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Donde varios equipos se tropiezan es en regenerar el token cada vez que reintentan. Ahí el correo A y el correo B ya no son equivalentes, así que el usuario recibe dos links validos o uno válido y otro vencido. Ese escenario es molesto de depurar y bastante feo para la experiencia final, la verdad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Como probarlo sin contaminar bandejas
&lt;/h2&gt;

&lt;p&gt;Para verificar que la solución funciona, intentaría algo muy concreto:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Crear un usuario de prueba nuevo.&lt;/li&gt;
&lt;li&gt;Llamar dos veces al endpoint con el mismo &lt;code&gt;intent_key&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Confirmar que solo existe un &lt;code&gt;email_job&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Revisar que la bandeja recibe un solo mensaje.&lt;/li&gt;
&lt;li&gt;Abrir el link y validar el estado final del usuario.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si ya vienes probando flujos parecidos, te puede servir este artículo sobre &lt;a href="https://dev.to/silviutech/como-aislar-emails-de-agentes-llm-en-flujos-automatizados-sin-perder-trazabilidad-26ac"&gt;probar emails transaccionales en FastAPI&lt;/a&gt;. La lección que más rescato es no mezclar trazabilidad con intuición: si API, worker y bandeja no comparten una clave, cualquier debug termina siendo medio artesanal.&lt;/p&gt;

&lt;p&gt;También pondría atención a las notas improvisadas del equipo. Cuando en Slack o en un ticket aparece algo como tepm mail com, normalmente no es solo un typo gracioso. Suele ser la señal de que el procedimiento manual todavía está difuso y cualquiera puede terminar revisando una bandeja incorrecta o un run viejo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Debo resolverlo con locks distribuidos?
&lt;/h3&gt;

&lt;p&gt;No siempre. Para muchos productos basta una clave única de negocio y una cola razonable. Los locks ayudan cuando hay mucha concurrencia real, pero meterlos demasiado pronto complica el backend sin necesidad.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿&lt;code&gt;BackgroundTasks&lt;/code&gt; alcanza?
&lt;/h3&gt;

&lt;p&gt;Para volumen bajo, sí. Si necesitas reintentos, observabilidad y control más fino, prefiero un worker dedicado con una tabla de jobs. Es menos elegante en teoria, pero en producción suele ser más facil de operar.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué reviso primero si siguen saliendo duplicados?
&lt;/h3&gt;

&lt;p&gt;Primero miraría si el &lt;code&gt;intent_key&lt;/code&gt; cambia entre intentos. Después revisaría si el worker vuelve a crear el envio en vez de consumir el job ya registrado. Es un fallo pequeño, pero aparece más veces de lo que deberia.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>backend</category>
      <category>automation</category>
    </item>
    <item>
      <title>React: loaders honestos para flujos lentos</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sat, 18 Jul 2026 17:25:10 +0000</pubDate>
      <link>https://dev.to/silviutech/react-loaders-honestos-para-flujos-lentos-ggk</link>
      <guid>https://dev.to/silviutech/react-loaders-honestos-para-flujos-lentos-ggk</guid>
      <description>&lt;p&gt;En frontend solemos dedicar mucho tiempo a la vista final y bastante menos al tiempo de espera. Es curioso, porque esa espera aparece en momentos delicados: enviar un formulario, activar una cuenta, validar permisos o esperar un email de confirmación. Si el loader se siente falso, la interfaz pierde credibilidad muy rapido.&lt;/p&gt;

&lt;p&gt;Lo he visto sobre todo en productos donde el flujo depende de varios sistemas. El usuario pulsa un botón, aparece un spinner perfecto, pero nadie explica si faltan dos segundos o treinta. En pruebas internas, incluso cuando alguien usa un &lt;code&gt;generador de correo desechable&lt;/code&gt; para validar onboarding o anota algo como &lt;code&gt;temp gamil com&lt;/code&gt;, el problema real no es la latencia sola. El problema es no saber si la app sigue trabajando o si ya se quedo colgada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué un loader puede romper la confianza
&lt;/h2&gt;

&lt;p&gt;Muchos loaders fallan por una razon simple: muestran actividad, pero no contexto. Un spinner girando no dice si la petición llegó, si falta una confirmación externa o si hay que revisar otra pantalla.&lt;/p&gt;

&lt;p&gt;Cuando eso pasa, suelen aparecer tres conductas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;la persona hace clic otra vez&lt;/li&gt;
&lt;li&gt;abre otra pestaña por inseguridad&lt;/li&gt;
&lt;li&gt;abandona porque asume que algo salio mal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ese comportamiento no es raro. Google lleva años recomendando estados de progreso claros para reducir incertidumbre y mejorar la percepción de rapidez (&lt;a href="https://m3.material.io/components/progress-indicators/overview" rel="noopener noreferrer"&gt;https://m3.material.io/components/progress-indicators/overview&lt;/a&gt;). No basta con "decorar" la espera. Hay que explicar qué está ocurriendo y cuál es la siguiente señal que debe observar el usuario.&lt;/p&gt;

&lt;p&gt;También me gusta pensar estos estados con la misma disciplina que usamos para construir &lt;a href="https://dev.to/silviutech/contratos-de-email-para-agentes-llm-4dh6"&gt;contratos de email que siguen siendo comprobables&lt;/a&gt;. Si un flujo asincrono no comunica bien sus etapas, el usuario termina interpretando síntomas en vez de entender el sistema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué debe comunicar un buen estado de espera
&lt;/h2&gt;

&lt;p&gt;Un loader útil no necesita mucho texto, pero sí necesita intención. Normalmente intento cubrir estas cuatro piezas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Qué acción se está procesando.&lt;/li&gt;
&lt;li&gt;Qué paso externo puede tardar.&lt;/li&gt;
&lt;li&gt;Qué debería hacer la persona mientras tanto.&lt;/li&gt;
&lt;li&gt;Cuándo conviene preocuparse de verdad.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Por ejemplo, "Cargando..." casi nunca alcanza. En cambio, "Estamos enviando tu enlace de verificación. Suele tardar menos de 20 segundos" orienta mucho mejor. Si además agregas "puedes seguir en esta pantalla", bajas la ansiedad un monton.&lt;/p&gt;

&lt;p&gt;En apps donde backend y frontend trabajan por separado, también ayuda nombrar el origen del retraso sin tecnicismos innecesarios. Decir "esperando confirmación del servicio de email" suele ser más honesto que mostrar un spinner infinito y listo. Esa claridad se vuelve aun más valiosa cuando el equipo ya ha invertido en &lt;a href="https://dev.to/silviutech/fastapi-emails-aislados-por-branch-3i51-temp-slug-9747599?preview=15d0f24ab23156ff7add433341db5b88816d63c95c3ecff5b0582256604f6e1410fd3181238aaa64757da0be78c0c8b8b797801df629e31b80e4e55a"&gt;emails aislados por branch&lt;/a&gt;: si internamente separas estados y causas, la UI deberia reflejar esa misma precision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patron en React para loaders honestos
&lt;/h2&gt;

&lt;p&gt;En vez de tener un booleano &lt;code&gt;isLoading&lt;/code&gt; para todo, me funciona mejor modelar el estado como fases. Así evitas mensajes ambiguos y puedes adaptar el feedback segun el paso real.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SubmitPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting_email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;phaseCopy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SubmitPhase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;idle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Listo para enviar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Completa el formulario y continua.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;sending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enviando solicitud&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Estamos guardando tus datos ahora mismo.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;waiting_email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revisa tu bandeja&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;El enlace de verificación suele llegar en menos de 20 segundos.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Todo listo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tu cuenta ya quedó verificada.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No pudimos completar el proceso&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Vuelve a intentarlo o revisa tu conexión.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y el componente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProgressNotice&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SubmitPhase&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;phaseCopy&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"progress-notice"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting_email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"spinner"&lt;/span&gt; &lt;span class="na"&gt;aria-hidden&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No es una arquitectura revolucionaria, pero mejora bastante el comportamiento de la interfaz. También hace más facil medir cada fase por separado. Si notas que la mayoría de usuarios pasa demasiados segundos en &lt;code&gt;waiting_email&lt;/code&gt;, ya no culpas al frontend por intuición nomas. Tienes una pista concreta para investigar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detalles de accesibilidad y CSS que importan
&lt;/h2&gt;

&lt;p&gt;Hay varios matices pequeños que cambian mucho la experiencia:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Usa &lt;code&gt;aria-live="polite"&lt;/code&gt; cuando el contenido cambie tras una acción del usuario.&lt;/li&gt;
&lt;li&gt;No reemplaces toda la pantalla si solo cambia una parte del flujo.&lt;/li&gt;
&lt;li&gt;Evita loaders gigantes con mucho movimiento si la acción tarda poco.&lt;/li&gt;
&lt;li&gt;Acompaña el spinner con texto; el icono solo se queda medio vacio semánticamente.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En CSS suelo preferir un bloque estable, con jerarquía de texto clara, antes que un overlay invasivo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.progress-notice&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt; &lt;span class="m"&gt;1.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--border-subtle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;180deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#fffdf7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#fff8ec&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.progress-notice&lt;/span&gt; &lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;52ch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--text-muted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sobre rendimiento percibido, una referencia util es el Web Almanac, que sigue mostrando el costo real de añadir JavaScript y complejidad extra a la experiencia web (&lt;a href="https://almanac.httparchive.org/en/2024/javascript" rel="noopener noreferrer"&gt;https://almanac.httparchive.org/en/2024/javascript&lt;/a&gt;). A veces un loader "sofisticado" añade más peso del que compensa. Se ve bonito en review, pero en producción queda un poco meh si retrasa el render o distrae demasiado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A rapida
&lt;/h2&gt;

&lt;h2&gt;
  
  
  ¿Skeleton o spinner?
&lt;/h2&gt;

&lt;p&gt;Si ya sabes la forma del contenido que va a aparecer, skeleton. Si el sistema está procesando una accion donde el resultado puede variar, prefiero texto + spinner pequeño. Mezclar ambos sin criterio suele confundir.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Hace falta mostrar tiempos estimados?
&lt;/h2&gt;

&lt;p&gt;Solo cuando tienes datos razonables. Inventar "5 segundos" y fallar seguido erosiona confianza. Mejor usar rangos prudentes, o frases como "suele tardar menos de 20 segundos".&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Debo bloquear toda la interfaz?
&lt;/h2&gt;

&lt;p&gt;No siempre. Si el usuario todavía puede leer, corregir o navegar sin romper el flujo, deja esas opciones activas. Bloquear todo por costumbre vuelve la app torpe y un poco brusca.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist para revisar antes de enviar
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;El loader explica qué está ocurriendo, no solo que "algo pasa".&lt;/li&gt;
&lt;li&gt;El texto distingue entre espera interna y dependencia externa.&lt;/li&gt;
&lt;li&gt;La fase de carga tiene una salida clara o un umbral de error.&lt;/li&gt;
&lt;li&gt;El componente usa accesibilidad básica sin sobreanunciar cambios.&lt;/li&gt;
&lt;li&gt;El estilo visual acompaña la espera sin volverla mas pesada.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Los loaders honestos no hacen magia con la latencia, pero sí mejoran la relación entre la persona y el sistema. Cuando la interfaz explica bien la espera, el producto parece más confiable, más cuidado y bastante menos fragil.&lt;/p&gt;

</description>
      <category>react</category>
      <category>a11y</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
