<?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>LLMs: revisa prompts por email sin perder trazas</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Wed, 12 Aug 2026 23:24:30 +0000</pubDate>
      <link>https://dev.to/silviutech/llms-revisa-prompts-por-email-sin-perder-trazas-37da</link>
      <guid>https://dev.to/silviutech/llms-revisa-prompts-por-email-sin-perder-trazas-37da</guid>
      <description>&lt;p&gt;Cuando un equipo mete LLMs en un flujo real, la parte fragil no suele ser el modelo. Suele ser el momento en que alguien revisa una propuesta por email, responde "ok", y otro proceso retoma horas despues con mas contexto, otro prompt base o un estado ya movido. El resultado paresce correcto desde fuera, pero la trazabilidad queda floja.&lt;/p&gt;

&lt;p&gt;En proyectos de automatizacion yo intento evitar justo eso: que la aprobacion humana sea un gesto informal. Si el agente genera un plan, la revision por correo debe apuntar a un artefacto congelado, no a un resumen que puede envejecer en minutos. Esa diferencia es pequena en papel, pero cambia mucho la confiabilidad del sistema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Donde se rompe la revision de prompts
&lt;/h2&gt;

&lt;p&gt;Yo lo dibujo en palabras asi:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El agente arma un plan y un borrador.&lt;/li&gt;
&lt;li&gt;El sistema envia un resumen por email.&lt;/li&gt;
&lt;li&gt;Una persona aprueba, corrige o rechaza.&lt;/li&gt;
&lt;li&gt;Un worker retoma la ejecucion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Los problemas aparecen en los cortes entre pasos. Si el email solo contiene texto libre, el worker termina "interpretando" una decision humana. Y si el plan ya cambio cuando llega la respuesta, la aprobacion deja de referirse a algo fijo.&lt;/p&gt;

&lt;p&gt;Por eso me gusta usar una vista pequena y estable: &lt;code&gt;run_id&lt;/code&gt;, &lt;code&gt;plan_hash&lt;/code&gt;, objetivo, riesgos y accion esperada. Nada mas. Para equipos backend esta idea se parece bastante a &lt;a href="https://dev.to/silviutech/fastapi-valida-emails-por-entorno-preview-40nd"&gt;validar correos por entorno preview&lt;/a&gt;: aislas el contexto antes de ejecutar, asi cada decision cae sobre un entorno y un artefacto claros.&lt;/p&gt;

&lt;h2&gt;
  
  
  El artefacto congelado que yo enviaria
&lt;/h2&gt;

&lt;p&gt;La regla que mejor me ha funcionado es simple: primero escribes el plan, luego generas el correo de revision desde ese plan, y despues ya no reescribes el contenido aprobado. El inbox no es base de datos ni fuente canonica; es la interfaz humana para aceptar o rechazar algo que ya existe.&lt;/p&gt;

&lt;p&gt;Un sobre minimo puede verse asi:&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;"20260812T232223Z-lucasg88"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plan_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b8c14a92"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"review_prompt_bundle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reply_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"approve_4b7c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-13T02:00:00Z"&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 eso el worker no necesita releer el hilo completo. Solo valida el &lt;code&gt;reply_token&lt;/code&gt;, carga el plan congelado y ejecuta. Es un patron un poco aburrido, si, pero hace que los fallos sean explicables.&lt;/p&gt;

&lt;p&gt;Tambien conviene dejar el resumen muy corto. Microsoft midio que los trabajadores son interrumpidos cada pocos minutos en promedio, lo que empeora cambios de contexto y revision superficial (&lt;a href="https://www.microsoft.com/en-us/worklab/work-trend-index" rel="noopener noreferrer"&gt;Microsoft Work Trend Index&lt;/a&gt;). Si mandas correos larguisimos para aprobar prompts, casi garantizas lecturas a medias.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separar lectura humana y ejecucion
&lt;/h2&gt;

&lt;p&gt;Para mi hay tres capas sanas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Planeacion: el agente genera &lt;code&gt;plan.json&lt;/code&gt; y artefactos.&lt;/li&gt;
&lt;li&gt;Revision: la persona opina sobre una version congelada.&lt;/li&gt;
&lt;li&gt;Ejecucion: un proceso aparte reanuda usando ids y hashes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ese corte reduce el riesgo de que una correccion textual se convierta en una reinterpretacion completa del trabajo. Tambien ayuda a auditar que paso de verda cuando algo sale raro.&lt;/p&gt;

&lt;p&gt;Un pseudocodigo corto:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resumeFromApproval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reply&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;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reply_token&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;decision&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approve&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plan&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;loadFrozenPlan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan_hash&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;executeApprovedPlan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plan&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;Si el equipo ya opera sistemas distribuidos, esto se parece a &lt;a href="https://dev.to/alexcarteruk/como-probar-correos-de-mantenimiento-en-kubernetes-4gif"&gt;probar correos operativos con trazabilidad&lt;/a&gt;: no basta con ver que el mensaje llego; necesitas demostrar que corresponde a una corrida concreta y a una accion exacta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuando usar inbox temporal y cuando no
&lt;/h2&gt;

&lt;p&gt;Aqui suelo ver bastante confusion. Una inbox temporal sirve muy bien para pruebas de entrega, smoke tests y escenarios de bajo riesgo. Para eso, un servicio como &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;temp mail so&lt;/a&gt; puede ayudar cuando necesitas crear correo temporal sin ensuciar bandejas reales ni mezclar QA con operacion. Pero yo no trataria esa inbox como sistema de aprobacion final.&lt;/p&gt;

&lt;p&gt;La razon es simple: una bandeja efimera puede confirmar formato, latencia y recepcion; no deberia convertirse en la evidencia principal de una decision humana. En pruebas rapidas he visto equipos mezclar datos de &lt;code&gt;tamp mail com&lt;/code&gt; o un &lt;code&gt;tem email&lt;/code&gt; cualquiera con revisiones reales, y luego cuesta bastante explicar que decision fue valida y cual era solo una prueba.&lt;/p&gt;

&lt;p&gt;Entonces la separacion que yo recomiendo es:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inbox temporal para verificar entrega y render.&lt;/li&gt;
&lt;li&gt;Canal controlado para aprobacion humana real.&lt;/li&gt;
&lt;li&gt;Artefacto congelado como referencia comun.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No es sofisticado, pero quita muchas dudas despues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist corto de implementacion
&lt;/h2&gt;

&lt;p&gt;Si tuviera que dejar esto mejor en una semana, haria lo siguiente:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Congelar el plan antes de enviar cualquier correo.&lt;/li&gt;
&lt;li&gt;Mostrar &lt;code&gt;run_id&lt;/code&gt; y &lt;code&gt;plan_hash&lt;/code&gt; en el asunto o al inicio del mensaje.&lt;/li&gt;
&lt;li&gt;Hacer que el worker consuma respuestas solo con &lt;code&gt;reply_token&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Expirar aprobaciones viejas para evitar reanudaciones raras.&lt;/li&gt;
&lt;li&gt;Guardar un recibo final con decision, artefacto y timestamp.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El tradeoff existe: introduces un poco mas de estructura y varios ids que al principio parescen exceso. Pero a cambio ganas auditoria, menos ambiguedad y menos retrabajo cuando el agente falla en una madrugada medio fea.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Debo mandar el prompt completo en el email?
&lt;/h3&gt;

&lt;p&gt;Yo no lo haria. Mejor manda resumen, riesgos y enlace interno al artefacto. El prompt completo cambia mas, ocupa mas y da pie a revisiones superficiales.&lt;/p&gt;

&lt;h3&gt;
  
  
  Que hago si alguien pide un cambio pequeno?
&lt;/h3&gt;

&lt;p&gt;Genera una nueva version del plan y otro &lt;code&gt;plan_hash&lt;/code&gt;. Reusar una aprobacion vieja para un plan nuevo es justo el atajo que despues rompe confianza.&lt;/p&gt;

&lt;h3&gt;
  
  
  Esto aplica solo a DEV.to o a cualquier automatizacion?
&lt;/h3&gt;

&lt;p&gt;Aplica a casi cualquier flujo donde LLMs producen una propuesta y una persona debe aprobarla. Publicacion, soporte, QA o tareas internas: el patron es parecido aunque cambie el payload.&lt;/p&gt;

&lt;p&gt;Si tu sistema ya manda correos de revision, yo empezaria por congelar el artefacto y hacer mas tonto al worker. Menos magia en la reanudacion, mas limites claros. Normalmente ahi esta la mejora que mas se nota, aunque no se vea muy glamorosa.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>automation</category>
      <category>devtools</category>
    </item>
    <item>
      <title>React: revisa email sin tapar el submit</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Wed, 12 Aug 2026 20:24:20 +0000</pubDate>
      <link>https://dev.to/silviutech/react-revisa-email-sin-tapar-el-submit-3a5n</link>
      <guid>https://dev.to/silviutech/react-revisa-email-sin-tapar-el-submit-3a5n</guid>
      <description>&lt;p&gt;En varios formularios de registro he visto el mismo error disfrazado de buena intencion: apenas la persona termina de escribir su correo, la UI bloquea el boton principal mientras hace una comprobacion extra. A veces consulta dominios, a veces revisa si el email ya existe, y a veces intenta detectar un alias temporal. La validacion puede ser util, pero la forma de mostrarla suele castigar la experiencia mas de la cuenta.&lt;/p&gt;

&lt;p&gt;Mi regla actual en React es simple: el submit no deberia desaparecer ni parecer roto solo porque hay una comprobacion asincrona en curso. El usuario necesita entender qué esta pasando, qué puede hacer ahora y qué pasará despues. Suena obvio, pero no siempre lo es en el codigo del dia a dia.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema real no es validar, es interrumpir
&lt;/h2&gt;

&lt;p&gt;Cuando una pantalla deshabilita el CTA demasiado pronto, aparecen tres costes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;el usuario cree que hizo algo mal aunque no haya error&lt;/li&gt;
&lt;li&gt;lector de pantalla recibe cambios pobres o tardios&lt;/li&gt;
&lt;li&gt;cada pulsacion dispara trabajo innecesario y ensucia el render&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En productos con growth loops o registro social, ese microcorte se nota bastante. Nielsen Norman Group lleva años remarcando que el &lt;a href="https://www.nngroup.com/articles/visibility-system-status/" rel="noopener noreferrer"&gt;estado del sistema debe ser visible&lt;/a&gt;, y en formularios eso incluye explicar si una revision sigue corriendo o si ya termino. No basta con poner un spinner pequeñito y esperar que todos lo interpreten igual.&lt;/p&gt;

&lt;p&gt;Tambien conviene recordar que no toda comprobacion merece frenar el flujo. Si tu equipo usa un &lt;code&gt;generador de correo temporal&lt;/code&gt; para QA o para aislar escenarios de soporte, la interfaz no deberia tratar todos esos casos como una crisis. Incluso cuando alguien escribe algo que en notas internas llamarían &lt;code&gt;tem email&lt;/code&gt;, el primer trabajo de la UI es aclarar, no castigar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que deberia pasar mientras React comprueba el email
&lt;/h2&gt;

&lt;p&gt;Yo intento separar tres capas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validacion sintactica inmediata&lt;/li&gt;
&lt;li&gt;comprobacion asincrona no bloqueante&lt;/li&gt;
&lt;li&gt;decision final al enviar&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La primera se resuelve en el cliente y debe ser rapida. La segunda puede consultar backend, pero sin romper el foco ni esconder el boton. La tercera ocurre en submit, que sigue siendo la fuente real de verdad.&lt;/p&gt;

&lt;p&gt;Ese orden reduce ruido y mejora conversion porque el usuario mantiene el control. Es parecido a &lt;a href="https://dev.to/alexcarteruk/facebook-en-guardias-aisla-correos-de-prueba-290"&gt;aislar correos de prueba para Facebook&lt;/a&gt;: separar señales ayuda a interpretar mejor el contexto. En frontend, separar estados hace lo mismo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patron pequeno con estado derivado
&lt;/h2&gt;

&lt;p&gt;Este patron me ha funcionado bien en formularios de React con chequeos de disponibilidad o politicas de dominio:&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;EmailCheckState&lt;/span&gt; &lt;span class="o"&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;kind&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="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&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;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&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;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;"&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EmailField&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;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;checkState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCheckState&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;EmailCheckState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kind&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;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="o"&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;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@&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;setCheckState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kind&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;setCheckState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&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;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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="o"&gt;=&amp;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="s2"&gt;`/api/email-check?value=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&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="s2"&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;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&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;result&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;setCheckState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&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="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&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="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warning&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="nx"&gt;result&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="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;250&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="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canSubmit&lt;/span&gt; &lt;span class="o"&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;length&lt;/span&gt; &lt;span class="o"&gt;&amp;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="p"&gt;&amp;lt;&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;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="s"&gt;"email-help"&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;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email-help"&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="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;checkState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Comprobando el correo...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;checkState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warning&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;checkState&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;canSubmit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Crear cuenta&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;/&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;Lo importante no es el snippet exacto. Lo importante es que &lt;code&gt;canSubmit&lt;/code&gt; no dependa de cada estado auxiliar. Si atas el boton a cualquier warning temporal, tu UI empieza a mandar señales mezcladas. Y luego todo se siente medio fragil, medio nervioso.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detalles de accesibilidad que evitan confusion
&lt;/h2&gt;

&lt;p&gt;Hay cuatro detalles chicos que pagan muy bien:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;usar &lt;code&gt;aria-live="polite"&lt;/code&gt; para mensajes no criticos&lt;/li&gt;
&lt;li&gt;no mover el layout de forma brusca cuando aparece ayuda&lt;/li&gt;
&lt;li&gt;mantener el foco en el input hasta que la persona decida avanzar&lt;/li&gt;
&lt;li&gt;reservar el estado deshabilitado para errores reales o campos vacios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WebAIM suele insistir en que los mensajes de error deben ser &lt;a href="https://webaim.org/techniques/formvalidation/" rel="noopener noreferrer"&gt;especificos y programaticamente asociados&lt;/a&gt; al campo. Estoy de acuerdo, pero además intentaria distinguir warning de bloqueo. Un warning como "ese dominio suele fallar en correos de activacion" puede convivir con el submit. Un bloqueo real como "falta el simbolo @" no.&lt;/p&gt;

&lt;p&gt;Si detrás hay APIs de registro o colas de correo, también ayuda conversar mejor con backend. En ese punto, tener &lt;a href="https://dev.to/silviutech/fastapi-logs-utiles-para-colas-de-email-4f6f"&gt;logs utiles para colas de email&lt;/a&gt; hace que soporte y frontend vean la misma pelicula, no dos versiones parciales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist para no romper conversion
&lt;/h2&gt;

&lt;p&gt;Antes de cerrar un formulario de este tipo, yo reviso esto:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;el boton principal sigue visible durante la comprobacion&lt;/li&gt;
&lt;li&gt;el texto de ayuda explica estado y siguiente paso&lt;/li&gt;
&lt;li&gt;los checks se cancelan al cambiar el valor&lt;/li&gt;
&lt;li&gt;el layout no salta cuando aparece feedback&lt;/li&gt;
&lt;li&gt;submit decide con datos finales, no con miedos temporales&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No es una receta magica, pero suele dar formularios mas calmados y mas legibles. Y eso, curiosamente, tambien mejora rendimiento porque dejas de disparar renders y bloqueos que nadie pidio. Aveces el mejor polish no es agregar mas logica, sino quitar friccion.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  ¿Cuando si bloquearias el submit?
&lt;/h3&gt;

&lt;p&gt;Cuando el campo esta vacio, el formato es claramente invalido o la accion seria insegura sin correccion. No por un check auxiliar todavia en progreso.&lt;/p&gt;

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

&lt;p&gt;No. Tambien sirve en recovery, invitaciones y cambios de email donde hay revisiones de dominio o disponibilidad.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Y el caso de correo temporal para Facebook?
&lt;/h3&gt;

&lt;p&gt;Lo trataria como contexto de producto o riesgo, no como razon automatica para esconder el CTA. Mejor advertir bien que reaccionar de forma brusca.&lt;/p&gt;

</description>
      <category>react</category>
      <category>a11y</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>React: valida emails sin castigar el foco</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Wed, 12 Aug 2026 17:24:23 +0000</pubDate>
      <link>https://dev.to/silviutech/react-valida-emails-sin-castigar-el-foco-15eh</link>
      <guid>https://dev.to/silviutech/react-valida-emails-sin-castigar-el-foco-15eh</guid>
      <description>&lt;p&gt;He visto un fallo bastante comun en formularios de signup: el input de email "ayuda" demasiado pronto. El usuario escribe tres letras, aparece un error rojo, cambia el layout, se mueve el foco visual y todo se siente mas torpe de lo que deberia. En productos reales eso no solo molesta, tambien baja la tasa de finalizacion y complica las pruebas.&lt;/p&gt;

&lt;p&gt;En equipos frontend suelo separar tres cosas: escritura, validacion y anuncio. Parece un detalle pequeno, pero cambia bastante la experiencia. Si ademas haces pruebas con cuentas de QA o con un servicio de &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;correo desechable&lt;/a&gt;, conviene que el formulario no dispare chequeos caros en cada tecla porque luego nadie sabe si el retraso viene del cliente, del API o del inbox de prueba.&lt;/p&gt;

&lt;p&gt;Si te sirvieron ideas previas sobre &lt;a href="https://dev.to/silviutech/fastapi-jobs-de-email-que-no-pierden-contexto-3mkb"&gt;jobs de email con contexto estable&lt;/a&gt; o sobre &lt;a href="https://dev.to/silviutech/como-aislar-emails-de-agentes-llm-en-flujos-automatizados-sin-perder-trazabilidad-26ac"&gt;aislar correos automatizados con trazabilidad&lt;/a&gt;, aqui va la version enfocada en el input y el feedback visual.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por que validar demasiado pronto rompe la experiencia
&lt;/h2&gt;

&lt;p&gt;Cuando un campo de email valida en cada &lt;code&gt;onChange&lt;/code&gt;, suelen aparecer tres problemas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;re-renderes innecesarios en formularios con varios campos&lt;/li&gt;
&lt;li&gt;mensajes de error antes de que exista una intencion completa&lt;/li&gt;
&lt;li&gt;anuncios repetidos para lectores de pantalla&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eso se nota bastante en mobile, donde el teclado tapa media pantalla y cualquier salto visual se siente peor. Segun &lt;a href="https://www.nngroup.com/articles/error-message-guidelines/" rel="noopener noreferrer"&gt;NN/g&lt;/a&gt;, los errores funcionan mejor cuando son claros, oportunos y faciles de corregir. La palabra importante ahi es "oportunos". No cada 120 ms, no en mitad de una direccion a medio escribir.&lt;/p&gt;

&lt;p&gt;Tambien he visto equipos persiguiendo bugs con nombres raros como temp org mail o fake e mail com en datos de prueba. Muchas veces no es que el usuario o QA usen cadenas raras, es que el formulario mezcla feedback de formato con feedback de disponibilidad y todo queda medio confuso.&lt;/p&gt;

&lt;h2&gt;
  
  
  La regla: separar escritura, chequeo y anuncio
&lt;/h2&gt;

&lt;p&gt;La regla que mejor me funciona es simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Mientras la persona escribe, solo guardo el valor.&lt;/li&gt;
&lt;li&gt;Al perder foco, hago validacion de formato.&lt;/li&gt;
&lt;li&gt;Solo despues de pasar formato lanzo chequeos async o disponibilidad.&lt;/li&gt;
&lt;li&gt;El mensaje accesible cambia pocas veces y con una prioridad clara.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No es una idea revolucionaria, pero si evita mucho ruido. Tambien hace mas facil medir rendimiento, por que sabes exactamente cuando empieza cada fase. En un par de productos vimos una baja visible en validaciones abortadas despues de mover la comprobacion async a &lt;code&gt;blur&lt;/code&gt;; no fue magia, fue quitar trabajo inutil nomas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patron de React que reduce ruido y re-renderes
&lt;/h2&gt;

&lt;p&gt;En React prefiero un estado pequeno y explicito:&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;EmailState&lt;/span&gt; &lt;span class="o"&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;phase&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="nl"&gt;value&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;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;format-error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;value&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;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;value&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;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;value&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;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server-error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;value&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isValidEmail&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="kr"&gt;string&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="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+@&lt;/span&gt;&lt;span class="se"&gt;[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y luego conecto eventos con responsabilidades distintas:&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;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="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EmailState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;phase&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="na"&gt;value&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;function&lt;/span&gt; &lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&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="p"&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;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="na"&gt;phase&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="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;current&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="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;onBlur&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="kr"&gt;string&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="nf"&gt;isValidEmail&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;format-error&lt;/span&gt;&lt;span class="dl"&gt;"&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="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;Escribe un correo valido.&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;setEmail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&lt;/span&gt;&lt;span class="dl"&gt;"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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="s2"&gt;`/api/email-check?email=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&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="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;data&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&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;data&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="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&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="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server-error&lt;/span&gt;&lt;span class="dl"&gt;"&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="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;No pudimos verificar este correo ahora.&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;No es el unico patron posible, pero tiene una ventaja practica: cada fase comunica una sola cosa. Eso vuelve mas facil estilizar, testear y anunciar cambios via &lt;code&gt;aria-live&lt;/code&gt;. Tambien evita la costumbre de meter cinco booleanos que se contradicen entre si, que pasa mas seguido de lo que nos gustaria admitir.&lt;/p&gt;

&lt;p&gt;En UI suelo dejar el mensaje persistente debajo del campo para no mover el layout. Si el texto cambia, cambia el contenido, no la estructura. Parece un detalle menor, pero el formulario se siente mucho mas estable y un poquito mas humano.&lt;/p&gt;

&lt;h2&gt;
  
  
  Como conectar pruebas reales sin contaminar el flujo
&lt;/h2&gt;

&lt;p&gt;Si tu equipo hace pruebas de onboarding, magic links o confirmacion de cuenta, este campo suele terminar conectado a un backend de verificacion o a una bandeja temporal. El error comun es usar el mismo trigger para todo: formato, disponibilidad, comprobacion remota y estado final. Ahi empiezan los tickets raros de "a veces tarda" y nadie sabe donde mirar.&lt;/p&gt;

&lt;p&gt;Yo intento mantener este contrato:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;el cliente resuelve formato localmente&lt;/li&gt;
&lt;li&gt;la red solo valida cuando ya hay una direccion plausible&lt;/li&gt;
&lt;li&gt;QA puede intercambiar un generador de inbox sin cambiar el comportamiento visual&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eso importa tambien para rendimiento. &lt;a href="https://web.dev/articles/optimize-input-delay" rel="noopener noreferrer"&gt;web.dev&lt;/a&gt; explica bien como reducir trabajo en interacciones para bajar la latencia percibida. Si tu input dispara validaciones costosas por tecla, el problema no siempre aparece en Lighthouse, pero si aparece en personas reales, y eso vale mas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist rapido para revisar accesibilidad y rendimiento
&lt;/h2&gt;

&lt;p&gt;Antes de dar por bueno un formulario de email, reviso esto:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;el campo no muestra error antes de tiempo&lt;/li&gt;
&lt;li&gt;el texto de ayuda y el error comparten una zona estable&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-invalid&lt;/code&gt; solo se activa cuando hay error real&lt;/li&gt;
&lt;li&gt;la validacion remota tiene cancelacion o ignora respuestas viejas&lt;/li&gt;
&lt;li&gt;el spinner, si existe, no roba foco ni ocupa demasiado espacio&lt;/li&gt;
&lt;li&gt;las pruebas cubren teclado, lector de pantalla y latencia de red&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No es una lista glamorosa, pero funciona. Y si algo falla, sabes mas rapido si fue CSS, estado, fetch o infraestructura. Ese orden ahorra horas, sinceramnte.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Debo validar solo al enviar?
&lt;/h3&gt;

&lt;p&gt;No siempre. Validar solo al enviar reduce ruido, pero puede llegar tarde para algunos flujos. Para email, &lt;code&gt;blur&lt;/code&gt; suele ser un punto bastante bueno entre ayuda y paciencia.&lt;/p&gt;

&lt;h3&gt;
  
  
  Que hago con dominios corporativos o reglas especiales?
&lt;/h3&gt;

&lt;p&gt;Primero separa formato basico de reglas de negocio. Si una empresa necesita dominios permitidos, eso va despues del formato correcto. Mezclar ambas cosas en un mismo mensaje queda confuso y aveces injusto para la persona usuaria.&lt;/p&gt;

&lt;h3&gt;
  
  
  Este patron sirve fuera de React?
&lt;/h3&gt;

&lt;p&gt;Si. La idea central no depende del framework. Lo importante es separar fases, limitar trabajo por tecla y mantener estable el anuncio accesible. React solo hace que ese modelado sea comodo.&lt;/p&gt;

</description>
      <category>react</category>
      <category>a11y</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>FastAPI: reintentos de verificacion sin duplicados</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Wed, 12 Aug 2026 14:24:17 +0000</pubDate>
      <link>https://dev.to/silviutech/fastapi-reintentos-de-verificacion-sin-duplicados-230p</link>
      <guid>https://dev.to/silviutech/fastapi-reintentos-de-verificacion-sin-duplicados-230p</guid>
      <description>&lt;p&gt;En flujos de signup, el problema no suele ser enviar el primer correo. El problema real aparece cuando la persona pulsa refresh, abre otra pestaña o el frontend reintenta por timeout, y de pronto salen dos o tres emails de verificacion. En FastAPI eso pasa bastante si el endpoint solo piensa en "mandar correo" y no en "recordar que ya hay uno en camino". Parece un detalle pequeno, pero luego rompe soporte, analitica y confianza del usuario.&lt;/p&gt;

&lt;h2&gt;
  
  
  El bug casi siempre esta en el contrato
&lt;/h2&gt;

&lt;p&gt;Cuando reviso este tipo de incidencias, casi siempre encuentro el mismo hueco:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;la API acepta la peticion&lt;/li&gt;
&lt;li&gt;crea o localiza al usuario&lt;/li&gt;
&lt;li&gt;manda el trabajo de email a background&lt;/li&gt;
&lt;li&gt;responde demasiado poco, o demasiado tarde&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si el cliente no recibe un estado reutilizable, vuelve a intentar. Si el worker tampoco tiene una llave estable, repite el envio. El fallo no esta solo en la cola ni en el frontend; esta en el contrato entre ambos. Y ese contrato deberia decir claramente si la verificacion ya esta &lt;code&gt;queued&lt;/code&gt;, &lt;code&gt;sending&lt;/code&gt; o &lt;code&gt;sent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Ese mismo principio me gusta en otros flujos asincronos, como estas &lt;a href="https://dev.to/silviutech/llms-aprobaciones-por-email-con-contexto-minimo-jf1"&gt;aprobaciones por email con contexto minimo&lt;/a&gt;. Cuando cada intento carga un identificador y un estado legible, depurar deja de ser una loteria.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patron pequeno para congelar reintentos
&lt;/h2&gt;

&lt;p&gt;La version simple funciona muy bien:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;normaliza el email&lt;/li&gt;
&lt;li&gt;genera una clave idempotente&lt;/li&gt;
&lt;li&gt;guarda una ventana corta de bloqueo&lt;/li&gt;
&lt;li&gt;devuelve el mismo estado si llega un intento igual&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Un ejemplo corto en FastAPI:&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;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&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;BackgroundTasks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FastAPI&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;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&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;verify_jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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;VerifyRequest&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;email&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;normalized_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&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="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="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&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;verify_key&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="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="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;normalized_email&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="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&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_verify_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_key&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;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="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;verify_jobs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;job_key&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="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;verify_jobs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;job_key&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&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="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# Aqui iria la integracion real con el proveedor
&lt;/span&gt;    &lt;span class="n"&gt;verify_jobs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;job_key&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="o"&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="n"&gt;verify_jobs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;job_key&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&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="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&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/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_verify&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;VerifyRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&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="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;job_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verify_key&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;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;verify_jobs&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="n"&gt;job_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;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;locked_until&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;locked_until&lt;/span&gt;&lt;span class="sh"&gt;"&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;locked_until&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&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;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;current&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;reused&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="n"&gt;verify_jobs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;job_key&lt;/span&gt;&lt;span class="p"&gt;]&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;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;locked_until&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="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;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;send_verify_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_key&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;email&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;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;reused&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&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 si congela el reintento durante unos minutos, el backend ya deja de abrir correos duplicados por nervios del cliente o por latencia medio fea. Para muchas APIs, eso resuelve la mayor parte del caos inicial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que estado devolver al cliente
&lt;/h2&gt;

&lt;p&gt;Aqui conviene ser humilde y consistente. Yo suelo devolver solo lo que el frontend necesita para no improvisar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reused&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retry_after_seconds&lt;/code&gt; cuando aplica&lt;/li&gt;
&lt;li&gt;un &lt;code&gt;attempt_id&lt;/code&gt; o &lt;code&gt;job_key&lt;/code&gt; si luego habrá consulta de estado&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Con eso, el cliente puede mostrar "Ya estamos procesando tu correo" en vez de lanzar otro POST a ciegas. Suena basico, pero evita una cantidad muy decente de ruido. Tambien hace mas faciles los tests de Automatizacion, porque el flujo deja de depender de esperas vagas o temporizadores magicos.&lt;/p&gt;

&lt;p&gt;Si el producto tiene emails sensibles, tambien separaria &lt;code&gt;sent&lt;/code&gt; de &lt;code&gt;accepted_by_provider&lt;/code&gt;. No siempre quieres prometer que el correo salio cuando solo se puso en cola. Ese matiz parece menor, pero ahorra varios tickets un poco incomodos despues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Donde encaja el correo temporal desechable
&lt;/h2&gt;

&lt;p&gt;En QA o staging, muchas pruebas se hacen con un correo temporal desechable. No me parece malo por si mismo; de hecho ayuda a no contaminar bandejas reales. El punto importante es no mezclar esas direcciones con metricas de onboarding o soporte. Si en tus seeds, tickets o notas aparece texto raro como &lt;code&gt;temp org mail&lt;/code&gt; o &lt;code&gt;fake e mail com&lt;/code&gt;, mejor tratarlo como evidencia de prueba y no como comportamiento del usuario final.&lt;/p&gt;

&lt;p&gt;Para equipos que necesitan una referencia controlada, un &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;generador de cuentas de correo temporal&lt;/a&gt; como tempmailso puede servir dentro de pruebas tecnicas o revisiones internas. La clave es que ese uso no cambie el contrato principal del endpoint: la API igual debe responder con estado estable, con o sin bandeja efimera.&lt;/p&gt;

&lt;p&gt;Tambien viene bien separar este trafico de mensajes de negocio, como los &lt;a href="https://dev.to/hannahdev56/saas-emails-de-churn-con-mejor-contexto-2b7i"&gt;emails de churn con mejor contexto&lt;/a&gt;. Son problemas distintos. Uno busca verificar una cuenta sin duplicados; el otro intenta comunicar una decision de producto. Si mezclas ambos mundos, las metricas se vuelven confusas y el copy se resiente un poco.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Cuanto deberia durar el bloqueo?
&lt;/h3&gt;

&lt;p&gt;Para signup normal, 2 a 5 minutos suele bastar. Menos tiempo deja escapar reintentos; mucho mas puede frustrar a quien de verdad necesita otro correo. No hay numero perfecto, pero tres minutos es un punto de partida bastante sano.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis o SQL?
&lt;/h3&gt;

&lt;p&gt;Si solo quieres una ventana breve y mucha velocidad, Redis encaja bien. Si además quieres auditoria o correlacion con eventos de autenticacion, una tabla SQL es mas comoda. Las dos valen, depende mas del sistema que del framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hay que bloquear todos los reenvios?
&lt;/h3&gt;

&lt;p&gt;No siempre. A veces basta con reutilizar el estado actual y habilitar un boton de reenviar cuando la ventana termina. Bloquear por completo suele ser mas tosco de lo necesario, y aveces hasta complica soporte.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>backend</category>
      <category>api</category>
    </item>
    <item>
      <title>FastAPI: webhooks de email sin carreras</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Wed, 12 Aug 2026 11:24:21 +0000</pubDate>
      <link>https://dev.to/silviutech/fastapi-webhooks-de-email-sin-carreras-2ea</link>
      <guid>https://dev.to/silviutech/fastapi-webhooks-de-email-sin-carreras-2ea</guid>
      <description>&lt;p&gt;Si tu app envia correos desde un worker y luego recibe webhooks de entrega, el punto fragil casi nunca es el envio. El problema real aparece cuando llegan eventos fuera de orden: &lt;code&gt;processed&lt;/code&gt;, &lt;code&gt;delivered&lt;/code&gt;, &lt;code&gt;opened&lt;/code&gt;, a veces repetidos y a veces varios segundos despues. En FastAPI eso suele terminar en estados raros, soporte mirando logs a mano y alguien diciendo "seguro fue un retry". Me ha pasado mas de una vez, y casi siempre el bug no estaba en el provider sino en nuestro contrato interno.&lt;/p&gt;

&lt;p&gt;La idea que mejor funciona es bastante simple: cada correo saliente necesita un &lt;code&gt;delivery_id&lt;/code&gt; estable desde el momento en que se crea el job, y todos los webhooks deben escribirse contra ese mismo identificador. Suena obvio, pero cuando no existe, el backend termina correlacionando por &lt;code&gt;email&lt;/code&gt;, por asunto o por timestamps aproximados. Eso es fragil, medio incomodo y aveces imposible de explicar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por que los webhooks de email se vuelven confusos
&lt;/h2&gt;

&lt;p&gt;Un flujo comun se ve asi:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;FastAPI crea un registro de envio&lt;/li&gt;
&lt;li&gt;un worker manda el correo&lt;/li&gt;
&lt;li&gt;el provider devuelve un &lt;code&gt;message_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;mas tarde llegan webhooks de estado&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El hueco aparece entre el paso 2 y 4. Si el worker reintenta, o si el provider reenvia eventos, puedes terminar actualizando el registro equivocado. Tambien pasa que staging usa cuentas efimeras para pruebas y soporte busca referencias en una bandeja externa con nombres como &lt;code&gt;dummy e mail&lt;/code&gt;. Ese detalle parece menor, pero mezcla lenguaje humano, aliases temporales y eventos tecnicos en una sola bolsa.&lt;/p&gt;

&lt;p&gt;Cuando el equipo ya procesa alertas o correos de incidentes, esta clase de orden tambien ayuda en otros contextos. El mismo principio que sirve para &lt;a href="https://dev.to/alexcarteruk/como-validar-correos-de-alertmanager-tras-rotar-secretos-en-kubernetes-4fi5"&gt;validar correos operativos despues de un cambio&lt;/a&gt; sirve aqui: si no puedes unir un evento con una accion concreta, depurar se vuelve lentisimo.&lt;/p&gt;

&lt;h2&gt;
  
  
  El contrato minimo que evita carreras
&lt;/h2&gt;

&lt;p&gt;Yo intentaria mantener solo estas piezas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;delivery_id&lt;/code&gt; generado por tu sistema&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;provider_message_id&lt;/code&gt; guardado cuando el envio sale&lt;/li&gt;
&lt;li&gt;&lt;code&gt;event_type&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;event_at&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload_hash&lt;/code&gt; o alguna huella para deduplicar&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Con eso ya puedes hacer dos cosas utiles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;aplicar idempotencia al webhook&lt;/li&gt;
&lt;li&gt;actualizar el estado visible del envio sin adivinar&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si durante QA usas un servicio externo para aislar cuentas, un &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;correo temporal gratis&lt;/a&gt; puede ser suficiente para separar escenarios. Pero ese link no arregla la trazabilidad por si solo. La parte importante es que el webhook llegue con metadata que apunte a &lt;code&gt;delivery_id&lt;/code&gt;, no que el equipo tenga otra inbox donde mirar.&lt;/p&gt;

&lt;p&gt;Tambien recomiendo exponer un endpoint interno como &lt;code&gt;GET /email-deliveries/{delivery_id}&lt;/code&gt;. No hace falta que sea publico ni bonito. Solo debe responder rapido a preguntas concretas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿se envio?&lt;/li&gt;
&lt;li&gt;¿que webhook fue el ultimo aceptado?&lt;/li&gt;
&lt;li&gt;¿hubo un duplicado descartado?&lt;/li&gt;
&lt;li&gt;¿que provider_message_id quedo asociado?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ese tipo de respuesta corta le ahorra mucho tiempo a backend, QA y soporte. Es una de esas mejoras pequenas que nadie celebra en la demo, pero todos notan cuando algo falla.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un ejemplo pequeno con FastAPI
&lt;/h2&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;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;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;APIRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Header&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;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;APIRouter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="nd"&gt;@router.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;/email-deliveries&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;create_delivery&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="n"&gt;delivery_id&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="s"&gt;dlv_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;save_delivery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&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;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&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="k"&gt;await&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;delivery_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;delivery_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;delivery_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;state&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="nd"&gt;@router.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;/webhooks/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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;email_webhook&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="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x_signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Header&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="nf"&gt;verify_signature&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;x_signature&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;401&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;invalid signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;delivery_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&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;delivery_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;event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;webhook_event_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_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;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;deduplicated&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;store_webhook_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delivery_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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;apply_delivery_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delivery_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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_type&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La parte que no conviene saltarse es &lt;code&gt;store_webhook_event&lt;/code&gt; antes de mutar estado. Si haces el update primero y guardas la evidencia despues, cuando llegue un retry podrias no saber si es duplicado o una segunda entrega valida. Es un bug re comun, y luego cuesta bastante reproducirlo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Como depurarlo sin revisar diez sistemas
&lt;/h2&gt;

&lt;p&gt;Para mi, la mejor rutina operativa es esta:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;buscar &lt;code&gt;delivery_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ver la linea de tiempo de eventos aceptados&lt;/li&gt;
&lt;li&gt;comparar &lt;code&gt;provider_message_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;revisar solo el payload del ultimo cambio de estado&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nada de abrir cinco dashboards a la vez. Nada de buscar por asunto del correo. Nada de "creo que este &lt;code&gt;tempail mail&lt;/code&gt; era del test correcto". Si el contrato esta bien hecho, una sola consulta te dice si el worker nunca envio, si el provider atraso el evento o si tu API aplico el webhook dos veces.&lt;/p&gt;

&lt;p&gt;Tambien ayuda guardar una transicion monotona de estados. Por ejemplo, permitir &lt;code&gt;queued -&amp;gt; sent -&amp;gt; delivered&lt;/code&gt;, pero no dejar que un webhook viejo mueva &lt;code&gt;delivered&lt;/code&gt; otra vez a &lt;code&gt;processed&lt;/code&gt;. Este detalle importa porque algunos proveedores reintentan callbacks durante horas. Según &lt;a href="https://postmarkapp.com/blog/why-idempotency-is-important" rel="noopener noreferrer"&gt;Postmark&lt;/a&gt;, los sistemas de webhooks deben tratar duplicados como algo normal, no excepcional.&lt;/p&gt;

&lt;p&gt;Si ya trabajaste con &lt;a href="https://dev.to/alexcarteruk/sre-correos-de-rollback-que-si-orientan-25fi"&gt;correos de rollback con contexto real&lt;/a&gt;, la idea es parecida: el correo en si importa, pero el valor operativo aparece cuando puedes explicar rapido que paso, cuando paso y por que ese mensaje pertenece a ese intento exacto. No es glamour, pero si te evita varias horas tontas por semana.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  ¿Hace falta guardar todos los payloads?
&lt;/h3&gt;

&lt;p&gt;No siempre. Yo guardaria el payload bruto por un tiempo corto y luego una version resumida. Para depuracion temprana sirve mucho, pero no hace falta retenerlo para siempre.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Puedo correlacionar solo con &lt;code&gt;provider_message_id&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Puedes, pero quedas atado a cuando ese valor aparece. Con &lt;code&gt;delivery_id&lt;/code&gt; propio, tus APIs ya tienen una referencia comun antes del envio.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Esto sirve solo para correos transaccionales?
&lt;/h3&gt;

&lt;p&gt;No. Tambien sirve para invitaciones, recibos, magic links o avisos internos. Cualquier flujo async con webhooks gana claridad cuando deja de adivinar correlaciones.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>backend</category>
      <category>api</category>
    </item>
    <item>
      <title>FastAPI: estado visible para correos async</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Wed, 12 Aug 2026 05:24:19 +0000</pubDate>
      <link>https://dev.to/silviutech/fastapi-estado-visible-para-correos-async-2p6d</link>
      <guid>https://dev.to/silviutech/fastapi-estado-visible-para-correos-async-2p6d</guid>
      <description>&lt;p&gt;Cuando un correo transaccional tarda, muchos equipos miran solo el worker o el proveedor. Yo suelo encontrar otro problema mas simple: la API no tiene una forma clara de decir que esta pasando. Entonces soporte pregunta, producto especula y el frontend termina reintentando por ansiedad. No es un fallo dramatico, pero si desgasta mucho.&lt;/p&gt;

&lt;p&gt;En proyectos con FastAPI, me ha servido separar dos cosas: el envio del correo y la visibilidad del estado. Parece obvio, pero cuando agregas un endpoint pequeno para consultar progreso, el flujo deja de sentirse opaco. El usuario ve algo util, el equipo depura mas rapido y el backend queda bastante mas ordenado.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema real no es enviar, sino saber que paso
&lt;/h2&gt;

&lt;p&gt;Mandar un email async no es dificil. Lo dificil es responder preguntas muy normales:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El correo ya entro a cola o la peticion fallo antes.&lt;/li&gt;
&lt;li&gt;El worker lo esta procesando o quedo atascado.&lt;/li&gt;
&lt;li&gt;El proveedor acepto el envio o todavia no.&lt;/li&gt;
&lt;li&gt;El usuario debe reintentar o solo esperar un poco mas.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cuando esa informacion no existe, empiezan los parches medio raros: timeouts largos, botones de reenviar sin contexto y logs que nadie entiende del todo. En equipos pequenos eso pasa bastante, y se nota mas cuando mezclas QA, staging y pruebas con direcciones de tempail mail copiadas desde notas rapidas.&lt;/p&gt;

&lt;p&gt;Tambien complica revisar incidentes. Si tus eventos de infraestructura ya generan &lt;a href="https://dev.to/alexcarteruk/terraform-correos-de-drift-que-si-orientan-211j"&gt;correos de drift con contexto util&lt;/a&gt;, pero el sistema de producto no deja ver el estado del email, terminas con dos niveles de opacidad. Ese cruce confunde mas de lo que ayuda.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un endpoint de estado cambia la conversacion
&lt;/h2&gt;

&lt;p&gt;El patron que mas me gusta es guardar un &lt;code&gt;delivery_id&lt;/code&gt; al crear el trabajo y exponer &lt;code&gt;GET /email-status/{delivery_id}&lt;/code&gt;. No resuelve todo, pero cambia la conversacion del equipo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El frontend ya no "espera por fe".&lt;/li&gt;
&lt;li&gt;Soporte puede ver si el correo sigue en cola.&lt;/li&gt;
&lt;li&gt;El usuario recibe mensajes mas honestos.&lt;/li&gt;
&lt;li&gt;Los reintentos dejan de ser un reflejo automatico.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;La gracia es que no necesitas acoplarte demasiado al proveedor. Tu API puede hablar en estados propios como &lt;code&gt;queued&lt;/code&gt;, &lt;code&gt;sending&lt;/code&gt;, &lt;code&gt;sent&lt;/code&gt; y &lt;code&gt;failed&lt;/code&gt;. Luego mapeas lo que venga de tu worker, de Celery, de una cola interna o del proveedor que uses. Es una capa pequena, pero baja mucho el ruido diario.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ejemplo pequeno en FastAPI
&lt;/h2&gt;

&lt;p&gt;Este ejemplo usa una memoria en proceso solo para explicar la idea. En produccion lo moveria a Redis o SQL, claro, pero el contrato queda casi igual:&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;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&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;BackgroundTasks&lt;/span&gt;&lt;span class="p"&gt;,&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="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="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;DELIVERIES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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;email&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&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delivery_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;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;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;DELIVERIES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;delivery_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;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;DELIVERIES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;delivery_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;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&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="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# aqui iria la llamada al proveedor real
&lt;/span&gt;
    &lt;span class="n"&gt;DELIVERIES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;delivery_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;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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="n"&gt;DELIVERIES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;delivery_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;provider_ref&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&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="s"&gt;mail-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delivery_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="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;DELIVERIES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;delivery_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;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&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="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&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;create_email_job&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;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;delivery_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;DELIVERIES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;]&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;payload&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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&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="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&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="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider_ref&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;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;send_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delivery_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;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;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;delivery_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;delivery_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;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="nd"&gt;@app.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;/email-status/{delivery_id}&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;email_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delivery_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;delivery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DELIVERIES&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="n"&gt;delivery_id&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;delivery&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;404&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;delivery not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;delivery&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lo importante no es el diccionario. Lo importante es el contrato. El cliente recibe un &lt;code&gt;delivery_id&lt;/code&gt;, puede consultar estado y deja de depender de sleeps raros. En flujos de Automatizacion esto viene muy bien porque los consumidores no siempre son humanos; a veces son otros servicios, jobs o tests que necesitan una señal simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que campos guardo en produccion
&lt;/h2&gt;

&lt;p&gt;Suelo empezar con estos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;delivery_id&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_ref&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;created_at&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;updated_at&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;last_error&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attempt_count&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si el correo participa en signup o recuperacion de cuenta, agrego tambien un &lt;code&gt;subject_type&lt;/code&gt; o &lt;code&gt;flow_name&lt;/code&gt;. Eso evita mezclar metricas y te deja responder mejor cuando alguien pregunta por que cierto email no llego. Parece un detalle, pero despues ahorra horas de ida y vuelta, sobretodo cuando aparecen reintentos desde clientes viejos o cron jobs medio apurados.&lt;/p&gt;

&lt;p&gt;Otra ventaja es que puedes alinear mejor tus mensajes operativos con otros sistemas. Si ya miras &lt;a href="https://dev.to/alexcarteruk/kubernetes-correos-de-rollback-sin-confusion-bk4"&gt;correos de rollback sin confusion&lt;/a&gt; para infraestructura, tener estados similares en producto hace que el equipo lea todo con el mismo lenguaje. Eso reduce errores tontos, o almenos los hace mas faciles de ver.&lt;/p&gt;

&lt;h2&gt;
  
  
  Como lo pruebo sin volverme loco
&lt;/h2&gt;

&lt;p&gt;Yo suelo probar este patron con tres escenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El worker completa rapido y el estado pasa de &lt;code&gt;queued&lt;/code&gt; a &lt;code&gt;sent&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;El worker tarda y el cliente consulta varias veces sin romper nada.&lt;/li&gt;
&lt;li&gt;El worker falla y el endpoint devuelve contexto suficiente para decidir si reintentar.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si puedes, agrega una prueba donde el frontend haga polling cada pocos segundos con backoff corto. Segun &lt;a href="https://web.dev/articles/optimize-long-tasks" rel="noopener noreferrer"&gt;Google Web Vitals guidance&lt;/a&gt;, reducir esperas bloqueantes y mostrar progreso ayuda a que la experiencia se perciba mas estable, incluso cuando el trabajo real tarda un poco. No hace milagros, pero si ordena la expectativa del usuario.&lt;/p&gt;

&lt;p&gt;Tambien me gusta medir tiempo en cola y tiempo hasta proveedor aceptado. En sistemas internos, esos dos numeros suelen explicar casi todo. Cuando alguien dice "el email nunca llego", muchas veces el problema real fue "el estado nunca fue visible". Son cosas distintas, y conviene no mezclarlas.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Conviene polling o webhooks?
&lt;/h3&gt;

&lt;p&gt;Para paneles internos y flujos de registro, polling simple suele alcanzar. Webhooks sirven mas cuando otro sistema externo necesita enterarse del cambio.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis o Postgres?
&lt;/h3&gt;

&lt;p&gt;Redis va muy bien si solo quieres velocidad y TTL. Postgres me gusta cuando necesito auditoria, joins o revisar historico despues.&lt;/p&gt;

&lt;h3&gt;
  
  
  Esto reemplaza logs?
&lt;/h3&gt;

&lt;p&gt;No. Los complementa. Los logs cuentan la historia tecnica; el endpoint de estado expone una version corta y consumible para clientes o soporte.&lt;/p&gt;

&lt;p&gt;Si tu sistema ya envia correos pero todavia obliga a adivinar, este patron merece una tarde de trabajo. No es una refactor enorme, y sin embargo deja un backend mucho mas legible, menos nervioso y, francamente, bastante mas facil de operar.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>backend</category>
      <category>api</category>
    </item>
    <item>
      <title>FastAPI: reenvios seguros en signup</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Tue, 11 Aug 2026 17:24:19 +0000</pubDate>
      <link>https://dev.to/silviutech/fastapi-reenvios-seguros-en-signup-5f56</link>
      <guid>https://dev.to/silviutech/fastapi-reenvios-seguros-en-signup-5f56</guid>
      <description>&lt;p&gt;En muchos equipos el problema no es enviar el correo de bienvenida, sino reenviarlo sin querer tres veces cuando el usuario toca refresh, abre otra pestaña o el frontend repite la llamada por timeout. En FastAPI eso pasa mas seguido de lo que parece, y luego soporte recibe el clasico "me llegaron varios correos". No suena grave, pero desgasta bastante el signup y deja logs medio feos.&lt;/p&gt;

&lt;p&gt;Mi regla simple es esta: el endpoint de registro no deberia pensar solo en "enviar email", tambien deberia decidir si ese email ya esta en camino. Cuando ese criterio vive en el backend, el flujo se vuelve mas estable y mucho mas facil de explicar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por que se duplican los correos de signup
&lt;/h2&gt;

&lt;p&gt;La secuencia normal suele verse asi:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El usuario manda el formulario.&lt;/li&gt;
&lt;li&gt;La API crea la cuenta o deja el signup en proceso.&lt;/li&gt;
&lt;li&gt;Se dispara un email de verificacion en background.&lt;/li&gt;
&lt;li&gt;El cliente no recibe señal clara y reintenta.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si no existe una llave de idempotencia o una ventana corta de enfriamiento, cada intento puede abrir otro envio. A veces el frontend lo causa; otras veces es un worker que reintenta sin revisar estado previo. El resultado se parece mucho a un bug raro, pero en realidad es un contrato incompleto.&lt;/p&gt;

&lt;p&gt;Esto tambien se nota cuando intentas &lt;a href="https://dev.to/alexcarteruk/como-validar-correos-tras-un-rollback-en-terraform-3jdg"&gt;validar correos durante cambios de infraestructura&lt;/a&gt;. Si no sabes si el problema fue el deploy, el proveedor o un reenvio repetido, la investigacion tarda mas de la cuenta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patron pequeno para reenvios seguros
&lt;/h2&gt;

&lt;p&gt;No hace falta montar una arquitectura enorme. Con tres decisiones ya mejora bastante:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generar una clave estable por intento de signup.&lt;/li&gt;
&lt;li&gt;Guardar el estado del envio con una expiracion corta.&lt;/li&gt;
&lt;li&gt;Reusar el estado si llega otra solicitud igual en pocos minutos.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Un ejemplo minimo:&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;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&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;BackgroundTasks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FastAPI&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;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&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;EMAIL_JOBS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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;SignupRequest&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;email&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;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;es&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;job_key&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="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="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&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="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&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_signup_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&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;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="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;EMAIL_JOBS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&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="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;EMAIL_JOBS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&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="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# aqui iria el proveedor real
&lt;/span&gt;    &lt;span class="n"&gt;EMAIL_JOBS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&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="o"&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="n"&gt;EMAIL_JOBS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&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="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&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/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;signup_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;SignupRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;job_key&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;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&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="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EMAIL_JOBS&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="n"&gt;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;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;locked_until&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;locked_until&lt;/span&gt;&lt;span class="sh"&gt;"&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;locked_until&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&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;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;current&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;reused&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="n"&gt;EMAIL_JOBS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&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;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;locked_until&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="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;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;send_signup_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&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;email&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;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;reused&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&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 el diseño final para produccion, claro. Pero la idea base ya funciona: si llega la misma peticion mientras el envio sigue "vivo", el backend responde con el mismo estado en lugar de abrir otro correo. Es una mejora chica, pero evita bastante ruido y hace que el sistema se sienta mas serio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que datos conviene guardar
&lt;/h2&gt;

&lt;p&gt;Cuando armo esto en un backend real, casi siempre guardo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;job_key&lt;/code&gt; o &lt;code&gt;attempt_key&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;estado actual: &lt;code&gt;queued&lt;/code&gt;, &lt;code&gt;sending&lt;/code&gt;, &lt;code&gt;sent&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;locked_until&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;provider_message_id&lt;/code&gt; si existe&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;last_error&lt;/code&gt; si algo sale mal&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Con eso puedes responder soporte, depurar colas y decidir si un boton de "reenviar" debe habilitarse o no. Tambien ayuda a &lt;a href="https://dev.to/silviutech/fastapi-espera-util-para-correos-async-3ojn"&gt;manejar la espera de correo asincrono&lt;/a&gt; sin dejar al usuario mirando una pantalla vacia. El frontend no necesita adivinar: consulta estado, muestra un mensaje claro y listo.&lt;/p&gt;

&lt;p&gt;Otro detalle util es distinguir entre correo de producto y correo de prueba. En QA aparecen direcciones de direccion de correo temporal, cuentas de staging y texto bastante raro, por ejemplo tem email o tempail mail pegados en notas, seeds o capturas. Si eso no queda etiquetado, luego cuesta entender que fue trafico real y que fue una prueba medio improvisada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Como probar el flujo sin confundir evidencia
&lt;/h2&gt;

&lt;p&gt;Aqui me funciona un checklist corto:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Crear un caso donde el cliente repita la peticion dos o tres veces.&lt;/li&gt;
&lt;li&gt;Verificar que solo exista un job activo por email normalizado.&lt;/li&gt;
&lt;li&gt;Confirmar que el estado pueda consultarse hasta terminar.&lt;/li&gt;
&lt;li&gt;Separar pruebas tecnicas de metricas de onboarding real.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si haces esto temprano, el equipo deja de perseguir "bugs fantasma". Muchas veces el envio estaba bien; lo roto era la falta de memoria entre intentos. Y eso, sinceramnte, es bastante comun en sistemas que crecieron rapido.&lt;/p&gt;

&lt;p&gt;Tambien conviene devolver un texto humilde pero claro, algo como: "Ya estamos procesando tu correo de verificacion". No es marketing brillante, pero evita el click nervioso. Ese pequeno detalle reduce reintentos tontos y deja al usuario mas tranquilo.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Conviene una tabla SQL o alcanza Redis?
&lt;/h3&gt;

&lt;p&gt;Para una ventana corta de bloqueo, Redis va muy bien. Si necesitas auditoria o analisis posterior, prefiero tabla SQL con timestamps y estado final. Depende del tipo de producto, pero ambas opciones sirven.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cuanto deberia durar el bloqueo?
&lt;/h3&gt;

&lt;p&gt;Para signup normal, 2 a 5 minutos suele alcanzar. Menos tiempo deja escapar duplicados; mucho mas tiempo puede frustrar a quien de verdad necesita reenviar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Esto reemplaza una cola de trabajos?
&lt;/h3&gt;

&lt;p&gt;No. Lo complementa. La cola sigue siendo responsable de ejecutar el envio. Esta capa solo decide si tiene sentido abrir otro intento o reusar el que ya existe.&lt;/p&gt;

&lt;p&gt;Si tu API de signup hoy envia correos "a lo que salga", empezar por este patron suele dar una mejora rapida. No es glamoroso, pero si quita ruido, ordena el backend y evita una clase de error que aparece mas de lo que deberia.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>backend</category>
      <category>api</category>
    </item>
    <item>
      <title>Debug Playwright Email Delays With Timelines</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Mon, 10 Aug 2026 17:26:01 +0000</pubDate>
      <link>https://dev.to/silviutech/debug-playwright-email-delays-with-timelines-4od9</link>
      <guid>https://dev.to/silviutech/debug-playwright-email-delays-with-timelines-4od9</guid>
      <description>&lt;p&gt;When a Playwright email test fails, the first guess is usually "the inbox was slow again". Sometimes that is true. Just as often, the app queued the message late, the worker retried, or the test started polling before the request that matters had even finished. If you change waits before you understand the sequence, you can make the suite pass for a day and still learn almost nothing.&lt;/p&gt;

&lt;p&gt;The workflow that has helped me most is boring in a good way: capture a tiny delivery timeline for one run, then decide which boundary is unstable. It sounds almost too simple, but it turns vague flakiness into something you can act on. For QA teams using Playwright and Automation heavily, this keeps triage grounded in evidence instead of vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why email delays look random in Playwright
&lt;/h2&gt;

&lt;p&gt;Email checks often hide three separate clocks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the browser action that triggers delivery&lt;/li&gt;
&lt;li&gt;the app job or provider handoff&lt;/li&gt;
&lt;li&gt;the inbox polling loop in the test&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If those clocks are not visible, one red test can lead to three wrong fixes. I still see suites where the test bumps &lt;code&gt;expect.poll()&lt;/code&gt; from 10 seconds to 30 seconds, the failure rate drops a bit, and everyone moves on. Then the same bug comes back next week in CI, just with more waiting and less signal. That is why I now prefer a small timeline log before I touch any timeout, even if it feels slighly slower at first.&lt;/p&gt;

&lt;p&gt;This also pairs well with &lt;a href="https://dev.to/silviutech/how-to-stop-playwright-email-tests-from-flaking-across-parallel-workers-ok8"&gt;parallel inbox isolation&lt;/a&gt;. Isolation stops worker collisions, but it does not explain whether a single worker saw a slow app event, a slow mailbox query, or a broken assertion. You need both pieces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture a delivery timeline before changing waits
&lt;/h2&gt;

&lt;p&gt;My rule is simple: for one failing scenario, write down five moments.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;user action submitted&lt;/li&gt;
&lt;li&gt;network response returned&lt;/li&gt;
&lt;li&gt;app acknowledged the email job&lt;/li&gt;
&lt;li&gt;inbox polling started&lt;/li&gt;
&lt;li&gt;matching email found&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That list looks obvious, but teams skip it because they want a fix fast. The timeline is what tells you if the test is early, the app is late, or the mailbox API is noisy. Without it, a "slow inbox" story can hide a regression in queue publishing or a missing idempotency key.&lt;/p&gt;

&lt;p&gt;I also like putting a run id in the email subject or metadata so the inbox query is narrow. If the test is searching a shared mailbox with only a broad subject filter, you will get messy evidence realy fast. A dedicated scenario id makes the failure easier to read and much easier to replay.&lt;/p&gt;

&lt;p&gt;Here is the small helper shape I reuse:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@playwright/test&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;InboxProbe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;foundAt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&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;waitForVerificationEmail&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="nx"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fetchMessage&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;marks&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&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;submitClickedAt&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="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;await&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;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Create account&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;responseSettledAt&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InboxProbe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;startedAt&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="na"&gt;attempts&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="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;expect&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;poll&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attempts&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;fetchMessage&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;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;foundAt&lt;/span&gt; &lt;span class="o"&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="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&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="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;intervals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_000&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;toBeTruthy&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;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;probe&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 code is not fancy, and that is the point. I want just enough structure to answer two questions after a failure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the browser finish the triggering step when we thought it did?&lt;/li&gt;
&lt;li&gt;Did the email arrive late, or did our polling begin too soon?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your app can emit a job-created event or response header, log that too. One extra timestamp can save an hour of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small Playwright helper for timeline logging
&lt;/h2&gt;

&lt;p&gt;Once the timing marks exist, I write them to a tiny artifact. A JSON file is enough. The useful part is not the format, it is that the file survives the failed run and can be compared across green and red builds. In CI, I upload it the same way I upload traces and screenshots.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFile&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;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&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;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;artifacts/email-timeline.json&lt;/span&gt;&lt;span class="dl"&gt;"&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="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;signup-verification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;totalMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;foundAt&lt;/span&gt; &lt;span class="o"&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="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;submitClickedAt&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="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 is where &lt;a href="https://dev.to/mrdapperx/preflight-files-for-safer-publish-scripts-3lgo"&gt;preflight evidence checks&lt;/a&gt; gave me a useful mindset. Before you trust the outcome, make sure the evidence files you need were actually produced. For tests, that means asking "do I have the timeline, the trace, and the request id?" before I accept a flaky failure story.&lt;/p&gt;

&lt;p&gt;One practical note: if your staging flow uses a shared inbox provider for throwaway accounts, label the account by scenario and keep the query narrow. I have seen teams debug the wrong message because a &lt;code&gt;dummy e mail&lt;/code&gt; account from a previous retry was still visible in the same mailbox window. That kind of mistake feels small, but it can waste a whole mornng.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist for deciding whether the app or the test is wrong
&lt;/h2&gt;

&lt;p&gt;After a couple of runs, I use this checklist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If submit-to-response is unstable, inspect the UI trigger first.&lt;/li&gt;
&lt;li&gt;If response-to-job-create is unstable, inspect the backend handoff.&lt;/li&gt;
&lt;li&gt;If job-create-to-found is unstable, inspect the mail provider or inbox filter.&lt;/li&gt;
&lt;li&gt;If the email is found but the assertion fails, inspect parsing and matching logic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sequence keeps the triage calm. It also stops the common "just increase the timeout" reflex, which is usefull in emergencies but rarely a good root-cause fix. It also makes bug reports more consisent, because each failure gets discussed with the same few timestamps.&lt;/p&gt;

&lt;p&gt;When I review these failures with teams, the most common surprise is how often the mailbox is innocent. The app retried late, the worker batched jobs, or the test started polling before the save action finished. The timeline makes those cases much less arguable, which is nice when everybody is a bit tired and somtimes a little impatient and just wants the red build gone.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Should I always log a timeline for email tests?
&lt;/h2&gt;

&lt;p&gt;Not for every green run. I usually keep the helper available and persist the artifact for CI failures, nightly runs, or newly unstable flows. That gives enough evidence without bloating every job.&lt;/p&gt;

&lt;h2&gt;
  
  
  What timeout should I start with?
&lt;/h2&gt;

&lt;p&gt;Start from a timeout that matches the system you actually own, then narrow the polling intervals and evidence around it. A random bigger timeout can hide the regression for a bit, but it does not make the suite more reliable.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>React: valida email sin castigar cada tecla</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Mon, 10 Aug 2026 05:24:21 +0000</pubDate>
      <link>https://dev.to/silviutech/react-valida-email-sin-castigar-cada-tecla-2k2p</link>
      <guid>https://dev.to/silviutech/react-valida-email-sin-castigar-cada-tecla-2k2p</guid>
      <description>&lt;p&gt;En muchos formularios React, el campo de email falla no porque la expresion regular sea mala, sino porque la interfaz se pone nerviosa demasiado pronto. La persona escribe tres letras, aparece un error rojo, desaparece, vuelve a salir, y el input termina sintiendose mas reganon que util. Ese detalle baja bastante la calidad percibida del flujo, aunque la logica sea correcta.&lt;/p&gt;

&lt;p&gt;Ultimamente intento separar tres momentos: escribir, comprobar y anunciar. Parece un cambio pequeño, pero mejora mucho la sensacion de control. Tambien reduce trabajo visual innecesario, que en moviles o equipos lentos se nota mas de lo que a veces admitimos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuando validar demasiado pronto empeora el formulario
&lt;/h2&gt;

&lt;p&gt;Validar en cada tecla suena moderno, pero muchas veces mete mas friccion que valor. Mientras alguien aun esta escribiendo &lt;code&gt;maria@emp&lt;/code&gt;, no necesita una alerta dramatica. Necesita terminar la idea.&lt;/p&gt;

&lt;p&gt;Ese error temprano rompe ritmo, ensucia el area visible y ademas hace que el usuario dude si el problema es suyo o del sistema. En productos con bastante onboarding, este tipo de micro-friccion se acumula rapido. La interfaz parece activa, si, pero no siempre parece lista.&lt;/p&gt;

&lt;p&gt;Por eso me gusta pensar este campo igual que otros flujos donde la claridad manda primero, como estos &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;. Si el sistema distingue bien cada etapa, luego es mas facil leer el estado y corregir algo sin adivinar.&lt;/p&gt;

&lt;p&gt;Tambien sirve cuando QA hace pruebas con un correo temporal gratis o con valores medio raros de tem email para revisar copy, foco y mensajes. Si el componente reacciona a todo como si fuera una emergencia, cuesta ver que bug era real y cual fue puro ruido, la verdad.&lt;/p&gt;

&lt;h2&gt;
  
  
  El patron: separa escritura, comprobacion y anuncio
&lt;/h2&gt;

&lt;p&gt;La idea practica es esta:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;mientras la persona escribe, solo guardas valor y limpias ruido viejo&lt;/li&gt;
&lt;li&gt;cuando sale del campo o intenta enviar, validas de verdad&lt;/li&gt;
&lt;li&gt;solo anuncias error cuando ya hay una decision util que mostrar&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Eso evita castigar cada tecla y mantiene el feedback ligado a una accion entendible. No es una regla rigida para todos los casos, pero en formularios de registro suele funcionar bastante bien.&lt;/p&gt;

&lt;p&gt;Tambien ayuda a performance percibida. Si cada pulsacion dispara validacion compleja, analytics, estilos y mensajes, el campo parece pegajoso. No hablo siempre de un cuello de botella enorme; hablo de esa sensacion pequena de "algo no va fino". Y esa sensacion importa un monton en frontend.&lt;/p&gt;

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

&lt;p&gt;Este es un enfoque bastante terrenal:&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;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;emailLooksValid&lt;/span&gt; &lt;span class="o"&gt;=&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="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="sr"&gt;+@&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.\S&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EmailField&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValue&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;touched&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTouched&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="kc"&gt;false&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;showError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;touched&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;emailLooksValid&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="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="nx"&gt;showError&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Escribe un email valido&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; &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;label&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;"field"&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;span&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;"label"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&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;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;value&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;setValue&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;onBlur&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTouched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&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-invalid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showError&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="s"&gt;"email-help"&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;span&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-help"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showError&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;note error&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;note&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;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;span&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="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;El truco no esta en la regex. Esta en que el error aparece cuando el usuario ya termino un primer intento. Eso vuelve el mensaje mas justo. Tambien deja el campo visualmente mas estable, porque la ayuda existe desde el principio y no entra a empujones.&lt;/p&gt;

&lt;p&gt;Si el formulario hace llamadas remotas, ya prefiero mover comprobaciones costosas fuera del &lt;code&gt;onChange&lt;/code&gt;. Primero confirmo formato local y luego, si hace falta, reviso disponibilidad o reglas del backend. En interfaces con varios estados, esa separacion recuerda bastante a escribir &lt;a href="https://dev.to/alexcarteruk/kubernetes-correos-claros-en-mantenimientos-sre-327l"&gt;correos claros en mantenimientos&lt;/a&gt;: cada señal llega cuando toca, no toda a la vez.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que revisar para que tambien rinda mejor
&lt;/h2&gt;

&lt;p&gt;Hay varias cosas pequeñas que suelo mirar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no recalcular validaciones pesadas en cada pulsacion&lt;/li&gt;
&lt;li&gt;reservar espacio para el helper o el error&lt;/li&gt;
&lt;li&gt;no pintar rojo un campo vacio nada mas cargar&lt;/li&gt;
&lt;li&gt;mantener mensajes cortos, especificos y faciles de leer&lt;/li&gt;
&lt;li&gt;probar en movil con teclado abierto, que ahi todo se vuelve mas sensible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si usas autoguardado, telemetria o una comprobacion async, mejor aun si desacoplas eso del feedback inmediato. Una persona no deberia notar que internamente estas registrando eventos, consultando reglas o comparando dominios raros como temp org mail. Si lo nota, algo ya esta compitiendo con la tarea principal.&lt;/p&gt;

&lt;p&gt;En accesibilidad, lo minimo para mi es:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;aria-invalid&lt;/code&gt; solo cuando el error es real&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-describedby&lt;/code&gt; enlazando la ayuda&lt;/li&gt;
&lt;li&gt;contraste suficiente&lt;/li&gt;
&lt;li&gt;texto que diga que corregir, no solo que algo "esta mal"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ese ultimo punto parece obvio, pero no siempre pasa. "Email invalido" informa poco. "Escribe un email con formato &lt;a href="mailto:nombre@dominio.com"&gt;nombre@dominio.com&lt;/a&gt;" guia bastante mejor, incluso si no es perfecto para todos los casos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas que me hago antes de darlo por bueno
&lt;/h2&gt;

&lt;p&gt;Q: ¿Valido al escribir alguna vez?&lt;br&gt;&lt;br&gt;
A: Si, pero solo en casos muy concretos, como sugerencias suaves o autocompletado. Para error formal, prefiero esperar a blur o submit.&lt;/p&gt;

&lt;p&gt;Q: ¿Esto hace el formulario mas lento?&lt;br&gt;&lt;br&gt;
A: Normalmente al reves. Al quitar trabajo y cambios visuales por tecla, el flujo se siente bastante mas liviano, aunque el ahorro tecnico sea pequeño.&lt;/p&gt;

&lt;p&gt;Q: ¿Y si necesito feedback instantaneo?&lt;br&gt;&lt;br&gt;
A: Intento que sea feedback neutral primero. Ayuda, pista o progreso. Error fuerte solo cuando ya hay contexto suficiente. Si no, el campo se vuelve un semaforo histérico, y eso cansa rapido.&lt;/p&gt;

&lt;p&gt;Al final, validar email bien no es solo aceptar o rechazar una cadena. Es decidir cuando hablar y con que intensidad. En React, ese timing vale casi tanto como la validacion misma. Cuando lo ajustas bien, el formulario se siente mas claro, mas rapido y un poco mas humano, aunque tenga algun detallecito imperfecto por ahi.&lt;/p&gt;

</description>
      <category>react</category>
      <category>a11y</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>React: valida email sin frenar el formulario</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Mon, 10 Aug 2026 02:24:34 +0000</pubDate>
      <link>https://dev.to/silviutech/react-valida-email-sin-frenar-el-formulario-3gdo</link>
      <guid>https://dev.to/silviutech/react-valida-email-sin-frenar-el-formulario-3gdo</guid>
      <description>&lt;p&gt;En muchos formularios, validar el email ya no es solo revisar si existe una &lt;code&gt;@&lt;/code&gt;. Tambien queremos detectar dominios mal escritos, evitar cuentas duplicadas y, a veces, comprobar si cierta direccion ya fue usada. El problema es que metemos todo eso en el mismo &lt;code&gt;onChange&lt;/code&gt;, y de pronto el campo mas simple del signup se vuelve el mas pesado. Se siente medio torpe, la verdad.&lt;/p&gt;

&lt;p&gt;Lo he visto bastante en productos hechos con React: escribes dos letras, aparece un spinner, desaparece, vuelve, cambia el helper text y la persona deja de confiar. No es que la red sea lentisima; es que el componente comunica mal la espera. Si ademas haces pruebas con un correo de usar y tirar para QA, esa friccion se nota mas porque repites el flujo varias veces y el ruido cansa rapido.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por que la validacion lenta se siente peor de lo que es
&lt;/h2&gt;

&lt;p&gt;La percepcion de velocidad importa mucho. Segun el &lt;a href="https://www.nngroup.com/articles/response-times-3-important-limits/" rel="noopener noreferrer"&gt;Nielsen Norman Group&lt;/a&gt;, cuando una respuesta pasa de 1 segundo el usuario ya nota una pausa clara. En un campo de email, ese segundo parece aun mas largo porque ocurre mientras la persona sigue escribiendo.&lt;/p&gt;

&lt;p&gt;Por eso intento separar una pregunta tecnica de una pregunta de experiencia: no solo "cuanto tarda la verificacion?", sino "cuando conviene molestar con esa verificacion?". Son cosas distintas, y a veces las mezclamos un poco feo.&lt;/p&gt;

&lt;p&gt;Tambien ayuda mirar como otros equipos hacen &lt;a href="https://dev.to/hannahdev56/como-validar-correos-de-reactivacion-de-trial-en-un-saas-sin-mezclar-cohortes-4hne"&gt;probar emails aislados sin mezclar corridas&lt;/a&gt;. Aunque ese caso va mas por QA y cohortes, la leccion sirve igual: si separas estados y contexto, entiendes mejor que esta pasando y evitas conclusiones raras.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separar formato, disponibilidad y estado pendiente
&lt;/h2&gt;

&lt;p&gt;Mi regla es simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;formato: corre local y responde al instante&lt;/li&gt;
&lt;li&gt;disponibilidad: corre con debounce y solo cuando el formato ya es valido&lt;/li&gt;
&lt;li&gt;estado pendiente: se muestra sin bloquear el resto del formulario&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ese corte reduce bastante trabajo innecesario. Tambien deja mas claro que parte del feedback es definitiva y cual todavia esta "pensando". Parece un detalle chico, pero cambia la sensacion de control.&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;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;EmailField&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="nx"&gt;onChange&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;formatError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFormatError&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;availability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setAvailability&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="sr"&gt;+@&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.\S&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&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="nf"&gt;setFormatError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Escribe un email valido&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;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setAvailability&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="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="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="nf"&gt;setTimeout&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setAvailability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&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;exists&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;checkEmailAvailability&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="nf"&gt;setAvailability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;taken&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;free&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="mi"&gt;350&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="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;value&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;label&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;"field"&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;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&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;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;value&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;onChange&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-invalid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formatError&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;taken&lt;/span&gt;&lt;span class="dl"&gt;"&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="s"&gt;"email-status"&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;span&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-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;formatError&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Comprobando disponibilidad...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;taken&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ese email ya existe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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; &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;span&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="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 un componente exotico ni nada. La gracia esta en que no detienes a la persona mientras esperas la llamada remota. Si el boton de submit necesita bloquearse, prefiero hacerlo por una regla clara del formulario completo, no por cada pulsacion. Si no, el campo empieza a pelearse con el resto de la UI y queda bastante meh.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patron simple en React para no bloquear
&lt;/h2&gt;

&lt;p&gt;Cuando el chequeo remoto depende de un backend o de una cola, me funciona mejor tratar el estado pendiente como informacion auxiliar, no como alarma. Eso evita el clasico spinner ansioso que entra demasiado pronto. Tambien reduce renderizados bobos si el usuario pega el email entero de una sola vez.&lt;/p&gt;

&lt;p&gt;En equipos donde los flujos de confirmacion son delicados, me gusta conectar esta idea con practicas de &lt;a href="https://dev.to/alexcarteruk/kubernetes-correos-de-rollback-que-si-ayudan-2737"&gt;validar correos antes de tocar produccion&lt;/a&gt;. El contexto es otro, claro, pero la disciplina es parecida: validar con señales utiles, no con ruido.&lt;/p&gt;

&lt;p&gt;Un detalle practico: si en tus tests usas cadenas raras como &lt;code&gt;tepm mail com&lt;/code&gt; o &lt;code&gt;tamp mail com&lt;/code&gt; para revisar sugerencias, no mezcles esa heuristica con la validacion principal. La sugerencia de dominio puede ser amable; la comprobacion real debe seguir siendo predecible. Si juntas ambas cosas en el mismo mensaje, el usuario no sabe si el problema es tipografico, de negocio o de red. Y ahi ya arrancamos mal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accesibilidad y rendimiento en el mismo componente
&lt;/h2&gt;

&lt;p&gt;La parte accesible no viene despues. Va dentro del patron:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;aria-describedby&lt;/code&gt; para que el estado del campo tenga contexto&lt;/li&gt;
&lt;li&gt;una zona de mensaje persistente para que el layout no salte&lt;/li&gt;
&lt;li&gt;debounce corto para bajar ruido de red&lt;/li&gt;
&lt;li&gt;texto distinto para "invalido", "comprobando" y "ocupado"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si quieres hilar fino, puedes medirlo. En una prueba interna muy simple, este cambio suele bajar consultas innecesarias porque solo dispara la verificacion cuando el formato ya paso. No te doy un numero magico porque depende muchisimo del producto, pero normalmente se nota en trazas, y tambien en la calma del formulario, que no siempre medimos pero deberiamos un poco mas.&lt;/p&gt;

&lt;p&gt;Otra cosa: no marques error de disponibilidad mientras el usuario aun esta escribiendo. Ese feedback prematuro parece rapido, pero en realidad distrae. Peor aun en movil, donde cada cambio visual ocupa mas espacio y el teclado ya te roba media pantalla. Ahi cualquier detallito pesa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas que me hago antes de darlo por bueno
&lt;/h2&gt;

&lt;p&gt;Antes de cerrar un campo asi, suelo revisar esto:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;el mensaje cambia sin mover el layout?&lt;/li&gt;
&lt;li&gt;el input sigue usable si la red tarda dos segundos?&lt;/li&gt;
&lt;li&gt;el submit explica por que se bloquea, si se bloquea?&lt;/li&gt;
&lt;li&gt;el lector de pantalla oye estados diferentes y utiles?&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Debo validar disponibilidad en cada tecla?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
No. Casi nunca compensa. Mejor despues de un debounce corto y solo con formato valido.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Y si el backend falla?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Muestra un estado neutro o reintento suave. No conviertas una duda de red en un error del usuario, eso se siente injusto nomas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sirve para cualquier formulario?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Sirve bastante en signup, invitaciones y recovery. En otros casos, quizá con validar formato basta y ya.&lt;/p&gt;

&lt;p&gt;Cuando el campo de email deja de comportarse como mini app independiente, todo el formulario respira mejor. React no necesita hacer trucos enormes para esto. Solo hace falta separar responsabilidades, comunicar espera con un poco de tacto y recordar que rendimiento tambien es una sensacion, no solo una metrica.&lt;/p&gt;

</description>
      <category>react</category>
      <category>performance</category>
      <category>a11y</category>
      <category>webdev</category>
    </item>
    <item>
      <title>LLMs: planes congelados para aprobar por email</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sun, 09 Aug 2026 14:24:24 +0000</pubDate>
      <link>https://dev.to/silviutech/llms-planes-congelados-para-aprobar-por-email-562n</link>
      <guid>https://dev.to/silviutech/llms-planes-congelados-para-aprobar-por-email-562n</guid>
      <description>&lt;p&gt;Cuando un agente LLM manda un correo para pedir aprobacion, el fallo serio rara vez esta en el texto del mensaje. Casi siempre aparece un paso despues: el equipo aprueba una version del plan, pero el worker ejecuta otra. Ese desajuste parece pequeno al principio, aunque termina rompiendo auditoria, soporte y confianza operativa.&lt;/p&gt;

&lt;p&gt;En equipos que ya automatizan contenido, reportes o acciones internas, yo prefiero tratar el email como una frontera de control, no como un canal donde el agente sigue pensando. El punto es congelar un artefacto antes de pedir respuesta humana. Si luego el job se reanuda, debe leer ese artefacto y no rearmar el mundo desde cero. Suena obvio, pero no siempre se hace, y ahi empiesa el ruido.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema no es el email, es el cambio de artefacto
&lt;/h2&gt;

&lt;p&gt;Lo dibujo asi, en palabras:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El agente propone una accion.&lt;/li&gt;
&lt;li&gt;Genera un plan estable.&lt;/li&gt;
&lt;li&gt;El sistema manda un resumen por email.&lt;/li&gt;
&lt;li&gt;Una persona responde.&lt;/li&gt;
&lt;li&gt;Un ejecutor retoma el trabajo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;La grieta aparece entre el paso 2 y el 5. Si el plan no esta congelado, la respuesta humana termina pegada a un resumen narrativo, no a una pieza verificable. En ese punto el inbox se vuelve una pseudo base de datos. Funciona un rato, luego deja rastros confusos y cuesta explicar que se aprobo de verda.&lt;/p&gt;

&lt;p&gt;En flujos de QA lo vi incluso con pruebas rapidas sobre &lt;code&gt;fake e mail com&lt;/code&gt; o &lt;code&gt;temp org mail&lt;/code&gt;: el email "llego bien", pero nadie podia probar que el ejecutor uso el mismo payload aprobado. El correo fue correcto; el sistema alrededor, no tanto.&lt;/p&gt;

&lt;h2&gt;
  
  
  El plan congelado como frontera entre pensar y ejecutar
&lt;/h2&gt;

&lt;p&gt;Mi preferencia es separar tres capas con limites bastate estrictos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Planeacion: el agente decide el que y produce &lt;code&gt;plan.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Aprobacion: el humano revisa un resumen corto con ids claros.&lt;/li&gt;
&lt;li&gt;Ejecucion: un script retoma usando artefactos congelados.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Este patron se parece a lo que me gusta en &lt;a href="https://dev.to/silviutech/contratos-de-inbox-para-agentes-llm-26i3"&gt;contratos de inbox para agentes LLM&lt;/a&gt;: menos interpretacion durante el polling, mas evidencia estable entre pasos. Y aunque el caso de uso sea distinto, tambien conecta con una idea muy util de &lt;a href="https://dev.to/hannahdev56/como-auditar-emails-de-upgrade-en-saas-8h0"&gt;auditar emails de upgrade en SaaS&lt;/a&gt;: si no puedes unir evento, contenido y resultado final, la revision humana se queda corta.&lt;/p&gt;

&lt;p&gt;El tradeoff es claro. Guardar un plan congelado mete un paso extra y algunos campos mas. A cambio, reduces reintentos ambiguos, corridas que mezclan contexto nuevo y respuestas humanas que parecen validas pero llegaron tarde.&lt;/p&gt;

&lt;h2&gt;
  
  
  Campos minimos que yo guardaria
&lt;/h2&gt;

&lt;p&gt;No hace falta una tonelada de metadatos. Yo empezaria con esto:&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;"20260809T142220Z-lucasg88"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plan_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b1c4e920"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"decision_scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"publish_post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reply_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"approve_71aa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-09T15:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"executor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"publish_devto.sh"&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;Cada campo responde una duda operativa concreta:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;run_id&lt;/code&gt;: une logs, archivos y respuesta humana.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;plan_hash&lt;/code&gt;: evita que la aprobacion se aplique a una version distinta.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;decision_scope&lt;/code&gt;: dice exactamente que permiso se esta dando.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reply_token&lt;/code&gt;: bloquea respuestas cruzadas o reenvios viejos.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expires_at&lt;/code&gt;: cierra la ventana de aprobacion cuando ya no tiene sentido.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;executor&lt;/code&gt;: deja claro que script hara el paso final.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Con eso basta para mantener la arquitectura legible. Si luego quieres adjuntar resumenes, diffs o checklist, mejor hacerlo como lectura secundaria. El correo principal debe seguir siendo compacto, porque un operador cansado no va a leer cinco parrafos para aprobar una accion simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Donde poner la logica de reanudacion
&lt;/h2&gt;

&lt;p&gt;Yo no meteria la logica de reanudacion en el agente escritor. La dejaria en el ejecutor o en un worker muy pequeño. Esa separacion importa por dos razones:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El agente puede variar su razonamiento entre corridas.&lt;/li&gt;
&lt;li&gt;El ejecutor debe ser aburrido, estable y facil de auditar.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Un pseudocodigo simple seria este:&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;type&lt;/span&gt; &lt;span class="nx"&gt;ApprovalEnvelope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;runId&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;planHash&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;replyToken&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;decisionScope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;publish_post&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resumeAfterApproval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApprovalEnvelope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reply&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replyToken&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;verdict&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approve&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plan&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;loadFrozenPlan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;planHash&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;executePlan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plan&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;Fijate en la idea central: el ejecutor no "entiende todo el email". Solo valida un sobre corto y luego carga el artefacto congelado. Esa decision baja bastante la superficie de errores, sobre todo si el prompt del agente cambia entre una corrida y la siguiente.&lt;/p&gt;

&lt;p&gt;Tambien ayuda cuando hay incidencias. Si alguien pregunta por que una publicacion salio con cierto titulo o cierto tag, la respuesta no depende de reconstruir el contexto del LLM a posteriori. Solo necesitas el run folder, el hash y el resultado final. Eso hace la depuracion mucho mas tranquila, la verdad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist para una implementacion tranquila
&lt;/h2&gt;

&lt;p&gt;Si tuviera una semana para endurecer este flujo, haria esto en este orden:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Escribir &lt;code&gt;plan.json&lt;/code&gt; antes de mandar el email.&lt;/li&gt;
&lt;li&gt;Calcular y mostrar &lt;code&gt;plan_hash&lt;/code&gt; en el asunto o cuerpo.&lt;/li&gt;
&lt;li&gt;Limitar la aprobacion a un &lt;code&gt;decision_scope&lt;/code&gt; exacto.&lt;/li&gt;
&lt;li&gt;Reanudar solo con &lt;code&gt;reply_token&lt;/code&gt; valido y no expirado.&lt;/li&gt;
&lt;li&gt;Guardar un &lt;code&gt;publish-result.json&lt;/code&gt; o veredicto equivalente.&lt;/li&gt;
&lt;li&gt;Evitar que el ejecutor regenere contenido durante publish.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ese ultimo punto me parece clave. Cuando el ejecutor tambien "mejora" el texto, reordena tags o toca el cuerpo, se pierde la frontera entre planeacion y entrega. El sistema sigue funcionando, pero ya no sabes bien cual parte tomar en serio cuando algo sale raro.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Debo incluir todo el prompt del agente en el correo?
&lt;/h3&gt;

&lt;p&gt;No. Pondria el objetivo, los riesgos visibles y los ids de control. El prompt completo cambia mucho, ocupa espacio y casi nunca mejora la decision humana.&lt;/p&gt;

&lt;h3&gt;
  
  
  Y si la persona responde varias horas despues?
&lt;/h3&gt;

&lt;p&gt;Yo invalidaria la respuesta si &lt;code&gt;expires_at&lt;/code&gt; ya paso. Es mejor pedir una aprobacion nueva que ejecutar una intencion vieja en un contexto distinto.&lt;/p&gt;

&lt;h3&gt;
  
  
  Esto sirve solo para publicar posts?
&lt;/h3&gt;

&lt;p&gt;Para nada. El mismo patron vale para borrados, despliegues internos, cambios de configuracion o cualquier accion donde un LLM prepara algo y otro componente lo ejecuta despues.&lt;/p&gt;

&lt;p&gt;En resumen: si el email aprueba una accion, el sistema necesita congelar el plan antes de pedir permiso. No es la parte mas llamativa del stack, pero si una de las que mas reduce confucion cuando el flujo crece.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>automation</category>
      <category>devtools</category>
    </item>
    <item>
      <title>FastAPI: pruebas de signup sin inbox real</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sun, 09 Aug 2026 05:24:28 +0000</pubDate>
      <link>https://dev.to/silviutech/fastapi-pruebas-de-signup-sin-inbox-real-4jg9</link>
      <guid>https://dev.to/silviutech/fastapi-pruebas-de-signup-sin-inbox-real-4jg9</guid>
      <description>&lt;p&gt;Cuando pruebas un flujo de signup en FastAPI, el email de confirmacion suele ser la parte mas inestable. El request responde bien, el usuario queda creado, pero la prueba termina esperando un inbox real que aveces tarda, aveces cambia de formato y aveces ni siquiera corresponde al intento correcto. Ese tipo de fallo consume tiempo porque no sabes si se rompio el backend o solo el borde del test.&lt;/p&gt;

&lt;p&gt;Lo que mejor me ha funcionado es tratar el inbox como un contrato pequeño, no como una bandeja "magica". En vez de buscar el ultimo correo disponible, el backend devuelve un &lt;code&gt;attempt_id&lt;/code&gt; y el worker de email adjunta esa referencia en metadata. Con eso, el test consulta un recurso de estado y valida un solo mensaje. Parece simple, pero vuelve el flujo bastante mas predecible.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema de probar signup con un inbox real
&lt;/h2&gt;

&lt;p&gt;El enfoque comun es este:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;hacer &lt;code&gt;POST /signup&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;esperar unos segundos&lt;/li&gt;
&lt;li&gt;leer el ultimo correo recibido&lt;/li&gt;
&lt;li&gt;abrir el link de confirmacion&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Funciona hasta que corres varias pruebas al mismo tiempo. Entonces empiezan los falsos positivos, los timeouts y la clasica duda de si el correo pertenece al intento actual. Tambien complica usar una &lt;code&gt;direccion de correo falsa&lt;/code&gt; en staging, porque el valor sirve para aislar cuentas, pero no te dice nada sobre la correlacion entre request, job e inbox.&lt;/p&gt;

&lt;p&gt;En equipos pequeños esto suele resolverse con paciencia y logs. En equipos mas ocupados, termina en tests apagados "temporalmente" que duran semanas. Si ya pasaste por flujos de &lt;a href="https://dev.to/hannahdev56/saas-activacion-de-trial-sin-correos-ciegos-3bhb"&gt;activar trials sin correos ciegos&lt;/a&gt;, la idea se siente conocida: el problema no es solo enviar el correo, es exponer suficiente contexto para que otros sistemas lo entiendan rapido.&lt;/p&gt;

&lt;h2&gt;
  
  
  El contrato de inbox que simplifica FastAPI
&lt;/h2&gt;

&lt;p&gt;El patrón que recomiendo tiene tres piezas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;el endpoint de signup devuelve &lt;code&gt;attempt_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;el worker guarda estado de entrega por &lt;code&gt;attempt_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;el inbox de pruebas se consulta por esa referencia&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eso evita depender del "ultimo correo" y te deja validar solo el mensaje correcto. Si necesitas un proveedor externo para pruebas aisladas, una &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;direccion de correo desechable&lt;/a&gt; puede ayudar a separar cuentas efimeras, pero la clave real sigue siendo el identificador que une API, worker e inbox.&lt;/p&gt;

&lt;p&gt;Tambien conviene exponer un endpoint minimo, por ejemplo &lt;code&gt;GET /signup-attempts/{attempt_id}&lt;/code&gt; con campos como:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;email&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delivery_state&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;confirmation_url&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;last_error&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No hace falta devolver todo desde el inicio. Solo lo suficiente para que QA o automation sepan si deben esperar, reintentar o fallar. Ese contrato, aunque pequeño, ordena mucho el trabajo diario.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un ejemplo pequeno con FastAPI
&lt;/h2&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;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;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;APIRouter&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmailStr&lt;/span&gt;

&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;APIRouter&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;SignupIn&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;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EmailStr&lt;/span&gt;
    &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="nd"&gt;@router.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="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;202&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;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SignupIn&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;attempt_id&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="s"&gt;signup_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;create_pending_user&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;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;password&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;save_signup_attempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;attempt_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;attempt_id&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;email&lt;/span&gt;&lt;span class="o"&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;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&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="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_confirmation_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;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt_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;attempt_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;attempt_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;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="nd"&gt;@router.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;/signup-attempts/{attempt_id}&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;get_signup_attempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt_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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;load_signup_attempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt_id&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 worker:&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_confirmation_email&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;attempt_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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;update_signup_attempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delivery_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;link&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;build_confirmation_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;email&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;signup_confirm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;metadata&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;attempt_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;attempt_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;variables&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;confirmation_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;link&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="nf"&gt;update_signup_attempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;attempt_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;delivery_state&lt;/span&gt;&lt;span class="o"&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="n"&gt;confirmation_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;link&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;Lo importante no es el provider. Lo importante es que el intento queda nombrado desde el principio. Si mañana cambias de cola o de servicio de email, el test sigue mirando el mismo contrato.&lt;/p&gt;

&lt;h2&gt;
  
  
  Como validar el flujo sin esperas fragiles
&lt;/h2&gt;

&lt;p&gt;En automation yo haria algo asi:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;crear un email efimero para la prueba&lt;/li&gt;
&lt;li&gt;llamar &lt;code&gt;POST /signup&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;guardar &lt;code&gt;attempt_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;hacer polling a &lt;code&gt;GET /signup-attempts/{attempt_id}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;abrir &lt;code&gt;confirmation_url&lt;/code&gt; solo cuando &lt;code&gt;delivery_state == "sent"&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;La ventaja es que el tiempo de espera se vuelve explicito. Ya no preguntas "llego algo?", sino "este intento ya genero su correo?". Ese cambio parece menor, pero reduce muchisimo el ruido. Tambien te deja detectar cuando el backend creo el usuario pero el worker no movio estado, que era justo el hueco que antes quedaba oculto.&lt;/p&gt;

&lt;p&gt;Otro detalle practico: inserta el typo keyword &lt;code&gt;temp org mail&lt;/code&gt; solo donde tenga sentido de negocio, por ejemplo en una lista de terminos que llegan desde soporte o marketing. Yo lo he visto pasar en tickets reales, asi que conviene normalizar esos textos antes de usarlos para filtros o reportes. No es glamoroso, pero evita bugs algo tontos.&lt;/p&gt;

&lt;p&gt;Si tu equipo tambien trabaja campañas o lifecycle emails, te puede servir la idea de &lt;a href="https://dev.to/hannahdev56/saas-emails-de-churn-con-mejor-contexto-2b7i"&gt;mejor contexto para emails de churn&lt;/a&gt;. Aunque el objetivo cambia, la leccion es parecida: sin un identificador compartido, cada parte interpreta el mismo evento de forma distinta.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  ¿Hace falta guardar &lt;code&gt;confirmation_url&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;No siempre. En algunos equipos prefieren guardar solo un token o un &lt;code&gt;message_id&lt;/code&gt;. Pero para pruebas de extremo a extremo, tener la URL lista vuelve el diagnostico mas rapido y mas comodo.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Esto reemplaza un inbox de pruebas?
&lt;/h3&gt;

&lt;p&gt;No. Lo complementa. El inbox sigue siendo util para verificar contenido, asunto o links. Lo que cambia es que dejas de usarlo como fuente unica de verdad, que es donde casi siempre empiezan los problemas.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Sirve solo para FastAPI?
&lt;/h3&gt;

&lt;p&gt;Para nada. FastAPI lo hace facil por que el contrato queda muy claro en la API, pero la misma idea funciona en cualquier backend con colas, workers y correos de confirmacion.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>backend</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
