<?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: Franchesco Romero</title>
    <description>The latest articles on DEV Community by Franchesco Romero (@elchesco_).</description>
    <link>https://dev.to/elchesco_</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1636392%2Fc4a5090b-c32c-48a0-884c-1d8299fa6fb7.JPG</url>
      <title>DEV Community: Franchesco Romero</title>
      <link>https://dev.to/elchesco_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/elchesco_"/>
    <language>en</language>
    <item>
      <title>Coordinar deploys de frontend y backend sin orquestado, usando Github Actions</title>
      <dc:creator>Franchesco Romero</dc:creator>
      <pubDate>Thu, 28 May 2026 16:52:34 +0000</pubDate>
      <link>https://dev.to/aws-builders/coordinar-deploys-de-frontend-y-backend-sin-orquestado-usando-github-actions-4fp5</link>
      <guid>https://dev.to/aws-builders/coordinar-deploys-de-frontend-y-backend-sin-orquestado-usando-github-actions-4fp5</guid>
      <description>&lt;h2&gt;
  
  
  El Setup
&lt;/h2&gt;

&lt;p&gt;Un setup chiquito de SPA + API donde dos workflows de GitHub Actions&lt;br&gt;
salen en paralelo en cada push a &lt;code&gt;main&lt;/code&gt;. Probablemente un setup que no usaría en prod, pero algo que si uso para mis proyectos personales.&lt;/p&gt;

&lt;p&gt;Ahora bien esto trae un problema de coordinación (el frontend llega a los usuarios antes de que exista el endpoint de la API), cuatro opciones, y el gate de ~80 líneas de bash que fue el ganador para nuestro caso de uso&lt;/p&gt;

&lt;p&gt;El codebase: una SPA de React en CloudFront + S3, un backend FastAPI en AWS ECS Fargate, infraestructura en CDK. &lt;br&gt;
Dos workflows (&lt;code&gt;deploy-frontend.yml&lt;/code&gt;, &lt;code&gt;deploy-backend.yml&lt;/code&gt;) disparados por push a &lt;code&gt;main&lt;/code&gt; con path filters.&lt;/p&gt;

&lt;p&gt;Acá el código para seguir el post paso a paso:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/elchesco" rel="noopener noreferrer"&gt;
        elchesco
      &lt;/a&gt; / &lt;a href="https://github.com/elchesco/github-actions-combined-deploy-with-no-orchestrator" rel="noopener noreferrer"&gt;
        github-actions-combined-deploy-with-no-orchestrator
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Coordinating frontend and backend deploys without an orchestrator:
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Code companion — frontend/backend deploy coordination post&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Each folder maps to one stage of the post's narrative:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;00-independent/         starting point — two workflows, no gate, the race exists
01-coupled-detection/   git diff vs the parent commit to know if backend changed
02-poll-by-sha/         GitHub Actions API filtered by head_sha
03-grace-window/        handle "backend run not registered yet" (race fix)
04-final/               full step with meaningful error surfacing
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Suggested reading order&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00-independent/&lt;/code&gt;&lt;/strong&gt; — the baseline; what most projects start with.&lt;/li&gt;
&lt;li&gt;Each stage folder in order — the post's evolution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;04-final/&lt;/code&gt;&lt;/strong&gt; — drop this into your &lt;code&gt;deploy-frontend.yml&lt;/code&gt; as
the first step inside &lt;code&gt;build-and-deploy&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Notes&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;00-independent/&lt;/code&gt; is two separate files — they exist as &lt;code&gt;.yml&lt;/code&gt; so
you can diff them against your own workflows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;04-final/&lt;/code&gt; contains the complete step block plus the workflow
permissions and checkout config it depends on. Copy all three
pieces or the gate will silently fail open.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;actions: read&lt;/code&gt; permission is…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/elchesco/github-actions-combined-deploy-with-no-orchestrator" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;El approach simple aguanta mientras tu push esté dominado por&lt;br&gt;
cambios de un solo dominio. Deja de funcionar cuando la mayoría de los pushes tocan los dos o más.&lt;/p&gt;
&lt;h2&gt;
  
  
  El problema
&lt;/h2&gt;

&lt;p&gt;Dos workflows en la misma branch &lt;code&gt;main&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# deploy-frontend.yml&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;frontend/**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# deploy-backend.yml&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend/**"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;infra/**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un push que toca los dos — digamos, "agrega el endpoint&lt;br&gt;
&lt;code&gt;/api/v1/leaderboard&lt;/code&gt; y la UI que lo llama" — dispara ambos workflows&lt;br&gt;
en paralelo. Los builds de frontend normalmente son más rápidos (sin&lt;br&gt;
Docker, sin rollout de ECS). Así que un usuario que refresca entre el&lt;br&gt;
minuto 2 (SPA subida) y el minuto 6 (backend sano en ECS) pega contra&lt;br&gt;
una SPA nuevecita apuntando a un endpoint que todavía no existe. La&lt;br&gt;
consola del browser muestra un 404. Sentry pega un brinco, se disparan alertas de Cloudwatch, el usuario recarga, pega contra el caché, y ve el mismo error.&lt;/p&gt;

&lt;p&gt;Difícil de cachar en &lt;code&gt;dev&lt;/code&gt; porque el stack local arranca los dos servicios juntos. Fácil de cachar en prod una vez que pasa.&lt;/p&gt;
&lt;h2&gt;
  
  
  Las opciones
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Opción 1: workflows paralelos independientes (el baseline de no hacer nada)
&lt;/h3&gt;

&lt;p&gt;Con lo que arrancamos. Cada workflow escucha sus propios paths y&lt;br&gt;
despliega en su propio tiempo. Cero coordinación.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ambos workflows&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;...&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pros: &lt;br&gt;
cero setup, deploys de un solo dominio lo más rápido posible,&lt;br&gt;
aislamiento total. &lt;br&gt;
Contras: &lt;br&gt;
la ventana de race de arriba. El costo solo aparece en los pushes acoplados.&lt;/p&gt;
&lt;h3&gt;
  
  
  Opción 2: colapsar en un solo workflow con orden explícito
&lt;/h3&gt;

&lt;p&gt;El fix más "obvio": escribir un &lt;code&gt;deploy.yml&lt;/code&gt; con &lt;code&gt;deploy-backend&lt;/code&gt; como &lt;code&gt;job 1&lt;/code&gt; y &lt;code&gt;deploy-frontend&lt;/code&gt; con &lt;code&gt;needs: [deploy-backend]&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy-backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self-hosted&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;...&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;deploy-frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;deploy-backend&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self-hosted&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;...&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pros: &lt;br&gt;
modelo mental trivial; GitHub Actions maneja el orden. &lt;br&gt;
Contras:&lt;br&gt;
un push de solo-frontend ahora espera por un backend que no cambió (o&lt;br&gt;
necesita un &lt;code&gt;if:&lt;/code&gt; explícito para saltárselo, que es su propia&lt;br&gt;
complejidad). Un step de lint flaky en el backend bloquea el deploy de frontend que ni siquiera dependía de los cambios del backend. Perdimos la independencia de path filters que hacía deseables los workflows paralelos para empezar.&lt;/p&gt;
&lt;h3&gt;
  
  
  Opción 3: el frontend &lt;em&gt;gatea&lt;/em&gt; contra el SHA del backend (el gate simple)
&lt;/h3&gt;

&lt;p&gt;Conservar los dos workflows. Agregar un step al inicio del workflow de frontend: &lt;em&gt;si este commit también tocó backend, espera a que el&lt;br&gt;
workflow de backend en el mismo SHA termine bien&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Pros: &lt;br&gt;
cero overhead en pushes desacoplados (el caso común); se conserva&lt;br&gt;
el paralelismo del caso común; el gate son ~80 líneas de bash + un&lt;br&gt;
&lt;code&gt;curl&lt;/code&gt; a la API de GitHub Actions. Sin infra nueva, sin servicio&lt;br&gt;
orquestador, sin artifact pinning.&lt;/p&gt;

&lt;p&gt;Contras: &lt;br&gt;
bash. Polling. El gate corre en el runner del frontend, así&lt;br&gt;
que cuesta minutos de runner mientras espera (gratis en self-hosted,&lt;br&gt;
facturable en &lt;code&gt;ubuntu-latest&lt;/code&gt;).&lt;/p&gt;
&lt;h3&gt;
  
  
  Opción 4: pin del frontend a un artifact buildeado del backend
&lt;/h3&gt;

&lt;p&gt;La respuesta correcta de principio: cada build de frontend embebe la&lt;br&gt;
versión de backend contra la que se construyó; la SPA se niega a llamar una API que no haga match.&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="c1"&gt;// frontend&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REQUIRED_API_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026.05.27.a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// inyectado en el build&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;apiHealthcheck&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;REQUIRED_API_VERSION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;showStaleBanner&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pros: &lt;br&gt;
cero ventana de race incluso con rollouts totalmente independientes; el cliente puede caer a un banner de "refresca para actualizar"; funciona durante rollbacks. &lt;br&gt;
Contras: &lt;br&gt;
cada cambio de endpoint se vuelve un contrato versionado; necesitas un endpoint de discovery &lt;code&gt;/api/version&lt;/code&gt; y lógica en la SPA para manejar el mismatch; coordinar across clientes móviles eventualmente cuesta más que el gate.&lt;/p&gt;
&lt;h3&gt;
  
  
  Cómo elegir
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Patrón de push&lt;/th&gt;
&lt;th&gt;Mejor opción&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pushes mayormente de un solo dominio&lt;/td&gt;
&lt;td&gt;Opción 3 (gate)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pushes mayormente acoplados&lt;/td&gt;
&lt;td&gt;Opción 2 (un solo workflow)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clientes de larga vida / móvil / offline / una app de escritorio que no puedes forzar a refrescar&lt;/td&gt;
&lt;td&gt;Opción 4 (contrato versionado)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nuestra distribución: ~80% solo-backend, ~15% solo-frontend, ~5%&lt;br&gt;
acoplado. La Opción 3 fue el match obvio. El resto del post es su&lt;br&gt;
evolución.&lt;/p&gt;
&lt;h2&gt;
  
  
  Etapa 0: el punto de partida
&lt;/h2&gt;

&lt;p&gt;Dos workflows, sin coordinación:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy-frontend.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Frontend&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;frontend/**"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.github/workflows/deploy-frontend.yml"&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self-hosted&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm install --frozen-lockfile &amp;amp;&amp;amp; pnpm build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws s3 sync dist s3://myapp-frontend&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws cloudfront create-invalidation --distribution-id $DIST_ID --paths '/*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Push acoplado → race → 404s en producción. &lt;br&gt;
Fix: &lt;em&gt;gatear&lt;/em&gt; el deploy de frontend contra el de backend cuando el commit cambió código de backend.&lt;/p&gt;
&lt;h2&gt;
  
  
  Etapa 1: detectar el push acoplado
&lt;/h2&gt;

&lt;p&gt;Antes de hacer nada más, el gate tiene que responder: &lt;em&gt;¿este commit de verdad tocó el backend?&lt;/em&gt; Si no, no esperamos, procede de inmediato y no desperdicies un minuto de runner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Necesitamos ≥ 2 commits para diffear contra el padre.&lt;/span&gt;
    &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Detect coupled push&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;set -euo pipefail&lt;/span&gt;
    &lt;span class="s"&gt;changed=$(git diff --name-only HEAD~1 HEAD 2&amp;gt;/dev/null || true)&lt;/span&gt;
    &lt;span class="s"&gt;if ! echo "$changed" | grep -qE "^(backend/|infra/|\.github/workflows/deploy-backend\.yml$)"; then&lt;/span&gt;
      &lt;span class="s"&gt;echo "No backend/infra changes — proceeding."&lt;/span&gt;
      &lt;span class="s"&gt;exit 0&lt;/span&gt;
    &lt;span class="s"&gt;fi&lt;/span&gt;
    &lt;span class="s"&gt;echo "Backend changes detected — gate engaged."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El patrón de paths espeja el bloque &lt;code&gt;paths:&lt;/code&gt; de &lt;code&gt;deploy-backend.yml&lt;/code&gt;&lt;br&gt;
exactito — si un path dispara el workflow de backend, el gate tiene que esperarlo. La diferencia entre los dos es la causa #1 de false&lt;br&gt;
proceeds, así que mantenlos pegaditos en el code review.&lt;/p&gt;

&lt;p&gt;El &lt;code&gt;fetch-depth: 2&lt;/code&gt; es la trampa — &lt;code&gt;actions/checkout@v5&lt;/code&gt; viene por&lt;br&gt;
default en shallow &lt;code&gt;1&lt;/code&gt;, y &lt;code&gt;git diff HEAD~1&lt;/code&gt; en un checkout de depth-1&lt;br&gt;
regresa nada en silencio, lo cual el script lee como "no hay cambios de backend — procede". (Pegamos contra esto en el primer deploy después de &lt;em&gt;shipear&lt;/em&gt; el gate. Cáchalo con un guard, no nomás con documentación.)&lt;/p&gt;
&lt;h2&gt;
  
  
  Etapa 2: hacer poll al run del backend por SHA
&lt;/h2&gt;

&lt;p&gt;Ahora sabemos que hay que esperar. El mecanismo es la REST API de&lt;br&gt;
GitHub Actions filtrada por &lt;code&gt;head_sha&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://api.github.com/repos/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_REPOSITORY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/actions/workflows/deploy-backend.yml/runs"&lt;/span&gt;
&lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"head_sha=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_SHA&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;per_page=1"&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 160&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;  &lt;span class="c"&gt;# techo de 40 min a 15s/poll&lt;/span&gt;
  &lt;span class="nv"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/vnd.github+json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.workflow_runs[0].status // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;conclusion&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.workflow_runs[0].conclusion // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"completed"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$conclusion&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
      &lt;/span&gt;success|skipped&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt;
      &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"::error::Backend &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;conclusion&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;esac&lt;/span&gt;
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;15
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"::error::Timed out waiting for backend deploy."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El filtro &lt;code&gt;head_sha&lt;/code&gt; es el eje de todo — regresa el run de &lt;em&gt;este commit exacto&lt;/em&gt;, no "el último run en &lt;code&gt;main&lt;/code&gt;", que haría race con un push de fast-follow.&lt;/p&gt;

&lt;p&gt;El permiso &lt;code&gt;actions: read&lt;/code&gt; se tiene que agregar al bloque &lt;code&gt;permissions:&lt;/code&gt; del workflow — sin eso la API regresa 403 y el gate&lt;br&gt;
falla open en silencio.&lt;/p&gt;
&lt;h2&gt;
  
  
  Etapa 3: manejar el "todavía no se registra"
&lt;/h2&gt;

&lt;p&gt;El primer run en prod reveló un race: el workflow de frontend puede&lt;br&gt;
arrancar antes de que GitHub haya registrado el run del workflow de&lt;br&gt;
backend en el mismo SHA. La API regresa &lt;code&gt;total_count: 0&lt;/code&gt;, el script lee "no hay run de backend en este SHA, procede", y ya volvimos al problema original del 404.&lt;/p&gt;

&lt;p&gt;Fix: una grace window. Dale a GitHub hasta 30s para registrar el run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Grace window — espera hasta 30s a que aparezca el run de backend.&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 6&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl ... | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.total_count'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break
  sleep &lt;/span&gt;5
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Los 30s son empíricos — medí unos cuantos pushes acoplados, el delay de registro siempre fue &amp;lt; 10s pero brincó a 18s una vez durante un&lt;br&gt;
incidente de GitHub. 30s es lo suficientemente generoso como para que&lt;br&gt;
no hagamos false-proceed; el timeout exterior de 40 minutos absorbe el costo.&lt;/p&gt;

&lt;p&gt;Si después de 30s el run sigue sin existir, el script &lt;em&gt;sí&lt;/em&gt; cae a "no&lt;br&gt;
hay run en este SHA — procede". Esa es la decisión correcta: o el&lt;br&gt;
workflow de backend no se disparó (el path filter excluyó los cambios), o GitHub está tan degradado que un deploy de frontend es el menor de nuestros problemas. No le busques tres pies al gato.&lt;/p&gt;
&lt;h2&gt;
  
  
  Etapa 4: superficies de error con sentido
&lt;/h2&gt;

&lt;p&gt;Dos modos de falla necesitan manejo explícito para que el dev que ve el build en rojo pueda actuar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. La API de GitHub regresa 4xx (auth, rate limit, etc.).&lt;/span&gt;
fetch_runs&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;response status body
  &lt;span class="nv"&gt;response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;%{http_code}"&lt;/span&gt; ...&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'$d'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"::error::GitHub API returned &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;curl -f&lt;/code&gt; sale con 22 sin body. El wrapper conserva el body para que el log de error diga "403 Forbidden: actions read permission missing" en lugar de "exit code 22, suerte".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 2. El deploy de backend falló — saca la conclusion tal cual.&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"::error::Backend deploy &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;conclusion&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Abortando el deploy de frontend para que la SPA nunca apunte a una API ausente."&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Los casos &lt;code&gt;failure|cancelled|timed_out&lt;/code&gt; todos colapsan a la misma&lt;br&gt;
acción (no deployar el frontend), pero imprimir la conclusion exacta te ahorra un click hacia el run del workflow de backend cuando estás&lt;br&gt;
investigando.&lt;/p&gt;

&lt;h2&gt;
  
  
  El resultado
&lt;/h2&gt;

&lt;p&gt;Un setup de dos workflows que:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cuesta &lt;strong&gt;0 segundos&lt;/strong&gt; en pushes desacoplados (el caso del 80%)&lt;/li&gt;
&lt;li&gt;Cuesta &lt;strong&gt;a lo mucho el wall time del deploy de backend&lt;/strong&gt; en pushes
acoplados&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falla closed&lt;/strong&gt; — si el deploy de backend falla, el frontend no
shipea (sin tormenta de 404s)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falla closed en el gate mismo&lt;/strong&gt; — mala respuesta de API, timeout,
permiso caído, todos salen con 1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Desde que lo shipeamos (medido sobre seis semanas):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;47 pushes acoplados deployados limpio&lt;/li&gt;
&lt;li&gt;3 pushes acoplados donde el gate cachó un deploy de backend fallido
antes de que el frontend saliera (habría sido un outage visible para
el usuario)&lt;/li&gt;
&lt;li&gt;0 casos de gate haciendo false-proceed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lo que NO ayudó
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Intentar detectar "cambios de endpoint de API" desde el diff. Un cambio en un campo de schema de Pydantic basta), y los false negatives aquí son peores que los false positives. El check de path por git-diff es suficientemente bueno.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancel-in-progress en el workflow de frontend.&lt;/strong&gt; Se ve atractivo matar el deploy de frontend en vuelo si llega un push nuevo — pero el cancel pasa a media S3-sync, dejando el bundle a medio subir. Combinado con un caché stale de CloudFront esto es &lt;em&gt;peor&lt;/em&gt; que el race condition original. Lo dejamos en &lt;code&gt;cancel-in-progress: false&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Qué sí ayudaría a futuro
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Mover la lógica del gate a un action reutilizable
(&lt;code&gt;actions/wait-for-workflow@v1&lt;/code&gt;). Las ~80 líneas de bash funcionan
pero están medio copy-pasteadas entre proyectos. &lt;/li&gt;
&lt;li&gt;Sacar el conteo de pushes acoplados a un dashboard.** Si el ratio se va del 5% hacia el 30%, el gate simple deja de ser la
herramienta correcta.&lt;/li&gt;
&lt;li&gt;Hacer el deploy de backend más rápido para que la espera sea más
corta cuando sí se active.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Lecciones
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Los path filters definen qué cosas tus workflows acuerdan que los
disparan. Mantenlos juntos y revísalos juntos.&lt;/li&gt;
&lt;li&gt;El filtro &lt;code&gt;head_sha&lt;/code&gt; en la API de Actions es lo más útil de todo
esto. Existe, es estable, está documentado, y convierte "¿en cuál run estoy esperando?" de un problema difícil a un solo query.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>githubactions</category>
      <category>ci</category>
    </item>
    <item>
      <title>Bajándole todos los minutos posibles al CI del backend con mas de 1000 tests</title>
      <dc:creator>Franchesco Romero</dc:creator>
      <pubDate>Tue, 26 May 2026 05:11:47 +0000</pubDate>
      <link>https://dev.to/aws-builders/bajandole-todos-los-minutos-posibles-al-ci-del-backend-con-mas-de-1000-tests-5ecm</link>
      <guid>https://dev.to/aws-builders/bajandole-todos-los-minutos-posibles-al-ci-del-backend-con-mas-de-1000-tests-5ecm</guid>
      <description>&lt;p&gt;Esta es la historia de muchas horas de trabajo en un hoyo continuo tratando de elegir optimizaciones entre package managers, caches, paralelismo, optimizaciones de PostgreSQL advisory locks, y el golpe de realidad de darme cuenta que el cuello de botella no era nada de lo que había estado optimizando, si no algo que no había analizado.&lt;/p&gt;

&lt;p&gt;Antes de empezar aca esta el código disponible en Github para seguir paso a paso:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/elchesco" rel="noopener noreferrer"&gt;
        elchesco
      &lt;/a&gt; / &lt;a href="https://github.com/elchesco/improve-ci-times" rel="noopener noreferrer"&gt;
        improve-ci-times
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      improve-ci-times
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Code companion — backend CI optimisation post&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Stand-alone, copy-pasteable files for every code block in
&lt;a href="https://github.com/elchesco/improve-ci-times/../blog-backend-ci-optimization.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/blog-backend-ci-optimization.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Each folder maps to one round of the blog narrative:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;00-starting-point/   the test job, Dockerfile, conftest before any work
01-uv/               drop pip for uv (Dockerfile + CI snippet)
02-buildkit/         BuildKit cache mounts + setup-buildx + --push
03-xdist-traps/      per-worker DB + DATABASE_URL alignment
04-template-db/      Postgres template DB + pg_advisory_lock
                     (plus the filelock dead-end as documentation)
05-diagnostic/       the measurement step that broke the assumption
06-final/            final shape — 4 matrix shards, no xdist, cumulative
                     Dockerfile + complete conftest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Suggested reading order&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00-starting-point/&lt;/code&gt;&lt;/strong&gt; — what we had.&lt;/li&gt;
&lt;li&gt;Each round folder in order — the post's narrative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;06-final/&lt;/code&gt;&lt;/strong&gt; — the result.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Notes&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;The xdist code (&lt;code&gt;03&lt;/code&gt;, &lt;code&gt;04&lt;/code&gt;) is still present in &lt;code&gt;06-final/conftest.py&lt;/code&gt;
for local &lt;code&gt;pytest -n N&lt;/code&gt; runs even though the CI dropped xdist in
favour of serial matrix shards.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;04-template-db/filelock-attempt-DEAD-END.py&lt;/code&gt; is kept around…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/elchesco/improve-ci-times" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;El punto de partida&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;El codebase: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;un backend FastAPI (~50 routers, ~45 modelos SQLAlchemy)&lt;/li&gt;
&lt;li&gt;1826 tests, desplegado vía GitHub Actions a AWS ECS Fargate en
arm64.&lt;/li&gt;
&lt;li&gt;El CI corre en un self-hosted spot runner (4X concurrencia, en Graviton).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;En un inicio comenzamos con un paralelismo tradicional, nada del otro mundo, solo dos shards vía &lt;code&gt;pytest-split&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy-backend.yml — inicial&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd backend &amp;amp;&amp;amp; pip install --cache-dir "$RUNNER_TEMP/pip-cache" -r requirements-dev.txt&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tests (shard ${{ matrix.shard }}/2)&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;cd backend&lt;/span&gt;
    &lt;span class="s"&gt;pytest tests/ \&lt;/span&gt;
      &lt;span class="s"&gt;--splits 2 --group ${{ matrix.shard }} \&lt;/span&gt;
      &lt;span class="s"&gt;--cov=app --cov-report= \&lt;/span&gt;
      &lt;span class="s"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;docker build&lt;/code&gt; estándar. Y usando &lt;code&gt;pip&lt;/code&gt; para todo.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# backend/Dockerfile — inicial&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.11-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    libpq-dev gcc curl &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Intento no. 1: instalar dependencias más rápido con &lt;code&gt;uv&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;uv&lt;/code&gt; es el reemplazo más sencillo que existe de pip creado por Astral, normalmente de 10x hasta 100× más rápido para resolver e instalar paquetes. La migración es bastante sencilla ya que son dos&lt;br&gt;
líneas en el Dockerfile y listo:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Pin vía la imagen multi-stage oficial para que el binario sea reproducible&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ghcr.io/astral-sh/uv:0.5.11 /uv /uvx /usr/local/bin/&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;requirements.txt&lt;/code&gt; y &lt;code&gt;requirements-dev.txt&lt;/code&gt; quedan exactamente iguales, &lt;code&gt;uv pip&lt;/code&gt; los lee nativo. No hay que migrar forzosamente a &lt;code&gt;pyproject.toml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Y en CI, le implementamos la action correspondiente:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v6&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.5.11"&lt;/span&gt;
    &lt;span class="na"&gt;enable-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;cache-suffix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shard-${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;matrix.shard&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd backend &amp;amp;&amp;amp; uv pip install --system -r requirements-dev.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;En este caso el &lt;code&gt;cache-suffix&lt;/code&gt; por shard replica el workaround del &lt;code&gt;--cache-dir&lt;/code&gt; por shard que teníamos con pip — sin él, los dos shards que caen en el mismo runner self-hosted se pelean por el mismo tarball y uno de los dos se muere con &lt;code&gt;tar exit code 2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;La comparativa, en números:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ time uv pip install -r requirements.txt
...
uv pip install -r requirements.txt  1.23s user 1.59s system 51% cpu 5.509 total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;5.5s contra ~60s con &lt;code&gt;pip&lt;/code&gt; y casi 80s en el runner. &lt;/p&gt;
&lt;h2&gt;
  
  
  Intento no. 2: BuildKit cache mounts en el Dockerfile
&lt;/h2&gt;

&lt;p&gt;El layer cache de Docker solo ayuda cuando el &lt;code&gt;COPY&lt;/code&gt; no invalida las&lt;br&gt;
layers de abajo. Cualquier cambio en &lt;code&gt;requirements.txt&lt;/code&gt; re construye&lt;br&gt;
todo lo que sigue. Los &lt;strong&gt;BuildKit cache mounts&lt;/strong&gt; ayudan a persistir el contenido entre builds sin importar la invalidación de layers:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# backend/Dockerfile — con cache mounts&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/var/cache/apt,sharing&lt;span class="o"&gt;=&lt;/span&gt;locked &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/var/lib/apt,sharing&lt;span class="o"&gt;=&lt;/span&gt;locked &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get update &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get upgrade &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        libpq-dev gcc curl

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/root/.cache/uv &lt;span class="se"&gt;\
&lt;/span&gt;    uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Algo importante a mencionar:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Quitar el &lt;code&gt;rm -rf /var/lib/apt/lists&lt;/code&gt;.&lt;/strong&gt; Ya que servía para
reducir el tamaño de la imagen, pero con cache mounts BuildKit es
dueño de esos paths y los limpia entre builds sin hacer nada.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sharing=locked&lt;/code&gt;&lt;/strong&gt; serializa lecturas concurrentes. Sin eso, dos
builds en paralelo en el mismo runner pueden corromper el caché.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El runner que en este caso es self-hosted trae el builder legacy de Docker por default, y en el primer push después del commit de cache mounts se rompió con:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the --mount option requires BuildKit. Refer to
https://docs.docker.com/go/buildkit/ to learn how to build images with
BuildKit enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Lo arreglamos, de lo más fácil: instalar buildx y alias de &lt;code&gt;docker build&lt;/code&gt; a &lt;code&gt;docker buildx build&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Docker Buildx&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-buildx-action@v3&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;El &lt;code&gt;install: true&lt;/code&gt; es la opción necesaria. Sin eso, &lt;code&gt;docker build&lt;/code&gt; sigue usando el builder legacy.&lt;/p&gt;

&lt;p&gt;Buildx no carga al daemon local por default, así que el step de&lt;br&gt;
&lt;code&gt;docker push&lt;/code&gt; que ya teníamos también dejó de funcionar. Cambiamos a&lt;br&gt;
&lt;code&gt;--push&lt;/code&gt; directo:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;cd backend&lt;/span&gt;
    &lt;span class="s"&gt;docker build \&lt;/span&gt;
      &lt;span class="s"&gt;--cache-from $IMAGE:latest \&lt;/span&gt;
      &lt;span class="s"&gt;--cache-to type=inline \&lt;/span&gt;
      &lt;span class="s"&gt;-t $IMAGE:sha-$TAG \&lt;/span&gt;
      &lt;span class="s"&gt;-t $IMAGE:latest \&lt;/span&gt;
      &lt;span class="s"&gt;--push \&lt;/span&gt;
      &lt;span class="s"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;El &lt;code&gt;--cache-to type=inline&lt;/code&gt; mete la metadata del layer cache dentro&lt;br&gt;
de la imagen, cojn estoel &lt;code&gt;--cache-from&lt;/code&gt; del siguiente build lo jala de regreso el ECR.&lt;/p&gt;

&lt;p&gt;El primer build todavía viene con el costo de instalación de las dependencias y paquetes; los builds siguientes con los mismos &lt;code&gt;requirements&lt;/code&gt; se lo brincan. Pasando de &lt;code&gt;~40s&lt;/code&gt; a &lt;code&gt;~10s&lt;/code&gt; en cache hits.&lt;/p&gt;
&lt;h2&gt;
  
  
  Intento no. 3: la trampa de &lt;code&gt;pytest-xdist&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;El instinto es pensar que entre mas shards, podemos ahorrarnos mas tiempo, es decir 2 shards × 4 workers = 8 procesos en paralelo.&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="c1"&gt;# backend/requirements-dev.txt
&lt;/span&gt;&lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;xdist&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mf"&gt;3.6&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy-backend.yml&lt;/span&gt;
&lt;span class="s"&gt;pytest tests/ \&lt;/span&gt;
  &lt;span class="s"&gt;--splits 2 --group ${{ matrix.shard }} \&lt;/span&gt;
  &lt;span class="s"&gt;-n 4 --dist worksteal \&lt;/span&gt;
  &lt;span class="s"&gt;--cov=app --cov-report= \&lt;/span&gt;
  &lt;span class="s"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Pero, es aquí es donde empieza el dilema.&lt;/p&gt;
&lt;h3&gt;
  
  
  Trampa 1: &lt;code&gt;DROP SCHEMA&lt;/code&gt; entre workers
&lt;/h3&gt;

&lt;p&gt;En mi configuración el &lt;code&gt;conftest&lt;/code&gt; remueve y recrea el schema al inicio de cada session:&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="nd"&gt;@pytest_asyncio.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autouse&lt;/span&gt;&lt;span class="o"&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup_db&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;with&lt;/span&gt; &lt;span class="n"&gt;test_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DROP SCHEMA public CASCADE&lt;/span&gt;&lt;span class="sh"&gt;"&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CREATE SCHEMA public&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;with&lt;/span&gt; &lt;span class="n"&gt;test_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;scope="session"&lt;/code&gt; significa una vez por session de &lt;code&gt;pytest&lt;/code&gt;. Con&lt;br&gt;
&lt;code&gt;pytest-xdist&lt;/code&gt;, cada worker es su propia session.  &lt;/p&gt;

&lt;p&gt;Los cuatro workers apuntando a la misma DB &lt;code&gt;myapp_test&lt;/code&gt; se la pasan removiendo el schema de los demás y en ocasiones nos quedamos a media corrida.&lt;/p&gt;

&lt;p&gt;Fix: una DB por worker, con un sufijo de &lt;code&gt;PYTEST_XDIST_WORKER&lt;/code&gt;:&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;def&lt;/span&gt; &lt;span class="nf"&gt;_suffix_dburl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&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;worker&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;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;url_part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;query&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;?&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;url_part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;url_part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url_part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;_XDIST_WORKER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PYTEST_XDIST_WORKER&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;_XDIST_WORKER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&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;TEST_DATABASE_URL&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;val&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="nf"&gt;_suffix_dburl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_XDIST_WORKER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Con esto el worker &lt;code&gt;gw0&lt;/code&gt; agarra &lt;code&gt;myapp_test_gw0&lt;/code&gt;, &lt;code&gt;gw1&lt;/code&gt; agarra &lt;code&gt;_gw1&lt;/code&gt;, y así de manera secuencial.&lt;/p&gt;
&lt;h3&gt;
  
  
  Trampa 2: &lt;code&gt;max_locks_per_transaction&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Primera corrida con &lt;code&gt;-n auto&lt;/code&gt; (10 workers en el runner self-hosted):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;================== 31 passed, 11 warnings, 8 errors in 14.73s ==================
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;DROP SCHEMA CASCADE&lt;/code&gt; sobre ~50 tablas toma un relation-level lock por cada una. &lt;/p&gt;

&lt;p&gt;10 workers x 50 = 500 locks. &lt;/p&gt;

&lt;p&gt;El default de PostgreSQL para &lt;code&gt;max_locks_per_transaction&lt;/code&gt; es 64.&lt;/p&gt;

&lt;p&gt;Como lo arreglamos, bajamos el limite/cap a 4 workers por shard en vez de auto. 4 x 2 shards = 8 procesos paralelos.&lt;/p&gt;
&lt;h3&gt;
  
  
  Trampa 3: la divergencia del &lt;code&gt;DATABASE_URL&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Después de limitar a los workers, un test empezó a fallar en CI:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_comp_expiry_worker_skips_stripe_managed
  sqlalchemy.exc.ProgrammingError: relation "subscriptions" does not
  exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;¿Por qué? El test invoca un background 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;test_comp_expiry_worker_skips_stripe_managed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;member&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;_seed_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TEST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sub&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;_seed_sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TEST&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="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.workers.comp_expiry_worker&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;expire_comp_subscriptions&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expire_comp_subscriptions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# ← abre su propio AsyncSessionLocal
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Fix: alinear &lt;code&gt;DATABASE_URL&lt;/code&gt; con &lt;code&gt;TEST_DATABASE_URL&lt;/code&gt; &lt;em&gt;antes&lt;/em&gt; de que&lt;br&gt;
&lt;code&gt;app.main&lt;/code&gt; importe lo que sea:&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="c1"&gt;# backend/tests/conftest.py — top del archivo, antes de importar app.main
&lt;/span&gt;&lt;span class="n"&gt;_test_db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TEST_DATABASE_URL&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;_test_db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&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;_test_db&lt;/span&gt;

&lt;span class="n"&gt;_XDIST_WORKER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PYTEST_XDIST_WORKER&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;_XDIST_WORKER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&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;TEST_DATABASE_URL&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;val&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="nf"&gt;_suffix_dburl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_XDIST_WORKER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.main&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;  &lt;span class="c1"&gt;# ← engine construido con la URL correcta desde el inicio
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Intento no. 4: template database de PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Cada worker todavía usa un &lt;code&gt;DROP SCHEMA&lt;/code&gt; + &lt;code&gt;CREATE TABLE&lt;/code&gt; × 50 al&lt;br&gt;
inicio de la session. &lt;/p&gt;

&lt;p&gt;En el runner ARM eso son ~5 segundos por worker.&lt;/p&gt;

&lt;p&gt;PostgreSQL tiene un truco: &lt;code&gt;CREATE DATABASE ... TEMPLATE&lt;/code&gt;&lt;br&gt;
clona una DB vía copia a nivel de archivo en ~100ms en lugar de&lt;br&gt;
ejecutar todo el SQL. &lt;/p&gt;

&lt;p&gt;Construyes el schema una vez en una DB template dedicada, y luego cada worker la clona:&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="c1"&gt;# backend/tests/conftest.py
&lt;/span&gt;&lt;span class="n"&gt;TEMPLATE_DB_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;myapp_test_template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;_TEMPLATE_LOCK_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7321456789012345&lt;/span&gt;  &lt;span class="c1"&gt;# int arbitrario 
&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;_ensure_template_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin_url&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_url&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;admin_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;admin_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isolation_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUTOCOMMIT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;poolclass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NullPool&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;admin_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="o"&gt;=&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT 1 FROM pg_database WHERE datname = :n&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TEMPLATE_DB_NAME&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;scalar_one_or_none&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;exists&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&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;CREATE DATABASE &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TEMPLATE_DB_NAME&lt;/span&gt;&lt;span class="si"&gt;}&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;finally&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;admin_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;tmpl_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;poolclass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NullPool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tmpl_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;has_schema&lt;/span&gt; &lt;span class="o"&gt;=&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT 1 FROM information_schema.tables &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHERE table_schema = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; AND table_name = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;users&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LIMIT 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="nf"&gt;scalar_one_or_none&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;has_schema&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;with&lt;/span&gt; &lt;span class="n"&gt;tmpl_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&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;tmpl_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&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;_clone_template_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin_url&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;dbname&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;admin_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;admin_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isolation_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUTOCOMMIT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;poolclass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NullPool&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;admin_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Matar conexiones stale para que el DROP no se bloquee
&lt;/span&gt;            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT pg_terminate_backend(pid) FROM pg_stat_activity &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHERE datname = :n AND pid &amp;lt;&amp;gt; pg_backend_pid()&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dbname&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&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;DROP DATABASE IF EXISTS &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&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;CREATE DATABASE &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="si"&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TEMPLATE_DB_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;
            &lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;finally&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;admin_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Trampa 4: &lt;code&gt;filelock&lt;/code&gt; se cuelga
&lt;/h3&gt;

&lt;p&gt;Los workers se pelean por crear el template. El primer intento usaba&lt;br&gt;
&lt;code&gt;filelock&lt;/code&gt;:&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="c1"&gt;# No hagas esto
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;filelock&lt;/span&gt;
&lt;span class="n"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filelock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FileLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/myapp_test_template.lock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&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;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Los workers consistentemente se quedan colgados y llegan a el timeout de 120s. &lt;/p&gt;

&lt;p&gt;Con un cambio a un advisory lock de PostgreSQL, se soluciona. Es el mismo recurso compartido que ya necesitamos, se auto libera al cerrar la conexión:&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;_setup_db_via_template&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;split&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_split_db_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_DATABASE_URL&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;split&lt;/span&gt; &lt;span class="ow"&gt;is&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;return&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;split&lt;/span&gt;
    &lt;span class="n"&gt;admin_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/postgres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;template_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TEMPLATE_DB_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;admin_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;admin_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isolation_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUTOCOMMIT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;poolclass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NullPool&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;admin_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Bloquea hasta conseguirlo; se auto-libera al cerrar la conexión
&lt;/span&gt;            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT pg_advisory_lock(:id)&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;_TEMPLATE_LOCK_ID&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_ensure_template_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;finally&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT pg_advisory_unlock(:id)&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;_TEMPLATE_LOCK_ID&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&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;admin_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&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;_clone_template_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;El primer worker que agarra el lock construye el template (~5s); los demás esperan y luego clonan en ~100ms cada uno.&lt;/p&gt;

&lt;p&gt;Después de todo esto tenemos DB por worker, alineación de &lt;code&gt;DATABASE_URL&lt;/code&gt;, template clones, advisory locks — corriendo un smoke local de 7 archivos, notamos la diferencia:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;88 passed, 5 warnings in 12.28s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Bajó de ~2 minutos con el setup inicial a unos cuantos segundos. &lt;/p&gt;
&lt;h2&gt;
  
  
  Intento no. 5: la medición que rompió el supuesto
&lt;/h2&gt;

&lt;p&gt;El wall time por shard seguía como en 7 minutos. &lt;br&gt;
El CI total en 11m una mejora modesta sobre el baseline inicial, pero no la bajada dramática que sugería el smoke local.&lt;/p&gt;

&lt;p&gt;Hora de medir en serio. &lt;br&gt;
Agregué un step diagnóstico que corre una vez y reporta tiempos:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Diagnose pytest startup cost&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;matrix.shard == &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;cd backend&lt;/span&gt;
    &lt;span class="s"&gt;echo "::group::A — solo collection de pytest, sin coverage"&lt;/span&gt;
    &lt;span class="s"&gt;time pytest tests/ --co -q --no-header --no-cov&lt;/span&gt;
    &lt;span class="s"&gt;echo "::endgroup::"&lt;/span&gt;
    &lt;span class="s"&gt;echo "::group::B — solo collection de pytest CON coverage"&lt;/span&gt;
    &lt;span class="s"&gt;time pytest tests/ --co -q --no-header --cov=app --cov-report=&lt;/span&gt;
    &lt;span class="s"&gt;echo "::endgroup::"&lt;/span&gt;
    &lt;span class="s"&gt;echo "::group::C — corrida chiquita serial, sin coverage, sin xdist"&lt;/span&gt;
    &lt;span class="s"&gt;time pytest tests/test_critical.py -q --no-header --no-cov&lt;/span&gt;
    &lt;span class="s"&gt;echo "::endgroup::"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Los resultados en el runner ARM self-hosted fueron:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A — solo collection, sin coverage          real    0m18.909s
B — solo collection CON coverage           real    0m23.810s
C — corrida chica serial, sin nada         real    0m21.481s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;La observación clave, hacer collection de 1826 tests y&lt;br&gt;
correr un solo archivo chico sin coverage tardan lo mismo.&lt;/p&gt;

&lt;p&gt;O sea, el costo no es la collection. &lt;br&gt;
No es coverage (solo 5s de diferencia). &lt;br&gt;
No es la ejecución de los tests. &lt;br&gt;
Es el &lt;strong&gt;startup de pytest + el import del conftest&lt;/strong&gt;. &lt;br&gt;
Específicamente el &lt;code&gt;from app.main import app&lt;/code&gt; en el conftest que jala ~45 modelos, ~50 routers, middleware, settings, todo de un jalón. Veinte segundos de cold import en este runner. &lt;/p&gt;

&lt;p&gt;Cada vez.&lt;/p&gt;

&lt;p&gt;Con &lt;code&gt;xdist&lt;/code&gt;, cada uno de los 4 workers paga este costo de 20s&lt;br&gt;
independiente.  &lt;/p&gt;

&lt;p&gt;Ahí estaban los ~80s perdidos.&lt;/p&gt;
&lt;h2&gt;
  
  
  Round 6: soltar &lt;code&gt;xdist&lt;/code&gt;, irse a 4 shards
&lt;/h2&gt;

&lt;p&gt;Si cada proceso de pytest usa 20s fijos de startup, la optimización&lt;br&gt;
más barata es usarlo menos veces. &lt;br&gt;
2 shards X 4 workers de xdist = 10 startups de pytest (2 controllers + 8 workers). &lt;br&gt;
4 shards X 1 proceso serial = 4 startups de pytest.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.runner || 'self-hosted' }}&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lint&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;shard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;4&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;myapp_test&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;appuser&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;testpass&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
        &lt;span class="s"&gt;--health-cmd pg_isready --health-interval 10s&lt;/span&gt;
        &lt;span class="s"&gt;--health-timeout 5s --health-retries 5&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.11"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.5.11"&lt;/span&gt;
        &lt;span class="na"&gt;enable-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;cache-suffix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shard-${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;matrix.shard&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd backend &amp;amp;&amp;amp; uv pip install --system -r requirements-dev.txt&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tests (shard ${{ matrix.shard }}/4)&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://someappuser:sometestpass@localhost:${{ job.services.postgres.ports[5432] }}/myapp_test&lt;/span&gt;
        &lt;span class="na"&gt;TEST_DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql+asyncpg://someappuser:sometestpass@localhost:${{ job.services.postgres.ports[5432] }}/myapp_test&lt;/span&gt;
        &lt;span class="na"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ci-test-secret-key-32bytes-minimum-length&lt;/span&gt;
        &lt;span class="na"&gt;ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
        &lt;span class="na"&gt;COVERAGE_FILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.coverage.${{ matrix.shard }}&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;cd backend&lt;/span&gt;
        &lt;span class="s"&gt;pytest tests/ \&lt;/span&gt;
          &lt;span class="s"&gt;--splits 4 --group ${{ matrix.shard }} \&lt;/span&gt;
          &lt;span class="s"&gt;--cov=app --cov-report= \&lt;/span&gt;
          &lt;span class="s"&gt;-q --no-header&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Cada shard corre ~450 tests en serial con un solo proceso de &lt;code&gt;pytest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sin fan-out de &lt;code&gt;xdist&lt;/code&gt;, sin re-import de &lt;code&gt;conftest&lt;/code&gt; por worker, sin temas de CPU en los cold imports. &lt;/p&gt;

&lt;p&gt;El runner self-hosted anuncia 4X de concurrencia disponible, así que los cuatro shards corren en paralelo.&lt;/p&gt;

&lt;p&gt;El coverage se extiende a cuatro archivos:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;coverage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.11"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.5.11"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv pip install --system coverage==7.6.1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/download-artifact@v5&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;coverage-*&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend/&lt;/span&gt;
        &lt;span class="na"&gt;merge-multiple&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;cd backend&lt;/span&gt;
        &lt;span class="s"&gt;coverage combine .coverage.1 .coverage.2 .coverage.3 .coverage.4&lt;/span&gt;
        &lt;span class="s"&gt;coverage report --fail-under=60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;También quité el &lt;code&gt;-v&lt;/code&gt; y lo cambié por &lt;code&gt;-q --no-header&lt;/code&gt;. Con &lt;code&gt;xdist&lt;/code&gt;, el &lt;code&gt;-v&lt;/code&gt; bufferea el output por worker hasta que termina un test, &lt;code&gt;-q&lt;/code&gt; tiene output instantáneo y muestra la salida de inmediato.&lt;/p&gt;
&lt;h2&gt;
  
  
  El resultado
&lt;/h2&gt;

&lt;p&gt;Corrida real del CI después del push:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ lint              in 1m6s
✓ test (1)          in 6m46s
✓ test (2)          in 6m44s
✓ test (3)          in 6m57s
✓ test (4)          in 6m57s
✓ coverage          in 10s
✓ build-and-deploy  in 1m53s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Timeline del test step del shard 1:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;16:31:44  inicio del step
16:32:57  [pytest-split] Running group 1/4   ← 1m13s adentro
16:33:25  ........... [ 15%]                  ← primer punto a 1m41s
16:34:18  ........... [ 45%]
16:37:04  ........... [ 91%]
16:37:47  ........... [100%]  472 passed in 5m21s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Tiempo al primer output: 1m13s vs 2m37s antes.&lt;/strong&gt; &lt;br&gt;
Más o menos a la mitad.&lt;/p&gt;

&lt;p&gt;CI total: 10m16s vs ~20m del baseline antes de cualquier optimización.&lt;/p&gt;
&lt;h2&gt;
  
  
  Otras cosas que probe y que definitivamente no ayudaron
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Precompile de pyc&lt;/strong&gt; (&lt;code&gt;python -m compileall&lt;/code&gt;). Medición local: 13.0s en frío vs 12.6s en caliente. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pytest-xdist --dist worksteal&lt;/code&gt;&lt;/strong&gt; está bueno cuando cada worker tiene un costo de setup parecido. Cuando el setup es ~20s y los tests son mayormente rápidos, el impuesto de startup por worker se come la ganancia de paralelismo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;filelock&lt;/code&gt; para serializar entre procesos.&lt;/strong&gt; No me serializaba bien los workers en mi setup. Me cambié a advisory locks de PG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El flag &lt;code&gt;-v&lt;/code&gt;.&lt;/strong&gt; Causaba 2 minutos de buffering de output bajo xdist sin beneficio en performance.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Qué sí ayudaría a futuro
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mejorar el conftest.&lt;/strong&gt; El &lt;code&gt;from app.main import app&lt;/code&gt; es el costo más grande en mi caso. La app importa cada router, cada modelo, cada service al arranque. Partirla (lazy router registration, o romper el import monolítico) bajaría a 20s de startup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Una segunda pasada  en los shards.&lt;/strong&gt; &lt;code&gt;pytest-split&lt;/code&gt; balancea por duración. Si un shard consistentemente va 30s atrás, re-balancea:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   pytest --splits 4 --group 1 --store-durations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;comitea un &lt;code&gt;.test_durations&lt;/code&gt; nuevo contra el que los futuros runs&lt;br&gt;
   se balancean.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sacar coverage del hot path.&lt;/strong&gt; Coverage solo agregó ~5s en ARM en nuestro benchmark, pero a ~5s × 4 shards = 20s ahorrados. Trade-off: o lo aceptas en el job de tests o agregas un job de coverage no-paralelo aparte. Nosotros lo dejamos en el path.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Lecciones
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mide antes de optimizar.&lt;/strong&gt; Varias horas de trabajo en xdist, template DBs y BuildKit fueron útiles, pero el verdadero unlock vino de un step diagnóstico de 30 segundos que me dijo que el cuello de botella era el startup de pytest, no nada de lo que llevaba atacando.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El pytest más rápido es uno que no inicias dos veces.&lt;/strong&gt; Cada invocación de pytest paga un costo de startup fijo. Con un conftest pesado, ese costo domina todo lo demás. Más paralelismo = más startups = más costo. Menos shards más grandes le ganan a más shards chiquitos pasado un umbral.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pytest-xdist&lt;/code&gt; no es gratis.&lt;/strong&gt; Funciona bien cuando el costo por test &amp;gt;&amp;gt; el costo de startup. Cuando el startup es 20s y los tests son de 500ms, la ecuación se invierte.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursos por worker necesitan aislamiento por worker.&lt;/strong&gt; El bug sutil fue que los background workers abrían su propio &lt;code&gt;AsyncSessionLocal&lt;/code&gt; apuntando a la DB equivocada. El fix no estaba en la aplicación — estaba en el conftest de tests, alineando las env vars antes de que la app importara nada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL tiene las primitivas.&lt;/strong&gt; Advisory locks para sincronizar entre procesos, &lt;code&gt;CREATE DATABASE ... TEMPLATE&lt;/code&gt; para bootstrap de schema. Las dos me salvaron de inventar mecanismos más débiles encima.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Los BuildKit cache mounts siguen sub-utilizados.&lt;/strong&gt; Dos líneas en un Dockerfile (&lt;code&gt;--mount=type=cache&lt;/code&gt; para los cachés de apt y uv/pip) bajaron los docker builds repetidos de ~40s a ~10s, pero solo después de cambiarse del builder legacy de Docker vía &lt;code&gt;setup-buildx-action&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Código completo disponible en Github:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/elchesco" rel="noopener noreferrer"&gt;
        elchesco
      &lt;/a&gt; / &lt;a href="https://github.com/elchesco/improve-ci-times" rel="noopener noreferrer"&gt;
        improve-ci-times
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      improve-ci-times
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Code companion — backend CI optimisation post&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Stand-alone, copy-pasteable files for every code block in
&lt;a href="https://github.com/elchesco/improve-ci-times/../blog-backend-ci-optimization.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/blog-backend-ci-optimization.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Each folder maps to one round of the blog narrative:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;00-starting-point/   the test job, Dockerfile, conftest before any work
01-uv/               drop pip for uv (Dockerfile + CI snippet)
02-buildkit/         BuildKit cache mounts + setup-buildx + --push
03-xdist-traps/      per-worker DB + DATABASE_URL alignment
04-template-db/      Postgres template DB + pg_advisory_lock
                     (plus the filelock dead-end as documentation)
05-diagnostic/       the measurement step that broke the assumption
06-final/            final shape — 4 matrix shards, no xdist, cumulative
                     Dockerfile + complete conftest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Suggested reading order&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;00-starting-point/&lt;/code&gt;&lt;/strong&gt; — what we had.&lt;/li&gt;
&lt;li&gt;Each round folder in order — the post's narrative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;06-final/&lt;/code&gt;&lt;/strong&gt; — the result.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Notes&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;The xdist code (&lt;code&gt;03&lt;/code&gt;, &lt;code&gt;04&lt;/code&gt;) is still present in &lt;code&gt;06-final/conftest.py&lt;/code&gt;
for local &lt;code&gt;pytest -n N&lt;/code&gt; runs even though the CI dropped xdist in
favour of serial matrix shards.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;04-template-db/filelock-attempt-DEAD-END.py&lt;/code&gt; is kept around…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/elchesco/improve-ci-times" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>githubactions</category>
      <category>ci</category>
      <category>python</category>
    </item>
    <item>
      <title>Bajándole todos los minutos posibles al CI del backend con mas de 1000 tests</title>
      <dc:creator>Franchesco Romero</dc:creator>
      <pubDate>Tue, 26 May 2026 05:11:47 +0000</pubDate>
      <link>https://dev.to/aws-builders/bajandole-todos-los-minutos-posibles-al-ci-del-backend-con-mas-de-1000-tests-7kg</link>
      <guid>https://dev.to/aws-builders/bajandole-todos-los-minutos-posibles-al-ci-del-backend-con-mas-de-1000-tests-7kg</guid>
      <description>&lt;p&gt;Esta es la historia de muchas horas de trabajo en un hoyo continuo tratando de elegir optimizaciones entre package managers, caches paralelismo, optimizaciones de PostgreSQL, advisory locks, y el golpe de realidad de darme cuenta que el cuello de botella no era nada de lo que había estado optimizando, si no algo que no había analizado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El punto de partida&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;El codebase: un backend FastAPI (~50 routers, ~45 modelos SQLAlchemy)&lt;br&gt;
con 1826 tests, desplegado vía GitHub Actions a AWS ECS Fargate en&lt;br&gt;
arm64. El CI corre en un self-hosted spot runner (4× concurrency,&lt;br&gt;
Graviton).&lt;/p&gt;

&lt;p&gt;En un inicio comenzamos con un paralelismo tradicional, nada del otro mundo, solo dos shards vía &lt;code&gt;pytest-split&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy-backend.yml — inicial&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd backend &amp;amp;&amp;amp; pip install --cache-dir "$RUNNER_TEMP/pip-cache" -r requirements-dev.txt&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tests (shard ${{ matrix.shard }}/2)&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;cd backend&lt;/span&gt;
    &lt;span class="s"&gt;pytest tests/ \&lt;/span&gt;
      &lt;span class="s"&gt;--splits 2 --group ${{ matrix.shard }} \&lt;/span&gt;
      &lt;span class="s"&gt;--cov=app --cov-report= \&lt;/span&gt;
      &lt;span class="s"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;docker build&lt;/code&gt; estándar. Y usando &lt;code&gt;pip&lt;/code&gt; para todo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# backend/Dockerfile — inicial&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.11-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    libpq-dev gcc curl &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Intento no. 1: instalar dependencias más rápido con uv
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;uv&lt;/code&gt; es el reemplazo más sencillo que existe de pip creado por Astral, normalmente de 10x hasta 100× más rápido para resolver e instalar paquetes. La migración es bastante sencilla ya que son dos&lt;br&gt;
líneas en el Dockerfile y listo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Pin vía la imagen multi-stage oficial para que el binario sea reproducible&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ghcr.io/astral-sh/uv:0.5.11 /uv /uvx /usr/local/bin/&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;requirements.txt&lt;/code&gt; y &lt;code&gt;requirements-dev.txt&lt;/code&gt; quedan exactamente iguales, &lt;code&gt;uv pip&lt;/code&gt; los lee nativo. No hay que migrar forzosamente a &lt;code&gt;pyproject.toml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Y en CI, le implementamos la action correspondiente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v6&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.5.11"&lt;/span&gt;
    &lt;span class="na"&gt;enable-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;cache-suffix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shard-${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;matrix.shard&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd backend &amp;amp;&amp;amp; uv pip install --system -r requirements-dev.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En este caso el &lt;code&gt;cache-suffix&lt;/code&gt; por shard replica el workaround del &lt;code&gt;--cache-dir&lt;/code&gt; por shard que teníamos con pip — sin él, los dos shards que caen en el mismo runner self-hosted se pelean por el mismo tarball y uno de los dos se muere con &lt;code&gt;tar exit code 2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;La comparativa, en números:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ time uv pip install -r requirements.txt
...
uv pip install -r requirements.txt  1.23s user 1.59s system 51% cpu 5.509 total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5.5s contra ~60s con &lt;code&gt;pip&lt;/code&gt; y casi 80s en el runner. &lt;/p&gt;

&lt;h2&gt;
  
  
  Intento no. 2: BuildKit cache mounts en el Dockerfile
&lt;/h2&gt;

&lt;p&gt;El layer cache de Docker solo ayuda cuando el &lt;code&gt;COPY&lt;/code&gt; no invalida las&lt;br&gt;
layers de abajo. Cualquier cambio en &lt;code&gt;requirements.txt&lt;/code&gt; re-construye&lt;br&gt;
todo lo que sigue. Los &lt;strong&gt;BuildKit cache mounts&lt;/strong&gt; ayudan a persistir el contenido entre builds sin importar la invalidación de layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# backend/Dockerfile — con cache mounts&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/var/cache/apt,sharing&lt;span class="o"&gt;=&lt;/span&gt;locked &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/var/lib/apt,sharing&lt;span class="o"&gt;=&lt;/span&gt;locked &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get update &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get upgrade &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        libpq-dev gcc curl

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/root/.cache/uv &lt;span class="se"&gt;\
&lt;/span&gt;    uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Algo importante a mencionar:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Quitar el &lt;code&gt;rm -rf /var/lib/apt/lists&lt;/code&gt;.&lt;/strong&gt; Ya que servía para
reducir el tamaño de la imagen, pero con cache mounts BuildKit es
dueño de esos paths y los limpia entre builds.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sharing=locked&lt;/code&gt;&lt;/strong&gt; serializa lecturas concurrentes. Sin eso, dos
builds en paralelo en el mismo runner pueden corromper el caché.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El runner que en este caso es self-hosted trae el builder legacy de Docker por default, y el primer push después del commit de cache mounts se murió con:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the --mount option requires BuildKit. Refer to
https://docs.docker.com/go/buildkit/ to learn how to build images with
BuildKit enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lo arreglamos, de lo más fácil: instalar buildx y alias de &lt;code&gt;docker build&lt;/code&gt; a &lt;code&gt;docker buildx build&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Docker Buildx&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-buildx-action@v3&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;install: true&lt;/code&gt; es la opción crítica. Sin eso, &lt;code&gt;docker build&lt;/code&gt; sigue usando el builder legacy.&lt;/p&gt;

&lt;p&gt;Buildx no carga al daemon local por default, así que el step de&lt;br&gt;
&lt;code&gt;docker push&lt;/code&gt; que ya teníamos también dejó de funcionar. Cambio a&lt;br&gt;
&lt;code&gt;--push&lt;/code&gt; directo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;cd backend&lt;/span&gt;
    &lt;span class="s"&gt;docker build \&lt;/span&gt;
      &lt;span class="s"&gt;--cache-from $IMAGE:latest \&lt;/span&gt;
      &lt;span class="s"&gt;--cache-to type=inline \&lt;/span&gt;
      &lt;span class="s"&gt;-t $IMAGE:sha-$TAG \&lt;/span&gt;
      &lt;span class="s"&gt;-t $IMAGE:latest \&lt;/span&gt;
      &lt;span class="s"&gt;--push \&lt;/span&gt;
      &lt;span class="s"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;--cache-to type=inline&lt;/code&gt; mete la metadata del layer cache dentro&lt;br&gt;
de la imagen, cojn estoel &lt;code&gt;--cache-from&lt;/code&gt; del siguiente build lo jala de regreso el ECR.&lt;/p&gt;

&lt;p&gt;El primer build todavía paga el costo de instalación de las dependencias y paquetes; los builds siguientes con los mismos requirements se lo brincan. Pasando de &lt;code&gt;~40s&lt;/code&gt; a &lt;code&gt;~10s&lt;/code&gt; en cache hits.&lt;/p&gt;
&lt;h2&gt;
  
  
  Intento no. 3: la trampa de &lt;code&gt;pytest-xdist&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;El instinto, pensar que entre mas shards, podemos ahorrarnos mas tiempo, es decir 2 shards × 4 workers = 8 procesos en paralelo.&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="c1"&gt;# backend/requirements-dev.txt
&lt;/span&gt;&lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;xdist&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mf"&gt;3.6&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy-backend.yml&lt;/span&gt;
&lt;span class="s"&gt;pytest tests/ \&lt;/span&gt;
  &lt;span class="s"&gt;--splits 2 --group ${{ matrix.shard }} \&lt;/span&gt;
  &lt;span class="s"&gt;-n 4 --dist worksteal \&lt;/span&gt;
  &lt;span class="s"&gt;--cov=app --cov-report= \&lt;/span&gt;
  &lt;span class="s"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aquí es donde empieza el dilema.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trampa 1: &lt;code&gt;DROP SCHEMA&lt;/code&gt; entre workers
&lt;/h3&gt;

&lt;p&gt;El &lt;code&gt;conftest&lt;/code&gt; remueve y recrea el schema al inicio de cada session:&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="nd"&gt;@pytest_asyncio.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autouse&lt;/span&gt;&lt;span class="o"&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup_db&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;with&lt;/span&gt; &lt;span class="n"&gt;test_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DROP SCHEMA public CASCADE&lt;/span&gt;&lt;span class="sh"&gt;"&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CREATE SCHEMA public&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;with&lt;/span&gt; &lt;span class="n"&gt;test_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;scope="session"&lt;/code&gt; significa una vez por session de pytest. Con&lt;br&gt;
&lt;code&gt;pytest-xdist&lt;/code&gt;, cada worker es su propia session.  &lt;/p&gt;

&lt;p&gt;Los cuatro workers apuntando a la misma DB &lt;code&gt;myapp_test&lt;/code&gt; se la pasan removiendo el schema de los demás a media corrida.&lt;/p&gt;

&lt;p&gt;Fix: una DB por worker, con sufijo de &lt;code&gt;PYTEST_XDIST_WORKER&lt;/code&gt;:&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;def&lt;/span&gt; &lt;span class="nf"&gt;_suffix_dburl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&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;worker&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;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;url_part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;query&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;?&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;url_part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;url_part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url_part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;_XDIST_WORKER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PYTEST_XDIST_WORKER&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;_XDIST_WORKER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&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;TEST_DATABASE_URL&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;val&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="nf"&gt;_suffix_dburl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_XDIST_WORKER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El worker &lt;code&gt;gw0&lt;/code&gt; agarra &lt;code&gt;myapp_test_gw0&lt;/code&gt;, &lt;code&gt;gw1&lt;/code&gt; agarra &lt;code&gt;_gw1&lt;/code&gt;, y así de manera secuencial.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trampa 2: &lt;code&gt;max_locks_per_transaction&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Primera corrida con &lt;code&gt;-n auto&lt;/code&gt; (10 workers en el runner self-hosted):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;================== 31 passed, 11 warnings, 8 errors in 14.73s ==================
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DROP SCHEMA CASCADE&lt;/code&gt; sobre ~50 tablas toma un relation-level lock por cada una. &lt;/p&gt;

&lt;p&gt;10 workers × 50 = 500 locks. &lt;/p&gt;

&lt;p&gt;El default de PostgreSQL para &lt;code&gt;max_locks_per_transaction&lt;/code&gt; es 64&lt;/p&gt;

&lt;p&gt;Como lo arreglamos, bajamos el limite/cap a 4 workers por shard en vez de auto. 4 × 2 shards = 8 procesos paralelos.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trampa 3: la divergencia del &lt;code&gt;DATABASE_URL&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Después de limitar a los workers, un test empezó a fallar en CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_comp_expiry_worker_skips_stripe_managed
  sqlalchemy.exc.ProgrammingError: relation "subscriptions" does not
  exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;¿Por qué? El test invoca un background 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;test_comp_expiry_worker_skips_stripe_managed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;member&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;_seed_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TEST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sub&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;_seed_sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TEST&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="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.workers.comp_expiry_worker&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;expire_comp_subscriptions&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expire_comp_subscriptions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# ← abre su propio AsyncSessionLocal
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix: alinear &lt;code&gt;DATABASE_URL&lt;/code&gt; con &lt;code&gt;TEST_DATABASE_URL&lt;/code&gt; &lt;em&gt;antes&lt;/em&gt; de que&lt;br&gt;
&lt;code&gt;app.main&lt;/code&gt; importe lo que sea:&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="c1"&gt;# backend/tests/conftest.py — top del archivo, antes de importar app.main
&lt;/span&gt;&lt;span class="n"&gt;_test_db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TEST_DATABASE_URL&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;_test_db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&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;_test_db&lt;/span&gt;

&lt;span class="n"&gt;_XDIST_WORKER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PYTEST_XDIST_WORKER&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;_XDIST_WORKER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&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;TEST_DATABASE_URL&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;val&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="nf"&gt;_suffix_dburl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_XDIST_WORKER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.main&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;  &lt;span class="c1"&gt;# ← engine construido con la URL correcta desde el inicio
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Intento no. 4: template database de PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Cada worker todavía usa un &lt;code&gt;DROP SCHEMA&lt;/code&gt; + &lt;code&gt;CREATE TABLE&lt;/code&gt; × 50 al&lt;br&gt;
inicio de la session. &lt;br&gt;
En el runner ARM eso son ~5 segundos por worker.&lt;/p&gt;

&lt;p&gt;PostgreSQL tiene un truco: &lt;code&gt;CREATE DATABASE ... TEMPLATE&lt;/code&gt;&lt;br&gt;
clona una DB vía copia a nivel de archivo en ~100ms en lugar de&lt;br&gt;
ejecutar todo el SQL. &lt;/p&gt;

&lt;p&gt;Construyes el schema una vez en una DB template dedicada, y luego cada worker la clona:&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="c1"&gt;# backend/tests/conftest.py
&lt;/span&gt;&lt;span class="n"&gt;TEMPLATE_DB_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;myapp_test_template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;_TEMPLATE_LOCK_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7321456789012345&lt;/span&gt;  &lt;span class="c1"&gt;# int arbitrario 
&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;_ensure_template_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin_url&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_url&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;admin_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;admin_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isolation_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUTOCOMMIT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;poolclass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NullPool&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;admin_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="o"&gt;=&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT 1 FROM pg_database WHERE datname = :n&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TEMPLATE_DB_NAME&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;scalar_one_or_none&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;exists&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&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;CREATE DATABASE &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TEMPLATE_DB_NAME&lt;/span&gt;&lt;span class="si"&gt;}&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;finally&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;admin_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;tmpl_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;poolclass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NullPool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tmpl_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;has_schema&lt;/span&gt; &lt;span class="o"&gt;=&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT 1 FROM information_schema.tables &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHERE table_schema = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; AND table_name = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;users&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LIMIT 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="nf"&gt;scalar_one_or_none&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;has_schema&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;with&lt;/span&gt; &lt;span class="n"&gt;tmpl_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&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;tmpl_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&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;_clone_template_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin_url&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;dbname&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;admin_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;admin_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isolation_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUTOCOMMIT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;poolclass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NullPool&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;admin_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Matar conexiones stale para que el DROP no se bloquee
&lt;/span&gt;            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT pg_terminate_backend(pid) FROM pg_stat_activity &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHERE datname = :n AND pid &amp;lt;&amp;gt; pg_backend_pid()&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dbname&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&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;DROP DATABASE IF EXISTS &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&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;CREATE DATABASE &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="si"&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TEMPLATE_DB_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;
            &lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;finally&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;admin_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Trampa 4: &lt;code&gt;filelock&lt;/code&gt; se cuelga
&lt;/h3&gt;

&lt;p&gt;Los workers se pelean por crear el template. El primer intento usaba&lt;br&gt;
&lt;code&gt;filelock&lt;/code&gt;:&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="c1"&gt;# No hagas esto
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;filelock&lt;/span&gt;
&lt;span class="n"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filelock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FileLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/myapp_test_template.lock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&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;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Los workers consistentemente llegan a el timeout de 120s. &lt;/p&gt;

&lt;p&gt;Con un cambio a un advisory lock de PostgreSQL, se soluciona. Es el mismo recurso compartido que ya necesitamos, se auto libera al cerrar la conexión (así que un worker que truene no puede dejar el lock huérfano como pasa con file locks):&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;_setup_db_via_template&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;split&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_split_db_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_DATABASE_URL&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;split&lt;/span&gt; &lt;span class="ow"&gt;is&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;return&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;split&lt;/span&gt;
    &lt;span class="n"&gt;admin_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/postgres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;template_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TEMPLATE_DB_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;admin_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;admin_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isolation_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUTOCOMMIT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;poolclass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NullPool&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;admin_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Bloquea hasta conseguirlo; se auto-libera al cerrar la conexión
&lt;/span&gt;            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT pg_advisory_lock(:id)&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;_TEMPLATE_LOCK_ID&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_ensure_template_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;finally&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT pg_advisory_unlock(:id)&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;_TEMPLATE_LOCK_ID&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&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;admin_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&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;_clone_template_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El primer worker que agarra el lock construye el template (~5s); los demás esperan y luego clonan en ~100ms cada uno.&lt;/p&gt;

&lt;p&gt;Después de todo esto — DBs por worker, alineación de &lt;code&gt;DATABASE_URL&lt;/code&gt;,&lt;br&gt;
template clones, advisory locks — corriendo un smoke local de 7&lt;br&gt;
archivos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;88 passed, 5 warnings in 12.28s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bajó de ~2 minutos con el setup roto inicial de xdist. &lt;/p&gt;

&lt;h2&gt;
  
  
  Intento no. 5: la medición que rompió el supuesto
&lt;/h2&gt;

&lt;p&gt;El wall time por shard seguía como en 7 minutos. &lt;br&gt;
El CI total en 11m una mejora modesta sobre el baseline, no la bajada dramática que sugería el smoke local.&lt;/p&gt;

&lt;p&gt;Hora de medir en serio. Agregué un step diagnóstico que corre una vez&lt;br&gt;
y reporta tiempos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Diagnose pytest startup cost&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;matrix.shard == &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;cd backend&lt;/span&gt;
    &lt;span class="s"&gt;echo "::group::A — solo collection de pytest, sin coverage"&lt;/span&gt;
    &lt;span class="s"&gt;time pytest tests/ --co -q --no-header --no-cov&lt;/span&gt;
    &lt;span class="s"&gt;echo "::endgroup::"&lt;/span&gt;
    &lt;span class="s"&gt;echo "::group::B — solo collection de pytest CON coverage"&lt;/span&gt;
    &lt;span class="s"&gt;time pytest tests/ --co -q --no-header --cov=app --cov-report=&lt;/span&gt;
    &lt;span class="s"&gt;echo "::endgroup::"&lt;/span&gt;
    &lt;span class="s"&gt;echo "::group::C — corrida chiquita serial, sin coverage, sin xdist"&lt;/span&gt;
    &lt;span class="s"&gt;time pytest tests/test_critical.py -q --no-header --no-cov&lt;/span&gt;
    &lt;span class="s"&gt;echo "::endgroup::"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Los resultados en el runner ARM self-hosted fueron:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A — solo collection, sin coverage          real    0m18.909s
B — solo collection CON coverage           real    0m23.810s
C — corrida chica serial, sin nada         real    0m21.481s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La observación clave, hacer collection de 1826 tests y&lt;br&gt;
correr un solo archivo chico sin coverage tardan lo mismo.&lt;/p&gt;

&lt;p&gt;O sea, el costo no es la collection. No es coverage (solo 5s de&lt;br&gt;
diferencia). &lt;br&gt;
No es la ejecución de los tests. Es el &lt;strong&gt;startup de pytest + el import del conftest&lt;/strong&gt;. &lt;br&gt;
Específicamente el &lt;code&gt;from app.main import app&lt;/code&gt; en el conftest que jala ~45 modelos, ~50 routers, middleware, settings — todo de un jalón. Veinte segundos de cold-import en este runner. &lt;br&gt;
Cada vez.&lt;/p&gt;

&lt;p&gt;Con xdist, cada uno de los 4 workers paga este costo de 20s&lt;br&gt;
independiente. Spawn en paralelo, pero el runner solo tiene ciertos&lt;br&gt;
cores; los cold imports de modelos pydantic y mappers de SQLAlchemy se pelean por CPU. &lt;/p&gt;

&lt;p&gt;Ahí estaban los ~80s perdidos.&lt;/p&gt;
&lt;h2&gt;
  
  
  Round 6: soltar &lt;code&gt;xdist&lt;/code&gt;, irse a 4 shards
&lt;/h2&gt;

&lt;p&gt;Si cada proceso de pytest usa 20s fijos de startup, la optimización&lt;br&gt;
más barata es usarlo menos veces. &lt;br&gt;
2 shards × 4 workers de xdist = 10 startups de pytest (2 controllers + 8 workers). &lt;br&gt;
4 shards × 1 proceso serial = 4 startups de pytest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.runner || 'self-hosted' }}&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lint&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;shard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;4&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;myapp_test&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;appuser&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;testpass&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
        &lt;span class="s"&gt;--health-cmd pg_isready --health-interval 10s&lt;/span&gt;
        &lt;span class="s"&gt;--health-timeout 5s --health-retries 5&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.11"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.5.11"&lt;/span&gt;
        &lt;span class="na"&gt;enable-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;cache-suffix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shard-${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;matrix.shard&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd backend &amp;amp;&amp;amp; uv pip install --system -r requirements-dev.txt&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tests (shard ${{ matrix.shard }}/4)&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://someappuser:sometestpass@localhost:${{ job.services.postgres.ports[5432] }}/myapp_test&lt;/span&gt;
        &lt;span class="na"&gt;TEST_DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql+asyncpg://someappuser:sometestpass@localhost:${{ job.services.postgres.ports[5432] }}/myapp_test&lt;/span&gt;
        &lt;span class="na"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ci-test-secret-key-32bytes-minimum-length&lt;/span&gt;
        &lt;span class="na"&gt;ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
        &lt;span class="na"&gt;COVERAGE_FILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.coverage.${{ matrix.shard }}&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;cd backend&lt;/span&gt;
        &lt;span class="s"&gt;pytest tests/ \&lt;/span&gt;
          &lt;span class="s"&gt;--splits 4 --group ${{ matrix.shard }} \&lt;/span&gt;
          &lt;span class="s"&gt;--cov=app --cov-report= \&lt;/span&gt;
          &lt;span class="s"&gt;-q --no-header&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada shard corre ~450 tests en serial con un solo proceso de&lt;code&gt;pytest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sin fan-out de &lt;code&gt;xdist&lt;/code&gt;, sin re-import de &lt;code&gt;conftest&lt;/code&gt; por worker, sin contención de CPU en los cold imports. &lt;/p&gt;

&lt;p&gt;El runner self-hosted anuncia 4x de concurrencia disponible, así que los cuatro shards corren en paralelo.&lt;/p&gt;

&lt;p&gt;El coverage se extiende a cuatro archivos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;coverage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.11"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.5.11"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv pip install --system coverage==7.6.1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/download-artifact@v5&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;coverage-*&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend/&lt;/span&gt;
        &lt;span class="na"&gt;merge-multiple&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;cd backend&lt;/span&gt;
        &lt;span class="s"&gt;coverage combine .coverage.1 .coverage.2 .coverage.3 .coverage.4&lt;/span&gt;
        &lt;span class="s"&gt;coverage report --fail-under=60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;También quité el &lt;code&gt;-v&lt;/code&gt; y lo cambié por &lt;code&gt;-q --no-header&lt;/code&gt;. Con &lt;code&gt;xdist&lt;/code&gt;, el &lt;code&gt;-v&lt;/code&gt; bufferea el output por worker hasta que termina un test, &lt;code&gt;-q&lt;/code&gt; tiene output instantáneo y muestra la salida de inmediato.&lt;/p&gt;

&lt;h2&gt;
  
  
  El resultado
&lt;/h2&gt;

&lt;p&gt;Corrida real del CI después del push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ lint              in 1m6s
✓ test (1)          in 6m46s
✓ test (2)          in 6m44s
✓ test (3)          in 6m57s
✓ test (4)          in 6m57s
✓ coverage          in 10s
✓ build-and-deploy  in 1m53s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Timeline del test step del shard 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;16:31:44  inicio del step
16:32:57  [pytest-split] Running group 1/4   ← 1m13s adentro
16:33:25  ........... [ 15%]                  ← primer punto a 1m41s
16:34:18  ........... [ 45%]
16:37:04  ........... [ 91%]
16:37:47  ........... [100%]  472 passed in 5m21s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tiempo al primer output: 1m13s vs 2m37s antes.&lt;/strong&gt; &lt;br&gt;
Más o menos a la mitad.&lt;/p&gt;

&lt;p&gt;CI total: 10m16s vs ~20m del baseline antes de cualquier optimización.&lt;/p&gt;

&lt;h2&gt;
  
  
  Otras cosas que probe y que definitivamente no ayudaron
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Precompile de pyc&lt;/strong&gt; (&lt;code&gt;python -m compileall&lt;/code&gt;). Medición local: 13.0s en frío vs 12.6s en caliente. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pytest-xdist --dist worksteal&lt;/code&gt;&lt;/strong&gt; está bueno cuando cada worker tiene un costo de setup parecido. Cuando el setup es ~20s y los tests son mayormente rápidos, el impuesto de startup por worker se come la ganancia de paralelismo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;filelock&lt;/code&gt; para serializar entre procesos.&lt;/strong&gt; No me serializaba bien los workers en mi setup. Me cambié a advisory locks de PG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El flag &lt;code&gt;-v&lt;/code&gt;.&lt;/strong&gt; Causaba 2 minutos de buffering de output bajo xdist sin beneficio en performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Qué sí ayudaría a futuro
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mejorar el conftest.&lt;/strong&gt; El &lt;code&gt;from app.main import app&lt;/code&gt; es el costo más grande en mi caso. La app importa cada router, cada modelo, cada service al arranque. Partirla (lazy router registration, o romper el import monolítico) bajaría a 20s de startup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Una segunda pasada  en los shards.&lt;/strong&gt; &lt;code&gt;pytest-split&lt;/code&gt;
balancea por duración. Si un shard consistentemente va 30s atrás,
re-balancea:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   pytest --splits 4 --group 1 --store-durations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;comitea un &lt;code&gt;.test_durations&lt;/code&gt; nuevo contra el que los futuros runs&lt;br&gt;
   se balancean.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sacar coverage del hot path.&lt;/strong&gt; Coverage solo agregó ~5s en ARM en nuestro benchmark, pero a ~5s × 4 shards = 20s ahorrados. Trade-off: o lo aceptas en el job de tests o agregas un job de coverage no-paralelo aparte. Nosotros lo dejamos en el path.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Lecciones
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mide antes de optimizar.&lt;/strong&gt; Varias horas de trabajo en xdist, template DBs y BuildKit fueron útiles, pero el verdadero unlock vino de un step diagnóstico de 30 segundos que me dijo que el cuello de botella era el startup de pytest, no nada de lo que llevaba atacando.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El pytest más rápido es uno que no inicias dos veces.&lt;/strong&gt; Cada invocación de pytest paga un costo de startup fijo. Con un conftest pesado, ese costo domina todo lo demás. Más paralelismo = más startups = más costo. Menos shards más grandes le ganan a más shards chiquitos pasado un umbral.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pytest-xdist&lt;/code&gt; no es gratis.&lt;/strong&gt; Funciona bien cuando el costo por test &amp;gt;&amp;gt; el costo de startup. Cuando el startup es 20s y los tests son de 500ms, la ecuación se invierte.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursos por worker necesitan aislamiento por worker.&lt;/strong&gt; El bug sutil fue que los background workers abrían su propio &lt;code&gt;AsyncSessionLocal&lt;/code&gt; apuntando a la DB equivocada. El fix no estaba en la aplicación — estaba en el conftest de tests, alineando las env vars antes de que la app importara nada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL tiene las primitivas.&lt;/strong&gt; Advisory locks para sincronizar entre procesos, &lt;code&gt;CREATE DATABASE ... TEMPLATE&lt;/code&gt; para bootstrap de schema. Las dos me salvaron de inventar mecanismos más débiles encima.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Los BuildKit cache mounts siguen sub-utilizados.&lt;/strong&gt; Dos líneas en un Dockerfile (&lt;code&gt;--mount=type=cache&lt;/code&gt; para los cachés de apt y uv/pip) bajaron los docker builds repetidos de ~40s a ~10s, pero solo después de cambiarse del builder legacy de Docker vía &lt;code&gt;setup-buildx-action&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>githubactions</category>
      <category>ci</category>
      <category>python</category>
    </item>
    <item>
      <title>Pros y Cons de las arquitecturas multi-región</title>
      <dc:creator>Franchesco Romero</dc:creator>
      <pubDate>Thu, 02 Apr 2026 22:07:40 +0000</pubDate>
      <link>https://dev.to/aws-builders/pros-y-cons-de-las-arquitecturas-multi-region-l5k</link>
      <guid>https://dev.to/aws-builders/pros-y-cons-de-las-arquitecturas-multi-region-l5k</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://speakerdeck.com/elchesco/pros-y-cons-de-las-arquitecturas-multi-region" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ffiles.speakerdeck.com%2Fpresentations%2F6f426a0c5c1647e5843fe82802a7233b%2Fslide_0.jpg%3F38969754" height="1080" class="m-0" width="1920"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://speakerdeck.com/elchesco/pros-y-cons-de-las-arquitecturas-multi-region" rel="noopener noreferrer" class="c-link"&gt;
            Pros y Cons de las arquitecturas multi-región  - Speaker Deck
          &lt;/a&gt;
        &lt;/h2&gt;
          
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fd1eu30co0ohy4w.cloudfront.net%2Fassets%2Ffavicon-bdd5839d46040a50edf189174e6f7aacc8abb3aaecd56a4711cf00d820883f47.png" width="512" height="512"&gt;
          speakerdeck.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Los retos reales de ir multi-región
&lt;/h2&gt;

&lt;p&gt;Antes de hablar de soluciones, hay que nombrar los retos con claridad porque es donde más se subestima el esfuerzo. El primero es elegir la solución tecnológica correcta — no todas las cargas de trabajo necesitan multi-región y no todos los servicios de AWS están disponibles igual en todas las regiones. El segundo es el manejo de fallos a escala: no basta con tener recursos en dos regiones si no has pensado cómo se comporta cada componente ante una falla. El tercero es la cercanía a los usuarios, que no siempre es puramente técnica — hay leyes, regulaciones y requisitos de soberanía de datos que dictan dónde puede vivir tu información.&lt;/p&gt;

&lt;p&gt;Ignorar cualquiera de estos puntos al inicio garantiza una conversación mucho más difícil después.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tolerancia a fallos: el modelo mental que todo lo rige
&lt;/h2&gt;

&lt;p&gt;El concepto clave aquí es el &lt;strong&gt;dominio de error&lt;/strong&gt; (fault domain). Cada componente de tu arquitectura pertenece a un dominio que define su política de falla: puede ser redundante (se replica), ignorable (su caída no afecta el sistema), o en cascada (si cae, arrastra a quien depende de él — el temido SPOF).&lt;/p&gt;

&lt;p&gt;El problema clásico es una arquitectura donde la base de datos es un dominio en cascada dentro de una sola AZ, en una sola región. Si esa AZ tiene problemas, caes completo. La estrategia multi-región resuelve esto añadiendo un nivel más en la jerarquía de dominios, pero también introduce nuevas preguntas sobre consistencia y latencia de replicación que hay que responder explícitamente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Las capas de una arquitectura multi-región
&lt;/h2&gt;

&lt;p&gt;Pensar en capas ayuda a no perderse. Cada capa tiene sus propias decisiones y sus propios servicios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capa de redes.&lt;/strong&gt; El CDN entrega contenido global con acceso seguro y rápido — CloudFront es el componente natural aquí en AWS. El DNS, específicamente Route 53, es quien realmente orquesta el tráfico entre regiones: puedes rutear por latencia, por failover, por geolocalización o con políticas ponderadas. Una buena estrategia de DNS hace más diferencia de lo que la gente espera — es literalmente el primer punto de decisión que toca cada request de usuario. Las redes internas entre regiones deben estar interconectadas y planificadas desde el inicio, no como un afterthought.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capa de cómputo.&lt;/strong&gt; Los servicios deben ser modulares, organizados por dominio de negocio y escalables bajo demanda. La elección entre Lambda, EC2, ECS o Kubernetes depende del caso de uso — no hay respuesta genérica, y lo que sí aplica siempre es que la capa de cómputo debe poder replicarse o levantarse en otra región sin fricción manual.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capa de aplicación.&lt;/strong&gt; Aquí hay un principio que marca la diferencia: la aplicación debe ser &lt;strong&gt;agnóstica a la región&lt;/strong&gt;. Eso implica configuración externalizada, procesos sin estado (stateless) y secretos administrables. Un ejemplo concreto: leer el &lt;code&gt;region_name&lt;/code&gt; desde una variable en lugar de hardcodearlo en el código. Suena básico y sin embargo es donde se rompen más arquitecturas multi-región en la práctica.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capa de datos.&lt;/strong&gt; Esta es la más compleja. Antes de elegir un servicio hay que identificar los patrones de acceso, el tipo de almacenamiento (bloque, archivo u objeto), el costo de replicación y dónde están los usuarios. AWS tiene soporte de replicación cross-region en DynamoDB, RDS Aurora, RDS estándar, S3, ElastiCache y DocumentDB. Cada uno tiene sus propias implicaciones de consistencia eventual vs. consistencia fuerte que hay que entender antes de decidir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capa de seguridad, identidad y acceso.&lt;/strong&gt; IAM es global, lo cual simplifica la gestión de usuarios, roles y grupos. KMS permite crear llaves con capacidad multi-región. Secrets Manager puede replicar secretos en regiones secundarias — y aquí hay un detalle importante de Terraform: cuando configuras un &lt;code&gt;aws_secretsmanager_secret&lt;/code&gt; con un bloque &lt;code&gt;replica&lt;/code&gt;, la región secundaria se sincroniza automáticamente. Parece trivial hasta que lo necesitas en un failover real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoreo: no es opcional, es parte de la arquitectura
&lt;/h2&gt;

&lt;p&gt;Una arquitectura multi-región sin observabilidad centralizada es básicamente una caja negra distribuida. CloudWatch, Config, GuardDuty y CloudTrail son servicios regionales, pero servicios como Security Hub y CloudTrail soportan agregados multi-región, lo que permite tener una vista unificada de eventos de seguridad sin tener que revisar consola por consola.&lt;/p&gt;

&lt;p&gt;Hay un punto importante aquí: una estrategia de monitoreo requiere varias iteraciones. No sale perfecta a la primera. Herramientas como Amazon DevOps Guru ayudan a identificar comportamientos anómalos, sugerir mejoras de configuración y alertar sobre fallos críticos — complementan bien el stack base de observabilidad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Despliegue: IaC o no escala
&lt;/h2&gt;

&lt;p&gt;En arquitecturas multi-región, el despliegue manual no es una opción viable a largo plazo. Infraestructura como código (Terraform, CDK, CloudFormation) no es solo una buena práctica — es lo que permite recrear un entorno completo en otra región en minutos en lugar de días. El control de cambios debe ser granular: por cuenta, por ambiente y por región. IAM debe seguir el principio de mínimo privilegio, y los fallos deben estar controlados — es decir, un error en el despliegue de una región no debe tumbar las otras.&lt;/p&gt;

&lt;p&gt;Un tip práctico: las nuevas regiones también funcionan muy bien como sandbox para validar nuevas funcionalidades o para simular desastres antes de que lleguen solos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que realmente hay que considerar antes de empezar
&lt;/h2&gt;

&lt;p&gt;Multi-región no es gratis ni en costo ni en complejidad operacional. El &lt;em&gt;operational overhead&lt;/em&gt; es real: cada recurso que existe en una región ahora existe en dos o más, con todo lo que eso implica en mantenimiento, monitoreo y actualizaciones. Los costos de transferencia de datos entre regiones también se acumulan rápido si no se modelan desde el inicio.&lt;/p&gt;

&lt;p&gt;Antes de empezar, vale la pena hacer un ejercicio de planeación con una matriz de prioridad, esfuerzo, complejidad y dependencias — algo similar al Método Eisenhower. No todo tiene que regionalizarse al mismo tiempo ni con la misma urgencia. Siempre hay componentes que son candidatos naturales para regionalizarse primero (típicamente los más críticos y con menor complejidad de replicación) y otros que pueden esperar.&lt;/p&gt;

&lt;p&gt;Un Well Architected Review es un buen punto de partida para hacer ese inventario con una metodología estructurada.&lt;/p&gt;

&lt;h2&gt;
  
  
  La arquitectura evoluciona
&lt;/h2&gt;

&lt;p&gt;El estado final de una arquitectura multi-región en AWS se ve algo así: el usuario llega a Route 53, que rutea al CloudFront más cercano, que a su vez dirige el tráfico a la región correspondiente — donde viven el API Gateway, las Lambdas y la base de datos Aurora replicada. Todo gestionado por certificados en ACM y con tráfico distribuido por políticas de latencia o failover en DNS.&lt;/p&gt;

&lt;p&gt;Llegar ahí no pasa de un día para otro. Llega por iteraciones, con IaC como columna vertebral y con una estrategia de DNS que desde el primer día esté pensada para escalar.&lt;/p&gt;




&lt;p&gt;Multi-región no es un problema de servicios, es un problema de diseño. Los servicios de AWS están listos. La pregunta es si tu arquitectura, tu código y tus procesos también lo están.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>aws</category>
      <category>cloud</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Multi-Stage Continuous Delivery</title>
      <dc:creator>Franchesco Romero</dc:creator>
      <pubDate>Thu, 02 Apr 2026 21:59:47 +0000</pubDate>
      <link>https://dev.to/aws-builders/multi-stage-continuous-delivery-2gmg</link>
      <guid>https://dev.to/aws-builders/multi-stage-continuous-delivery-2gmg</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://speakerdeck.com/elchesco/multi-stage-continuous-delivery" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ffiles.speakerdeck.com%2Fpresentations%2F725d7651d322473f9a80b6a9ad378d3b%2Fslide_0.jpg%3F38969727" height="1080" class="m-0" width="1920"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://speakerdeck.com/elchesco/multi-stage-continuous-delivery" rel="noopener noreferrer" class="c-link"&gt;
            Multi-Stage Continuous Delivery - Speaker Deck
          &lt;/a&gt;
        &lt;/h2&gt;
          
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fd1eu30co0ohy4w.cloudfront.net%2Fassets%2Ffavicon-bdd5839d46040a50edf189174e6f7aacc8abb3aaecd56a4711cf00d820883f47.png" width="512" height="512"&gt;
          speakerdeck.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  El problema con los pipelines tradicionales
&lt;/h2&gt;

&lt;p&gt;El concepto de Multi-Stage CD es sencillo: llevas código a prod en varias iteraciones y a través de diferentes ambientes — dev, staging, prod — con fases bien definidas: build, prepare, deploy, test, notify, rollback. Suena limpio. Y en papel, lo es.&lt;/p&gt;

&lt;p&gt;El problema es la realidad. Según el State of DevOps Report 2020, el 95% del tiempo se va en mantenimiento de pipelines, el 80% en tareas manuales, y el 90% en remediación también manual. Nadie escribe esas métricas en su README, pero todos las vivimos.&lt;/p&gt;

&lt;p&gt;Los retos concretos son tres y son los de siempre: la disponibilidad de ambientes (el clásico &lt;em&gt;"no le muevan a dev que estoy probando algo"&lt;/em&gt;), satisfacer dependencias externas correctamente — JS, Python, AWS, lo que sea — y los ambientes con candado cuando hay un bug en prod y todo se paraliza. A eso le sumas llegada lenta a producción, más de siete herramientas involucradas en el proceso, y pipelines distintos para web, API y mobile que cada quien personalizó a su manera. El resultado es un Frankenstein difícil de mantener para cualquier persona del equipo.&lt;/p&gt;

&lt;p&gt;Lo que realmente se necesita no es magia: capacidad de poner ambientes en cuarentena, dependencias siempre disponibles y seguras, configuración que realmente funcione, y despliegues validados con tests, métricas de performance y SLOs/SLIs bien definidos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keptn: un control plane para gobernarlos a todos
&lt;/h2&gt;

&lt;p&gt;La solución que propongo es Keptn — y el título de esta sección es intencional. Keptn es una plataforma open source de orquestamiento que automatiza la configuración y provee en un solo control plane todo lo que normalmente está disperso: monitoreo, despliegue, remediación y resiliencia.&lt;/p&gt;

&lt;p&gt;Lo que lo hace diferente es su enfoque declarativo y orientado a GitOps. Defines tus ambientes y estrategias en un archivo &lt;code&gt;shipyard.yaml&lt;/code&gt; y Keptn se encarga de la orquestación basada en eventos. No necesitas escribir la lógica de coordinación entre herramientas — eso ya está resuelto.&lt;/p&gt;

&lt;p&gt;Desde el punto de vista de plataforma, Keptn entrega progressive delivery, automatización de SRE, auto-remediación y rollback, y una configuración codificable e independiente de herramientas. Pero la parte más importante: mantiene conectividad con las herramientas que ya tienes — JMeter, Argo, Jenkins, Helm, lo que ya está corriendo en tu stack.&lt;/p&gt;

&lt;p&gt;Un beneficio que no es obvio a primera vista: &lt;strong&gt;los pipelines tradicionales dejan de ser necesarios&lt;/strong&gt;. Keptn reemplaza esa necesidad con fases dedicadas y orquestamiento event-driven. Tienes estrategias out-of-the-box como Blue/Green y Canary, más observabilidad integrada en el proceso con auditabilidad y trazabilidad completas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo funciona por dentro
&lt;/h2&gt;

&lt;p&gt;El modelo mental es el siguiente: Keptn expone servicios a los cuales las herramientas se suscriben por medio de integraciones. Los eventos de Keptn se traducen a llamadas API hacia y desde esas herramientas.&lt;/p&gt;

&lt;p&gt;En la práctica: Keptn crea un evento y lo distribuye a cualquier servicio que esté escuchando — por ejemplo, &lt;code&gt;sh.keptn.event.hello-world.triggered&lt;/code&gt;. El Job Executor Service (JES) detecta el evento, busca la configuración en el YAML correspondiente y ejecuta el contenedor. Una vez que termina, el JES envía de vuelta un par de eventos &lt;code&gt;.started&lt;/code&gt; y &lt;code&gt;.finished&lt;/code&gt;. Keptn los recibe, sabe que la tarea está completa y avanza en la secuencia. Simple, trazable, predecible.&lt;/p&gt;

&lt;p&gt;El ecosistema de integraciones es amplio. Para despliegue: Argo, Jenkins, CircleCI. Para observabilidad: Prometheus, Grafana, Splunk. Para testing: JMeter, Selenium, Artillery. Para notificaciones: Slack, webhooks, Tekton. Para automatización: Ansible, webhooks, AWS Lambda. La idea es clara — &lt;strong&gt;Keptn maneja la orquestación, las tareas y la ejecución; nosotros decidimos las herramientas&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué esto importa frente a pipelines tradicionales
&lt;/h2&gt;

&lt;p&gt;La comparación es directa. Los pipelines tradicionales sufren de falta de separación de responsabilidades, código lleno de dependencias y personalizaciones ad hoc, y dificultad para incorporar herramientas específicas sin romper todo. Keptn resuelve esto con fases dedicadas y orquestamiento basado en eventos, interoperabilidad a través de abstracciones bien definidas, y flexibilidad real para cambiar herramientas sin reescribir la lógica de entrega.&lt;/p&gt;

&lt;h2&gt;
  
  
  Próximos pasos: quality gates y progressive delivery
&lt;/h2&gt;

&lt;p&gt;Una vez que el flujo básico está corriendo, los casos de uso avanzados son los que realmente cambian el juego. Los Quality Gates basados en SLI/SLO permiten que un despliegue sólo avance si cumple criterios medibles — por ejemplo, que el porcentaje de éxito de probes sea mayor al 95%, o que la duración de respuesta sea menor a 200ms. El score total determina si el pipeline pasa o emite una advertencia.&lt;/p&gt;

&lt;p&gt;El Progressive Delivery lleva esto un paso más lejos: defines un flujo que va de dev a hardening a production, con estrategias blue/green en los ambientes de mayor criticidad y remediación automatizada en prod. Keptn evalúa quality gates entre cada etapa y sólo promueve si los números lo justifican.&lt;/p&gt;




&lt;p&gt;El punto de todo esto no es adoptar una herramienta más por el gusto de hacerlo. Es reconocer que los pipelines monolíticos tienen un techo bajo, y que un modelo orientado a eventos con separación clara de responsabilidades escala mucho mejor — tanto en complejidad técnica como en tamaño de equipo.&lt;/p&gt;

&lt;p&gt;Si quieres profundizar, el punto de partida es &lt;a href="https://keptn.sh" rel="noopener noreferrer"&gt;keptn.sh&lt;/a&gt; y los recursos de la comunidad en &lt;a href="https://keptn.sh/resources/slides/" rel="noopener noreferrer"&gt;keptn.sh/resources/slides&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>Optimización de costos para transacciones de alto volumen</title>
      <dc:creator>Franchesco Romero</dc:creator>
      <pubDate>Thu, 02 Apr 2026 21:51:48 +0000</pubDate>
      <link>https://dev.to/aws-builders/optimizacion-de-costos-para-transacciones-de-alto-volumen-5159</link>
      <guid>https://dev.to/aws-builders/optimizacion-de-costos-para-transacciones-de-alto-volumen-5159</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://speakerdeck.com/elchesco/optimizacion-de-costos-para-transacciones-de-alto-volumen" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ffiles.speakerdeck.com%2Fpresentations%2F5347f9da1b504e3381e3a1cd603ee7c1%2Fslide_0.jpg%3F38969699" height="1080" class="m-0" width="1920"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://speakerdeck.com/elchesco/optimizacion-de-costos-para-transacciones-de-alto-volumen" rel="noopener noreferrer" class="c-link"&gt;
            Optimización de costos para transacciones de alto volumen - Speaker Deck
          &lt;/a&gt;
        &lt;/h2&gt;
          
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fd1eu30co0ohy4w.cloudfront.net%2Fassets%2Ffavicon-bdd5839d46040a50edf189174e6f7aacc8abb3aaecd56a4711cf00d820883f47.png" width="512" height="512"&gt;
          speakerdeck.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h1&gt;
  
  
  Optimización de Costos para Transacciones de Alto Volumen en AWS
&lt;/h1&gt;

&lt;p&gt;Cuando se habla de optimización de costos en AWS, la conversación suele ir a los mismos lugares de siempre: Reserved Instances, Savings Plans, apagar recursos que no usas. Todo eso está bien y deberías hacerlo, pero hay un nivel más profundo que la mayoría de los equipos no toca — las decisiones de arquitectura y tecnología que generan costos innecesarios a escala sin que nadie se dé cuenta. Este post va sobre eso.&lt;/p&gt;

&lt;h2&gt;
  
  
  La economía oculta de las API calls
&lt;/h2&gt;

&lt;p&gt;En arquitecturas serverless, cada llamada API tiene un costo. Eso parece obvio, pero las implicaciones no siempre son visibles cuando estás diseñando el sistema.&lt;/p&gt;

&lt;p&gt;El patrón más común que genera costo silencioso es el polling. Imagina una aplicación que consulta un endpoint REST cada 5 segundos para verificar si hay nuevas órdenes en DynamoDB. A volumen bajo nadie lo nota. A volumen alto, estás pagando por miles de requests a API Gateway y Lambda que en su mayoría regresan vacío. La corrección es dejar de preguntar y empezar a escuchar: DynamoDB Streams detecta los cambios en la tabla y los envía a EventBridge, que filtra y transforma el evento antes de disparar el downstream — SNS, Step Functions, WebSockets. Cero polling, costo proporcional al trabajo real.&lt;/p&gt;

&lt;p&gt;Otro patrón costoso es usar Lambda para cada operación que pasa por API Gateway. Si tienes un frontend enviando datos JSON que necesitan ser validados y escritos en DynamoDB, el reflejo natural es meter una Lambda en el medio. El problema es que cada invocación tiene costo. API Gateway tiene Mapping Templates que pueden hacer validaciones y transformaciones de request sin necesidad de invocar ninguna función. Para operaciones simples y de alto volumen, eliminar esa Lambda puede significar cientos de miles de invocaciones menos por mes.&lt;/p&gt;

&lt;p&gt;El tercer caso tiene que ver con los límites internos de AWS. Cuando una aplicación escribe a DynamoDB a alta velocidad y alcanza el límite de Write Capacity Units, AWS aplica backoff exponencial y genera reintentos automáticos — que también cuestan. Meter SQS como buffer entre la aplicación y DynamoDB nivela el throughput y elimina esos reintentos, convirtiendo un patrón de escritura caótico en uno predecible y controlable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data transfer: el costo que nadie presupuesta
&lt;/h2&gt;

&lt;p&gt;La transferencia de datos en AWS tiene una particularidad: es casi invisible en el diseño inicial y muy visible en la factura a fin de mes.&lt;/p&gt;

&lt;p&gt;El ejemplo más costoso y fácil de corregir es usar NAT Gateway para que instancias en subnets privadas accedan a S3. NAT Gateway cobra por procesamiento de datos — a $0.045 por GB, 5TB al mes son $225 adicionales que no agregan ningún valor técnico. La solución es un VPC Endpoint para S3: el tráfico fluye directamente dentro de la red de AWS sin pasar por NAT, sin costo por transferencia. Es una de las optimizaciones con mejor ratio esfuerzo/impacto que existen.&lt;/p&gt;

&lt;p&gt;Para APIs con alto tráfico de lectura, CloudFront delante de API Gateway puede eliminar la gran mayoría de invocaciones a Lambda. Si configuras CloudFront para cachear respuestas comunes por 5 minutos, todo ese tráfico repetido deja de llegar al backend. Para catálogos de productos, configuraciones, datos de referencia — el impacto puede ser dramático.&lt;/p&gt;

&lt;p&gt;El caso de GenAI agrega una dimensión nueva. Si tienes un chatbot en AWS que manda cada query a un modelo externo como OpenAI, estás pagando transferencia de datos de salida en cada request. Mover el modelo a Amazon Bedrock o SageMaker dentro de AWS no solo elimina esa transferencia — también te da más control sobre latencia, disponibilidad y costos por token.&lt;/p&gt;

&lt;h2&gt;
  
  
  La economía del almacenamiento
&lt;/h2&gt;

&lt;p&gt;El formato de compresión que usas para tus datos puede parecer un detalle técnico menor. En cargas de análisis a escala, no lo es. GZIP es el default en muchos pipelines de Redshift, EMR y Glue, pero tiene latencia de descompresión alta y peor ratio de compresión comparado con alternativas más modernas. Zstandard en nivel 3 ofrece mayor eficiencia tanto de almacenamiento como de procesamiento, con reducciones de más de un 30% en tamaño. Para 50TB de logs de transacciones, ese 30% es dinero real.&lt;/p&gt;

&lt;p&gt;El zero-copy data sharing es otro concepto que vale la pena entender. El patrón ineficiente es copiar datos entre cuentas de AWS para que cada equipo tenga su propia copia en S3. Cada copia es storage adicional, cada sincronización es transferencia. Con AWS Lake Formation y Glue Catalog Cross-Account, puedes dar acceso a las mismas tablas registradas a múltiples cuentas sin mover un solo byte. Los datos viven en un lugar, el acceso se gestiona con permisos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Computación selectiva: elegir bien el tipo de recurso
&lt;/h2&gt;

&lt;p&gt;Aquí hay una distinción que muchos equipos no hacen: no todos los workloads tienen el mismo cuello de botella. Algunos son CPU-intensivos, otros son memory-bound, otros son IO-bound. Elegir la familia de instancia equivocada significa pagar por recursos que no estás usando.&lt;/p&gt;

&lt;p&gt;Un ejemplo concreto: una base de datos MySQL en RDS con instancias t3.medium que tiene la RAM llena y hace swapping a disco. El problema no es CPU — es memoria. La solución no es subir a una instancia con más CPU, es cambiar a una familia memory-optimized. Las instancias R6g con Graviton2 ofrecen 50% más memoria por dólar que T3. Para Elasticsearch con alto uso de IOPS donde EBS gp3 ya es el cuello de botella, instancias I4i con almacenamiento NVMe local reducen la latencia de consultas en un 60%.&lt;/p&gt;

&lt;p&gt;En Lambda, la relación memoria-CPU tiene una implicación de costos contraintuitiva. Con 128MB de RAM, una función de procesamiento de imágenes puede tardar 6 segundos. Con 1024MB tiene acceso a una vCPU completa y la misma operación tarda 0.8 segundos — 7 veces más rápida. Lambda Power Tuning existe para encontrar el punto óptimo entre costo por ejecución y tiempo de ejecución, y el resultado frecuentemente sorprende: más memoria puede ser más barato.&lt;/p&gt;

&lt;p&gt;En Kubernetes, el costo escala con el número de nodos, y el número de nodos escala con el número de pods. Si cada microservicio tiene su propio pod con límites de CPU y memoria dedicados, terminas con mucha capacidad reservada que en promedio está subutilizada. Consolidar servicios relacionados en pods multi-tenant reduce el número total de nodos necesarios y mejora la utilización del clúster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Estrategias de bases de datos
&lt;/h2&gt;

&lt;p&gt;Aurora tiene una característica que justifica su adopción para cargas intensivas en I/O: Aurora I/O-Optimized elimina el cobro por operaciones de lectura y escritura, cambiando el modelo de precio a uno más predecible basado en capacidad. Si tienes un workload con miles de IOPS, la diferencia puede ser sustancial. Combinado con auto-tiering para mover datos históricos a almacenamiento más barato y Aurora Backtrack para reducir snapshots innecesarios, el costo total de una instancia de 5TB puede bajar considerablemente.&lt;/p&gt;

&lt;p&gt;Para logs de transacciones en PostgreSQL que crecen a millones de registros con el tiempo, la solución no es escalar la instancia — es particionar los datos por tiempo. En lugar de una tabla gigante donde las consultas escanean todo el historial, particiones mensuales o diarias limitan el scope de cada query al período relevante. Amazon Timestream también es una alternativa cuando el patrón de acceso es fundamentalmente time-series.&lt;/p&gt;

&lt;p&gt;Con DynamoDB a escala, TTL es una herramienta de costo que se subestima. Los ítems expirados se eliminan automáticamente sin consumir Write Capacity Units, lo que mantiene la tabla limpia sin operaciones de borrado explícitas. DAX como capa de caché elimina lecturas repetidas a la tabla principal y DynamoDB Streams permite reaccionar a cambios sin polling — ambos ya cubiertos en la sección de API calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los anti-patterns que hay que evitar
&lt;/h2&gt;

&lt;p&gt;Tan importante como saber qué optimizar es saber qué no hacer. Over-engineering en arquitecturas event-centric es frecuente: agregar capas de EventBridge, SNS y SQS a flujos que podrían ser síncronos simples, creando complejidad operacional por ahorros marginales que nunca se materializan.&lt;/p&gt;

&lt;p&gt;El mito de que serverless siempre es más económico que servidores es exactamente eso: un mito. Para cargas con throughput constante y predecible, un servicio administrado de contenedores o incluso instancias reservadas pueden salir más baratos que pagar por invocación a escala. El modelo correcto depende del patrón de tráfico.&lt;/p&gt;

&lt;p&gt;Los logs sin filtrar son otro agujero silencioso. Loggear todo en CloudWatch Logs a nivel DEBUG en producción, sin retention policies, sin filtros, genera costos de ingesta y almacenamiento que crecen con el tráfico. Filtrar a nivel del source, definir retention apropiado y usar Log Insights solo cuando se necesita mantiene eso bajo control.&lt;/p&gt;

&lt;p&gt;La conclusión más importante es también la más sencilla: la optimización no es un proyecto con fecha de inicio y fin, es una práctica continua. Las estrategias no convencionales que van más allá de Reserved Instances y right-sizing pueden aportar entre un 15-30% adicional de ahorro. Pero solo si el equipo tiene cultura de cost awareness integrada desde el diseño, no como una corrección posterior.&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>aws</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Optimizando Cargas de Trabajo Serverless Técnicas para mejorar Rendimiento y Eficiencia</title>
      <dc:creator>Franchesco Romero</dc:creator>
      <pubDate>Thu, 02 Apr 2026 21:19:45 +0000</pubDate>
      <link>https://dev.to/aws-builders/optimizando-cargas-de-trabajo-serverless-tecnicas-para-mejorar-rendimiento-y-eficiencia-48og</link>
      <guid>https://dev.to/aws-builders/optimizando-cargas-de-trabajo-serverless-tecnicas-para-mejorar-rendimiento-y-eficiencia-48og</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://speakerdeck.com/elchesco/optimizando-cargas-de-trabajo-en-lambda" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ffiles.speakerdeck.com%2Fpresentations%2Ff651310d2143405087d150771cf6621e%2Fslide_0.jpg%3F38969317" height="1080" class="m-0" width="1920"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://speakerdeck.com/elchesco/optimizando-cargas-de-trabajo-en-lambda" rel="noopener noreferrer" class="c-link"&gt;
            Optimizando Cargas de Trabajo en Lambda - Speaker Deck
          &lt;/a&gt;
        &lt;/h2&gt;
          
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fd1eu30co0ohy4w.cloudfront.net%2Fassets%2Ffavicon-bdd5839d46040a50edf189174e6f7aacc8abb3aaecd56a4711cf00d820883f47.png" width="512" height="512"&gt;
          speakerdeck.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Este post va sobre las técnicas que realmente mueven la aguja cuando estás buscando rendimiento y eficiencia en Lambda.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo está construido lo que estás ejecutando
&lt;/h2&gt;

&lt;p&gt;Lambda tiene una arquitectura en capas que vale la pena entender antes de optimizar cualquier cosa: tu función vive dentro de un Language Runtime, que a su vez corre en un Execution Environment, administrado por el Lambda Service sobre un Compute Substrate. Cada capa tiene implicaciones en cómo se comporta tu función al arrancar y durante la ejecución.&lt;/p&gt;

&lt;p&gt;Dos mecanismos que se aprovechan bien una vez que entiendes esto son las &lt;strong&gt;Layers&lt;/strong&gt; y las &lt;strong&gt;Extensions&lt;/strong&gt;. Las capas te permiten separar tu código de función de sus dependencias y recursos compartidos — librerías, SDKs, utilerías comunes — y reutilizarlos en múltiples funciones sin empaquetar lo mismo en cada deployment. Las extensiones, por su parte, se integran en el ciclo de vida de la invocación para conectar Lambda con herramientas de monitoreo, seguridad y observabilidad, corriendo en paralelo al runtime sin modificar el código de tu función.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema de los Cold Starts
&lt;/h2&gt;

&lt;p&gt;Si hay un tema que aparece en todas las conversaciones sobre Lambda es este. Un cold start ocurre cuando AWS necesita provisionar un nuevo contexto de ejecución — descarga el código, levanta el entorno, inicializa el runtime, y luego ejecuta tu función. Esto pasa con cambios de código, durante scale-up, en rebalanceos de AZs, o después de fallos.&lt;/p&gt;

&lt;p&gt;El impacto varía bastante por runtime. Python y JavaScript tienen cold starts en el rango de 200-500ms. Java y Docker pueden llegar fácilmente al segundo y medio. No es lo mismo si tienes una API interactiva donde el usuario espera la respuesta, que un pipeline batch asíncrono donde nadie nota 800ms extra.&lt;/p&gt;

&lt;p&gt;La estrategia más simple para mitigarlo es el warmup con eventos de CloudWatch o EventBridge que disparan la función cada X minutos para mantenerla activa. Funciona, pero tiene sus trade-offs: es periódico, no puedes controlar qué instancias específicas se calientan, y a escala puede tener un costo no despreciable. Haz los números: una sola función ejecutándose cada 5 minutos cuesta alrededor de $0.18 al mes. Diez funciones: $14.58 más costos de CloudWatch. No es dramático, pero tampoco es gratis.&lt;/p&gt;

&lt;p&gt;La alternativa más robusta es &lt;strong&gt;Provisioned Concurrency&lt;/strong&gt;: le dices a Lambda cuántos entornos de ejecución quieres inicializados y listos en todo momento. Por cada unidad de simultaneidad aprovisionada se mantienen mínimo dos entornos en AZs separadas, lo que también da alta disponibilidad. El contra es que pagas por esa capacidad aunque no la uses, y hay restricciones importantes: no funciona con &lt;code&gt;$LATEST&lt;/code&gt; ni con &lt;a href="mailto:Lambda@Edge"&gt;Lambda@Edge&lt;/a&gt;. Úsalo con inteligencia — tiene más sentido en runtimes de inicio lento como Java, y puedes combinarlo con Application Auto Scaling para ajustar la capacidad según patrones de tráfico.&lt;/p&gt;

&lt;p&gt;Para Java específicamente, &lt;strong&gt;SnapStart&lt;/strong&gt; es un game changer: toma una snapshot del estado inicializado de la función y la restaura en invocaciones posteriores, logrando hasta 10x mejora en tiempo de arranque sin cambios en el código. Disponible desde la consola, SAM y CDK.&lt;/p&gt;

&lt;p&gt;Vale mencionar también &lt;strong&gt;LLRT (Low Latency Runtime)&lt;/strong&gt; — un runtime JavaScript liviano escrito en Rust que AWS Labs lanzó como experimental. Ofrece inicio hasta 10x más rápido y costo hasta 2x menor comparado con el runtime de Node.js estándar. Si estás en JavaScript y los cold starts te están afectando, vale la pena explorarlo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Right-sizing y optimización de costos
&lt;/h2&gt;

&lt;p&gt;Más memoria no siempre significa más costo — en Lambda, memoria y CPU están acoplados, así que a veces asignar más RAM resulta en ejecuciones más rápidas que al final cuestan lo mismo o menos. El problema es que nadie sabe exactamente cuánta memoria necesita una función sin medirlo.&lt;/p&gt;

&lt;p&gt;Para esto existe &lt;strong&gt;AWS Lambda Power Tuning&lt;/strong&gt;, una State Machine de Step Functions que ejecuta tu función con diferentes configuraciones de memoria, mide duración y costo, y te da una visualización clara del trade-off. La diferencia entre la configuración óptima para costo (1536MB en el ejemplo de la presentación) vs. la óptima para velocidad (3008MB) puede ser significativa, y depende completamente de tu workload específico.&lt;/p&gt;

&lt;p&gt;Otros recursos útiles para right-sizing son AWS Compute Optimizer, Lambda Insights, y DevOps Guru, que pueden darte recomendaciones basadas en patrones de uso histórico.&lt;/p&gt;

&lt;p&gt;Si tu workload tolera migrar a &lt;strong&gt;arm64 (Graviton2)&lt;/strong&gt;, el ahorro en precio/performance es aproximadamente un 34% frente a x86. La migración no es siempre trivial — lenguajes compilados y algunos containers requieren adaptaciones — pero el beneficio es real y verificable con Lambda Power Tuning comparando ambas arquitecturas. Los &lt;strong&gt;Compute Savings Plans&lt;/strong&gt; también aplican a Lambda y pueden dar hasta un 17% adicional de ahorro sobre demanda.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimizaciones que están en tu control
&lt;/h2&gt;

&lt;p&gt;Hay cosas que AWS optimiza por ti (descarga del código, setup del entorno) y cosas que dependen de ti (init code y handler code). La mayor palanca que tienes está en mantener tus funciones ligeras: dependencias mínimas, frameworks más pequeños, sin código muerto, y empaquetado en un único archivo minificado. El tamaño del ZIP afecta directamente cuánto tarda Lambda en descargar y cargar tu función.&lt;/p&gt;

&lt;p&gt;Lazy-loading es otra práctica que marca diferencia: en lugar de importar todo al inicio del módulo, carga las dependencias solo cuando realmente se necesitan. El código de la presentación lo ilustra bien tanto en JavaScript como en Python — inicializas el cliente de DynamoDB o S3 dentro del handler, condicionado a si ya está inicializado, y lo reutilizas en invocaciones posteriores dentro del mismo contexto de ejecución.&lt;/p&gt;

&lt;p&gt;Para bases de datos relacionales, si estás llamando RDS desde Lambda, &lt;strong&gt;RDS Proxy&lt;/strong&gt; resuelve el problema de agotamiento de conexiones que ocurre cuando Lambda escala agresivamente. Agrupación de conexiones, mayor disponibilidad y caché de queries — sin cambios en el código de la función.&lt;/p&gt;

&lt;p&gt;Si tu función sirve respuestas que no cambian con cada invocación, configurar caché a nivel de API Gateway o CloudFront puede eliminar completamente la invocación de Lambda para esas peticiones. Para algunos patrones esto es la optimización más impactante de todas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Métricas que realmente importan
&lt;/h2&gt;

&lt;p&gt;Medir promedios de latencia es engañoso. Lo que importa es el percentil p95 o p99 — el rendimiento que experimentan el 5% o el 1% más lento de tus usuarios. Un promedio de 200ms con un p99 de 4 segundos es una función con problemas serios que el promedio oculta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS X-Ray&lt;/strong&gt; es la herramienta para esto: tracing end-to-end de requests, service map de tu aplicación, y visibilidad sobre exactamente dónde se producen los cuellos de botella. Un &lt;code&gt;Tracing: Active&lt;/code&gt; en tu template de SAM o CDK y ya tienes trazas automáticas de Lambda y los servicios downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  El punto que más se subestima
&lt;/h2&gt;

&lt;p&gt;Lambda no debería ser el default para absolutamente todo. Hay operaciones donde otros servicios hacen el trabajo mejor y más barato sin invocar ninguna función. Orquestación compleja de workflows: Step Functions. Filtrado de mensajes antes de procesarlos: SNS con filter policies. Scheduling de tareas: EventBridge Scheduler. Integrar Lambda innecesariamente en esos flujos agrega latencia y costo sin valor real.&lt;/p&gt;

&lt;p&gt;Optimizar Lambda no es una sola cosa — es la suma de decisiones en configuración, código, arquitectura y monitoreo. El punto de partida es medir con X-Ray, ajustar con Lambda Power Tuning, y construir con las restricciones del caso de uso en mente, no con el toolset que conoces mejor.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>aws</category>
      <category>performance</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Mejorando tu Seguridad en AWS con ML y AI</title>
      <dc:creator>Franchesco Romero</dc:creator>
      <pubDate>Thu, 02 Apr 2026 21:10:57 +0000</pubDate>
      <link>https://dev.to/aws-builders/mejorando-tu-seguridad-en-aws-con-ml-y-ai-4122</link>
      <guid>https://dev.to/aws-builders/mejorando-tu-seguridad-en-aws-con-ml-y-ai-4122</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://speakerdeck.com/elchesco/mejorando-tu-seguridad-en-aws-con-ml-y-ai" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ffiles.speakerdeck.com%2Fpresentations%2F0a8539bf3a6644038344519eb489a702%2Fslide_0.jpg%3F38969279" height="1080" class="m-0" width="1920"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://speakerdeck.com/elchesco/mejorando-tu-seguridad-en-aws-con-ml-y-ai" rel="noopener noreferrer" class="c-link"&gt;
            Mejorando tu Seguridad en AWS con ML y AI - Speaker Deck
          &lt;/a&gt;
        &lt;/h2&gt;
          
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fd1eu30co0ohy4w.cloudfront.net%2Fassets%2Ffavicon-bdd5839d46040a50edf189174e6f7aacc8abb3aaecd56a4711cf00d820883f47.png" width="512" height="512"&gt;
          speakerdeck.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h1&gt;
  
  
  Mejorando tu Seguridad en AWS con ML y AI
&lt;/h1&gt;

&lt;p&gt;Las aplicaciones modernas son complejas, distribuidas, y cada nuevo servicio que agregas es también una nueva superficie de ataque potencial. A eso súmale el volumen brutal de logs, métricas y trazas que genera una plataforma en producción, y tienes la receta perfecta para que identificar la causa raíz de un incidente se convierta en una tarea que consume horas — o días — de personas con conocimiento muy especializado.&lt;/p&gt;

&lt;p&gt;Las amenazas tampoco se quedan quietas. Suplantación de identidad, ransomware, fraudes, amenazas internas, accesos no autorizados... la lista es larga y la frecuencia con la que ocurren no para de crecer. El problema de la seguridad tradicional es que trabaja con reglas fijas y estáticas, y los atacantes hace mucho que aprendieron a moverse entre los huecos que esas reglas dejan.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué necesitamos realmente?
&lt;/h2&gt;

&lt;p&gt;Más allá de detectar incidentes, necesitamos hacerlo rápido. Reducir el tiempo de detección, de triaging, de debugging. Necesitamos protección continua que se adapte a nuestra operación, que nos ayude a cumplir con marcos normativos, y que no nos despierte a las 3am por un falso positivo. En resumen: necesitamos que el sistema aprenda.&lt;/p&gt;

&lt;p&gt;Y ahí es donde entran ML y AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo funciona
&lt;/h2&gt;

&lt;p&gt;El concepto es sencillo: un algoritmo aprende a reconocer patrones en datos históricos, y una vez entrenado, usa ese conocimiento para hacer predicciones sobre datos nuevos y disparar acciones en consecuencia. Nada de magia, pura estadística aplicada a escala. Lo interesante es cómo AWS ha integrado esto directamente en sus servicios de seguridad, sin que tengas que construir ni operar los modelos tú mismo.&lt;/p&gt;

&lt;p&gt;Las aplicaciones concretas van desde detección de anomalías en tráfico de red — identificar un DDoS antes de que te rompa el día — hasta modelado de comportamiento para detectar actividad maliciosa o fraudulenta que ningún operador humano hubiera notado a tiempo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los servicios que importan
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Amazon GuardDuty&lt;/strong&gt; es el punto de entrada natural. Es un servicio administrado que usa ML para analizar de forma continua eventos de CloudTrail, VPC Flow Logs y DNS Logs. Lo que hace bien es identificar patrones: acceso inicial sospechoso, escalación de privilegios, reconocimiento, acciones defensivas del atacante para cubrir sus huellas. No tienes que configurar reglas; el modelo se encarga.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon Detective&lt;/strong&gt; entra cuando ya ocurrió algo y necesitas entender qué pasó. Usa aprendizaje automático para analizar los mismos logs y te da contexto: qué cuentas estuvieron involucradas, desde qué IPs, qué recursos tocaron, y desde dónde geográficamente. La diferencia entre tardar 2 horas o 2 días en contener un incidente muchas veces está en tener ese contexto disponible de inmediato.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CloudWatch&lt;/strong&gt; tiene algo que mucha gente no usa: detección automática de anomalías basada en ML. Analiza el histórico de tus métricas, aprende el comportamiento esperado, y te permite definir ventanas de detección y umbrales de tolerancia adaptados a tu operación — por ejemplo, excluir el ruido durante una ventana de deployments.&lt;/p&gt;

&lt;p&gt;Para cumplimiento normativo, &lt;strong&gt;Amazon Macie&lt;/strong&gt; usa ML para descubrir y clasificar datos sensibles en S3: PII, datos financieros, credenciales expuestas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS DevOps Guru&lt;/strong&gt; apunta más hacia el análisis predictivo: monitoreo continuo con algoritmos de ML sobre tus métricas de aplicación, detectando comportamientos anómalos antes de que se conviertan en incidentes, y creando automáticamente OpsItems en SSM OpsCenter para que el equipo los atienda.&lt;/p&gt;

&lt;p&gt;Finalmente, &lt;strong&gt;IAM Access Analyzer&lt;/strong&gt; usa Automated Reasoning — no ML clásico, sino análisis formal — para revisar tus políticas de IAM e identificar configuraciones que podrían permitir acceso no autorizado. Es el tipo de análisis que un humano haría manualmente en una auditoría, automatizado y corriendo de forma continua.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que hay que tener claro
&lt;/h2&gt;

&lt;p&gt;Ninguno de estos servicios es plug-and-play en el sentido de que funciona perfecto desde el día uno. Hay un período de aprendizaje — entre 15 minutos y 48 horas dependiendo del servicio — y cada ambiente es único, así que la configuración importa. El punto más importante, y el que más confunde a los equipos que recién empiezan: &lt;strong&gt;anómalo no es lo mismo que vulnerable, roto o malicioso&lt;/strong&gt;. Un comportamiento fuera de lo normal es una señal para investigar, no necesariamente una alarma de incendio.&lt;/p&gt;

&lt;p&gt;La combinación de GuardDuty, Detective, Macie, CloudWatch Anomaly Detection y DevOps Guru crea una capa de seguridad inteligente que detecta lo que las reglas estáticas no ven, responde más rápido de lo que cualquier equipo humano podría, y se adapta al comportamiento real de tu plataforma. ML y AI ya son parte central de la oferta de seguridad de AWS — la pregunta no es si usarlos, sino cuáles activar primero.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>machinelearning</category>
      <category>security</category>
    </item>
    <item>
      <title>Dominando el Caos en Cargas de Trabajo Sin Servidores</title>
      <dc:creator>Franchesco Romero</dc:creator>
      <pubDate>Thu, 02 Apr 2026 20:44:50 +0000</pubDate>
      <link>https://dev.to/aws-builders/dominando-el-caos-en-cargas-de-trabajo-sin-servidores-3d78</link>
      <guid>https://dev.to/aws-builders/dominando-el-caos-en-cargas-de-trabajo-sin-servidores-3d78</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://speakerdeck.com/elchesco/dominando-el-caos-en-cargas-de-trabajo-sin-servidores" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ffiles.speakerdeck.com%2Fpresentations%2F0945f96490874639addaf30e575fe66a%2Fslide_0.jpg%3F38968755" height="1080" class="m-0" width="1920"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://speakerdeck.com/elchesco/dominando-el-caos-en-cargas-de-trabajo-sin-servidores" rel="noopener noreferrer" class="c-link"&gt;
            Dominando el Caos en Cargas de Trabajo Sin Servidores - Speaker Deck
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            La ingeniería del caos implica provocar intencionalmente interrupciones en las cargas de trabajo, generalmente en servidores tradicionales. En esta char&amp;amp;hellip;
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fd1eu30co0ohy4w.cloudfront.net%2Fassets%2Ffavicon-bdd5839d46040a50edf189174e6f7aacc8abb3aaecd56a4711cf00d820883f47.png" width="512" height="512"&gt;
          speakerdeck.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h1&gt;
  
  
  Dominando el Caos en Cargas de Trabajo Sin Servidores
&lt;/h1&gt;

&lt;p&gt;La ingeniería del caos es la práctica de introducir fallos intencional y proactivamente en un sistema para identificar y solucionar debilidades. Esta definición suena contraintuitiva — ¿por qué romper algo que funciona? — pero la premisa es simple: si no pruebas cómo falla tu sistema en condiciones controladas, lo descubrirás en el peor momento posible. &lt;/p&gt;

&lt;p&gt;Los beneficios son concretos: mejor resiliencia, mayor disponibilidad, experiencia de usuario más confiable, y equipos que confían en sus sistemas porque los han probado de verdad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-requisitos antes de introducir el caos
&lt;/h2&gt;

&lt;p&gt;No se puede empezar a romper cosas sin preparación. Antes de ejecutar cualquier experimento de caos en un entorno serverless, hay cinco condiciones que deben estar cubiertas.&lt;/p&gt;

&lt;p&gt;Primero, necesitas tener pruebas existentes — un suite de tests que defina qué es comportamiento correcto, para poder detectar desviaciones cuando introduces fallos. &lt;br&gt;
Segundo, debes conocer a fondo la arquitectura: qué servicios están involucrados, cómo se comunican, qué dependencias existen. &lt;br&gt;
Tercero, hay que determinar alcances y riesgos antes de ejecutar, no durante — saber exactamente qué se puede afectar y qué queda fuera de scope. &lt;br&gt;
Cuarto, es preferible contar con entornos dedicados para los experimentos más agresivos; no todo debe probarse en producción desde el primer día. &lt;br&gt;
Quinto, necesitas monitoreo y observabilidad en su lugar antes de comenzar, porque sin métricas no puedes saber qué está pasando durante el experimento.&lt;/p&gt;

&lt;h2&gt;
  
  
  El ciclo del caos
&lt;/h2&gt;

&lt;p&gt;La ingeniería del caos no es caos sin estructura — es un ciclo bien definido. Empieza con una &lt;strong&gt;hipótesis&lt;/strong&gt;: una pregunta específica sobre el comportamiento del sistema bajo estrés. A partir de ella se diseña un experimento, se crean los fallos, se analizan los resultados, se corrigen los fallos encontrados, se valida la hipótesis, y los aprendizajes alimentan mejora continua que genera nuevas hipótesis. El ciclo se repite.&lt;/p&gt;

&lt;p&gt;El diseño de cada experimento sigue cuatro pasos: definir el &lt;strong&gt;estado estable&lt;/strong&gt; (los KPIs que representan operación normal), formular la &lt;strong&gt;hipótesis&lt;/strong&gt; (qué se espera que pase bajo estrés), especificar la &lt;strong&gt;definición y ejecución&lt;/strong&gt; (tipos de fallo, sistemas a probar, condiciones), y establecer el &lt;strong&gt;monitoreo y análisis&lt;/strong&gt; (cómo se verificarán los KPIs y las desviaciones).&lt;/p&gt;

&lt;p&gt;Un ejemplo concreto: dado un API Gateway multi-región con funciones Lambda detrás de un enrutamiento basado en latencia, la hipótesis sería que inyectar latencia en una región no debería afectar el rendimiento de las demás, y que Route 53 debería detectar el aumento de latencia y redirigir el tráfico a una región más saludable. La ejecución del experimento tendría tres componentes: acción (inyectar latencia en Lambda), objetivo (funciones Lambda en una región específica), y condición de monitoreo (métricas de latencia en CloudWatch).&lt;/p&gt;

&lt;h2&gt;
  
  
  Técnicas de inyección de fallos
&lt;/h2&gt;

&lt;p&gt;Hay tres categorías principales de fallos que se pueden inyectar en entornos serverless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resiliencia de red&lt;/strong&gt; cubre latencia en respuestas y pérdida de paquetes — los fallos más comunes en sistemas distribuidos y los más fáciles de introducir de forma controlada.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interrupciones&lt;/strong&gt; simula servicios no disponibles y respuestas inesperadas — el escenario donde una dependencia simplemente deja de responder o responde con errores.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Límites&lt;/strong&gt; prueba qué pasa cuando se alcanzan los topes de uso de APIs, memoria, o tiempo de ejecución — particularmente relevante en Lambda donde estos límites son hardcoded.&lt;/p&gt;

&lt;p&gt;Más allá de la capa de red, los fallos también pueden inyectarse a nivel de código, de ambiente, o de configuración — tres vectores distintos que cubren la mayoría de los escenarios de fallo reales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Herramientas disponibles en AWS
&lt;/h2&gt;

&lt;p&gt;Para entornos serverless en AWS, hay cuatro opciones principales que vale comparar:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Fault Injection Service (FIS)&lt;/strong&gt; es el servicio nativo de AWS, 100% manejado e integrado con el ecosistema. Es programable desde consola, CLI y CDK, permite experimentos controlados tanto en secuencia como en paralelo, y opera a todos los niveles — red, infra, aplicación. Tiene excelente documentación pero tiene costo por uso.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chaos Toolkit&lt;/strong&gt; es una herramienta open source para caos en entornos generales. Documentación buena, facilidad media, gratuita.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chaos_lambda&lt;/strong&gt; está enfocada en fallos a nivel de runtime de Lambda. Documentación limitada, gratuita.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure Lambda&lt;/strong&gt; opera a nivel de código de Lambda. También gratuita pero con documentación limitada.&lt;/p&gt;

&lt;p&gt;Para la mayoría de los casos en AWS, FIS es la opción más práctica por su integración nativa y el nivel de control que ofrece.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Fault Injection Service en detalle
&lt;/h2&gt;

&lt;p&gt;Un experimento en FIS se construye con cuatro componentes: una &lt;strong&gt;plantilla de experimento&lt;/strong&gt; que define todo lo que va a ocurrir, &lt;strong&gt;acciones&lt;/strong&gt; que especifican qué fallos se van a introducir, &lt;strong&gt;objetivos&lt;/strong&gt; que determinan sobre qué recursos se aplican (por ejemplo, instancias EC2 con tags específicos), y &lt;strong&gt;condiciones de paro&lt;/strong&gt; vinculadas a alarmas de CloudWatch que detienen el experimento si las cosas se salen de control.&lt;/p&gt;

&lt;p&gt;Para entornos serverless específicamente, FIS soporta los siguientes fallos: DynamoDB no responde, una región completa no funciona, Lambda tarda mucho en responder, Lambda responde con error, y throttling de API. Estos cubren los escenarios de fallo más comunes en arquitecturas event-driven y de microservicios sin servidor.&lt;/p&gt;

&lt;p&gt;El monitoreo durante los experimentos se hace con CloudWatch para métricas, X-Ray para rastreo distribuido, y dashboards personalizados que permiten ver el comportamiento en tiempo real mientras el experimento está activo.&lt;/p&gt;

&lt;p&gt;La arquitectura de implementación de FIS para Lambda sigue un flujo claro: FIS lee una plantilla de experimento, ejecuta una automatización que interactúa con Parameter Store para configurar el comportamiento de caos, y la Lambda bajo prueba reporta métricas a CloudWatch. En el caso del patrón &lt;strong&gt;Chaos Injection Lambda Layers&lt;/strong&gt;, FIS orquesta a través de una plantilla y una automatización que actualiza Parameter Store, y la Lambda tiene un Chaos Lambda Layer adicional que lee esa configuración e inyecta el comportamiento anómalo antes de ejecutar el código de negocio. Esto permite activar y desactivar el caos sin modificar el código de la función.&lt;/p&gt;

&lt;p&gt;La extensión &lt;strong&gt;chaos-lambda-extension&lt;/strong&gt; es otro patrón que opera dentro del ambiente de ejecución de Lambda: se registra como una extension que se ejecuta junto al runtime, intercepta las invocaciones a través de la Extension API y la Runtime API, e inyecta los fallos configurados antes de que el código de la función se ejecute. Es más invasiva que las Lambda Layers pero también más precisa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Técnicas de resiliencia para responder al caos
&lt;/h2&gt;

&lt;p&gt;Identificar dónde falla el sistema es la mitad del trabajo. La otra mitad es construir resiliencia que haga que los fallos sean manejables. Hay tres principios fundamentales para aplicaciones serverless con tolerancia a fallos:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redundancia&lt;/strong&gt; significa tener múltiples instancias de los componentes críticos — en el contexto serverless, esto se traduce en deployments multi-región, DLQs para mensajes fallidos, y funciones de respaldo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Degradación elegante&lt;/strong&gt; implica mantener funcionalidad parcial cuando un componente falla. En lugar de retornar un error 500, la aplicación retorna una respuesta reducida pero funcional — datos en caché, respuestas por defecto, o funcionalidad limitada con un mensaje claro al usuario.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recuperación automatizada&lt;/strong&gt; significa que los procesos de recuperación de fallos no dependen de intervención manual. Los runbooks deben ejecutarse solos.&lt;/p&gt;

&lt;p&gt;A nivel de código, las tres técnicas más importantes son lógica de reintento con backoff exponencial, circuit breakers, y throttling.&lt;/p&gt;

&lt;p&gt;La &lt;strong&gt;lógica de reintento con backoff exponencial&lt;/strong&gt; es el patrón básico de resiliencia para fallos transitorios. En Python con boto3, se traduce a reintentar la operación hasta N veces, con un &lt;code&gt;time.sleep(2 ** attempt)&lt;/code&gt; entre intentos — los tiempos de espera crecen exponencialmente para no sobrecargar un servicio que ya está bajo presión.&lt;/p&gt;

&lt;p&gt;Los &lt;strong&gt;circuit breakers&lt;/strong&gt; son más sofisticados: un objeto que rastrea el número de fallos consecutivos y, cuando supera un umbral, abre el circuito durante un período de recuperación. Durante ese período, las llamadas fallan inmediatamente sin intentar la operación, dando tiempo al servicio dependiente de recuperarse. Una vez pasado el timeout de recuperación, el circuito se cierra y las operaciones se reanudan.&lt;/p&gt;

&lt;p&gt;Los &lt;strong&gt;límites de throttling&lt;/strong&gt; en API Gateway, configurados a través de UsagePlans con BurstLimit y RateLimit, protegen los backends de ser sobrecargados bajo picos de tráfico inesperados.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mejores prácticas
&lt;/h2&gt;

&lt;p&gt;Independientemente de las herramientas que uses, hay cinco principios que aplican a cualquier práctica de ingeniería del caos:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comenzar pequeño.&lt;/strong&gt; El primer experimento no debe ser "apagamos una región". Empieza con latencia en una función, en un ambiente de staging, con radio de blast controlado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automatizar.&lt;/strong&gt; Los experimentos ejecutados manualmente son inconsistentes y difíciles de repetir. Automatizar la ejecución, el monitoreo y la condición de paro desde el principio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitorear de cerca.&lt;/strong&gt; No ejecutes un experimento y te vayas a hacer otra cosa. El valor está en observar el comportamiento en tiempo real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comunicar.&lt;/strong&gt; El equipo necesita saber cuándo se está ejecutando un experimento, qué se está probando, y cuál es el plan de rollback. Los experimentos no anunciados generan pánico innecesario.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentar.&lt;/strong&gt; Cada experimento, sus resultados, y los cambios que generó deben quedar registrados. La ingeniería del caos sin documentación es simplemente romper cosas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resumen
&lt;/h2&gt;

&lt;p&gt;La ingeniería del caos en entornos serverless no es una práctica de nicho — es una necesidad para cualquier sistema que aspire a alta disponibilidad. AWS FIS hace que sea significativamente más accesible.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>aws</category>
      <category>serverless</category>
      <category>testing</category>
    </item>
    <item>
      <title>Integrando IA generativa con Bases de Datos relacionales en AWS</title>
      <dc:creator>Franchesco Romero</dc:creator>
      <pubDate>Thu, 02 Apr 2026 20:15:07 +0000</pubDate>
      <link>https://dev.to/aws-builders/integrando-ia-generativa-con-bases-de-datos-relacionales-en-aws-3phn</link>
      <guid>https://dev.to/aws-builders/integrando-ia-generativa-con-bases-de-datos-relacionales-en-aws-3phn</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://speakerdeck.com/elchesco/integrando-ia-generativa-con-bases-de-datos-relacionales-en-aws" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ffiles.speakerdeck.com%2Fpresentations%2F91b88230200d42b6a87409f324b7d474%2Fslide_0.jpg%3F38968718" height="1080" class="m-0" width="1920"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://speakerdeck.com/elchesco/integrando-ia-generativa-con-bases-de-datos-relacionales-en-aws" rel="noopener noreferrer" class="c-link"&gt;
            Integrando IA generativa con Bases de Datos relacionales en AWS - Speaker Deck
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            En esta charla mostraremos cómo AWS Bedrock puede conectarse a una base de datos tradicional, comprender su estructura, y generar consultas a partir de &amp;amp;hellip;
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fd1eu30co0ohy4w.cloudfront.net%2Fassets%2Ffavicon-bdd5839d46040a50edf189174e6f7aacc8abb3aaecd56a4711cf00d820883f47.png" width="512" height="512"&gt;
          speakerdeck.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h1&gt;
  
  
  Integrando IA Generativa con Bases de Datos Relacionales en AWS
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Foundation Models y el ecosistema de Bedrock
&lt;/h2&gt;

&lt;p&gt;Antes de entrar en la implementación, vale la pena aclarar los conceptos base. La IA Generativa, en términos prácticos, es un tipo de IA capaz de crear nuevo contenido: textos, conversaciones, resúmenes, imágenes. &lt;/p&gt;

&lt;p&gt;Los Foundation Models son los modelos de aprendizaje automático que hacen esto posible, entrenados sobre cantidades masivas de datos no etiquetados y luego adaptados para tareas específicas como generación de texto, extracción de información, Q&amp;amp;A o chatbots. &lt;/p&gt;

&lt;p&gt;AWS Bedrock es el servicio que te da acceso a varios de estos modelos — desde Claude de Anthropic hasta Titan de Amazon, Llama 2 de Meta, Mistral, Cohere y Stable Diffusion — a través de una API unificada, sin tener que administrar infraestructura de ML por tu cuenta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arquitectura
&lt;/h2&gt;

&lt;p&gt;La arquitectura que propuse es deliberadamente simple: un usuario interactúa con una aplicación, esta invoca una Lambda Function, la Lambda se conecta a Amazon Aurora, y Aurora llama a Bedrock directamente usando &lt;strong&gt;Aurora ML&lt;/strong&gt;. Este componente es la pieza central del patrón — Aurora ML es una característica de Amazon Aurora (disponible desde la versión 3.06 en adelante) que permite invocar algoritmos de machine learning directamente desde SQL. Esto significa que puedes hacer un &lt;code&gt;SELECT&lt;/code&gt; que internamente llama a un Foundation Model en Bedrock y regresa el resultado como si fuera cualquier otro valor de la base de datos. La simpleza de esto es lo que hace la integración tan poderosa: los datos nunca salen de Aurora, no hay ETL, no hay pipelines intermedios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-requisitos
&lt;/h2&gt;

&lt;p&gt;La configuración requiere cuatro pasos previos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Crear un IAM Role con los permisos necesarios para que Aurora pueda llamar a Bedrock, y asignar ese rol al cluster.&lt;/li&gt;
&lt;li&gt;Habilitar los modelos base en la consola de Bedrock para la región donde está tu cluster — esto es un paso explícito que AWS requiere antes de poder usarlos.&lt;/li&gt;
&lt;li&gt;Crear las funciones SQL que mapean a cada modelo de Bedrock.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Este último paso es lo más interesante: con apenas un &lt;code&gt;CREATE FUNCTION&lt;/code&gt; que usa el alias &lt;code&gt;AWS_BEDROCK_INVOKE_MODEL&lt;/code&gt; y especifica el MODEL ID, ya tienes disponible en tu base de datos una función que puedes llamar desde cualquier query o stored procedure. En la demo creé dos funciones, una para Amazon Titan y otra para Claude 3 Haiku.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;invoke_titan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_body&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="k"&gt;ALIAS&lt;/span&gt; &lt;span class="n"&gt;AWS_BEDROCK_INVOKE_MODEL&lt;/span&gt;
&lt;span class="n"&gt;MODEL&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="s1"&gt;'amazon.titan-text-express-v1'&lt;/span&gt;
&lt;span class="n"&gt;CONTENT_TYPE&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;
&lt;span class="n"&gt;ACCEPT&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;claude3_haiku&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_body&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="k"&gt;ALIAS&lt;/span&gt; &lt;span class="n"&gt;AWS_BEDROCK_INVOKE_MODEL&lt;/span&gt;
&lt;span class="n"&gt;MODEL&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="s1"&gt;'anthropic.claude-3-haiku-20240307-v1:0'&lt;/span&gt;
&lt;span class="n"&gt;CONTENT_TYPE&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;
&lt;span class="n"&gt;ACCEPT&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Casos de uso
&lt;/h2&gt;

&lt;p&gt;Para ilustrar los patrones usé el dataset público de películas de IMDb, que incluye títulos, calificaciones, géneros e información de personas.&lt;/p&gt;

&lt;h3&gt;
  
  
  Insights de cultura pop
&lt;/h3&gt;

&lt;p&gt;El primer caso fue generar análisis culturales sobre películas de los 90s. Un stored procedure itera sobre las películas más populares de la década, construye un prompt con el título, año y géneros de cada una, lo pasa a Claude 3 Haiku vía la función SQL, y guarda el resultado en una tabla de &lt;code&gt;title_insights&lt;/code&gt;. El modelo recibe el contexto de que la película fue lanzada en los 90s y genera análisis sobre qué reflejaba sobre el momento histórico, qué tendencias capturaba y por qué resonó con su audiencia. Todo esto ejecutado desde un simple &lt;code&gt;CALL generate_90s_insights()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consultas en lenguaje natural
&lt;/h3&gt;

&lt;p&gt;El segundo caso fue más interesante: un stored procedure &lt;code&gt;natural_language_query&lt;/code&gt; recibe una pregunta en texto libre — por ejemplo, &lt;em&gt;"Encuentra las 10 películas mejor calificadas de la década de 1990"&lt;/em&gt; — y hace dos cosas. Primero, le pasa esa pregunta a Bedrock junto con el schema de la base de datos como contexto, pidiendo que genere únicamente el SQL necesario para responderla. Segundo, ejecuta ese SQL generado dinámicamente con &lt;code&gt;PREPARE&lt;/code&gt; y &lt;code&gt;EXECUTE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;El truco está en el prompt engineering: hay que ser muy explícito en las instrucciones — solo SQL ejecutable, sin explicaciones, sin markdown, sin backticks — y luego limpiar la respuesta con &lt;code&gt;REGEXP_REPLACE&lt;/code&gt; antes de ejecutarla. El resultado es que un usuario sin conocimiento de SQL puede hacer preguntas en lenguaje natural y obtener resultados reales de la base de datos.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resumen de películas
&lt;/h3&gt;

&lt;p&gt;El tercer caso fue enriquecimiento en batch: un procedimiento que toma películas sin resumen en la base de datos, construye un prompt con título, año y géneros, y le pide a Claude que genere una descripción breve. El resultado se guarda directamente con un &lt;code&gt;UPDATE&lt;/code&gt;. Un &lt;code&gt;CALL generate_movie_summaries()&lt;/code&gt; es todo lo que necesitas para enriquecer un catálogo entero.&lt;/p&gt;

&lt;h3&gt;
  
  
  Casos avanzados
&lt;/h3&gt;

&lt;p&gt;Más allá de estos tres patrones básicos, la integración abre posibilidades más sofisticadas: análisis de query plans con sugerencias de índices, detección de anomalías en datos con propuestas de corrección automática, o análisis de evolución de schemas. En todos los casos el patrón es el mismo — datos de Aurora como contexto, Foundation Model como motor de razonamiento, resultado guardado de vuelta en la base de datos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mejores prácticas
&lt;/h2&gt;

&lt;p&gt;Hay cuatro áreas que vale subrayar:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safe defaults.&lt;/strong&gt; Configurar defaults seguros tanto a nivel de base de datos como del cliente, para evitar que errores del modelo afecten datos críticos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chain of Transformations.&lt;/strong&gt; Pensar en los flujos de enriquecimiento como cadenas de transformaciones explícitas: &lt;code&gt;get_data()&lt;/code&gt; → &lt;code&gt;enrich_with_ai()&lt;/code&gt; → &lt;code&gt;validate_data()&lt;/code&gt; → &lt;code&gt;store_in_db()&lt;/code&gt;. No mezclar estas responsabilidades en un solo procedimiento.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt Engineering con templates.&lt;/strong&gt; La calidad del output depende directamente de la calidad del prompt. Vale la pena invertir tiempo en construir plantillas bien estructuradas y reutilizables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching, rate limiting y manejo de errores.&lt;/strong&gt; Implementar caching inteligente para consultas similares, rate limiting para evitar resource exhaustion cuando el volumen escala, y manejo de errores robusto para cuando el modelo devuelve algo inesperado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observabilidad
&lt;/h2&gt;

&lt;p&gt;Las métricas que más importan en este tipo de integración son:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Latencia de generación de prompts&lt;/li&gt;
&lt;li&gt;Tasa de éxito y fallo de llamadas al LLM&lt;/li&gt;
&lt;li&gt;Calidad de respuestas (requiere algún tipo de evaluación downstream)&lt;/li&gt;
&lt;li&gt;Consumo de tokens y costos asociados&lt;/li&gt;
&lt;li&gt;Cache hit/miss ratio&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Consideraciones
&lt;/h2&gt;

&lt;p&gt;El control de acceso a datos sensibles es una preocupación válida. Bedrock Guardrails y el abuse detection nativo son los mecanismos disponibles para esto. También hay que tener en cuenta las limitaciones inherentes de precisión de los modelos, que hacen necesario validar outputs antes de usarlos en decisiones críticas.&lt;/p&gt;

&lt;p&gt;Para quienes prefieran alternativas open source, el patrón equivalente sería PostgreSQL o MariaDB conectado a LangChain, con LLaMA 2 o Mistral como modelo, y un Vector Store para búsqueda semántica. Es una arquitectura perfectamente válida, con más flexibilidad pero también más infraestructura que administrar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resumen
&lt;/h2&gt;

&lt;p&gt;Este patrón es valioso por tres razones concretas: facilita el acceso a datos complejos sin mover nada fuera de la base de datos, puede incorporarse a infraestructuras Aurora existentes sin rediseño mayor, y aprovecha Foundation Models de clase mundial con la misma interfaz SQL que tu equipo ya conoce.&lt;/p&gt;

&lt;p&gt;El código de la demo está disponible en GitHub bajo Amazon Bedrock and Aurora MySQL Integration.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>database</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>AWS mas allá de la nube con IAM Anywhere y Amazon Verified Permissions</title>
      <dc:creator>Franchesco Romero</dc:creator>
      <pubDate>Thu, 02 Apr 2026 18:46:26 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-mas-alla-de-la-nube-con-iam-anywhere-y-amazon-verified-permissions-18h8</link>
      <guid>https://dev.to/aws-builders/aws-mas-alla-de-la-nube-con-iam-anywhere-y-amazon-verified-permissions-18h8</guid>
      <description>&lt;p&gt;Cuando hablamos de seguridad en AWS, la conversación casi siempre empieza y termina en IAM. &lt;/p&gt;

&lt;p&gt;Pero hay un escenario que muchas organizaciones enfrentan y que no siempre tiene una respuesta obvia: ¿qué pasa cuando el workload que necesita acceder a recursos en AWS no vive dentro de AWS?&lt;/p&gt;

&lt;p&gt;Hay compañías con regulaciones estrictas de seguridad y cumplimiento que mantienen parte de su infraestructura detrás de data centers propios o firewalls corporativos. Hay equipos en medio de migraciones o modernizaciones que todavía tienen servidores tradicionales que necesitan hablar con servicios en la nube. En todos estos casos, la solución más obvia — usar access keys — trae consigo una lista de problemas conocidos: pueden olvidarse, necesitan rotarse periódicamente, hay que controlar cómo se exponen, y las sesiones expiran de formas que a veces nadie monitorea. Sin mencionar que la trazabilidad de quién usó qué credencial y cuándo se vuelve una pesadilla de auditoría.&lt;/p&gt;

&lt;p&gt;La respuesta que propusimos es IAM Roles Anywhere, un servicio que permite emitir credenciales temporales de AWS para cargas de trabajo que viven fuera de la nube, ya sean servidores bare metal, contenedores on-prem o cualquier aplicación corriendo en un data center corporativo. &lt;/p&gt;

&lt;p&gt;El mecanismo de verificación se basa en certificados X.509 y PKI, lo que significa que en lugar de manejar access keys estáticas, estás manejando una infraestructura de confianza basada en certificados — algo que los equipos de seguridad entienden y controlan mucho mejor.&lt;/p&gt;

&lt;p&gt;Primero, una Certificate Authority emite certificados para cada carga de trabajo, confirmando que son entidades reconocidas dentro de tu infraestructura. &lt;br&gt;
Luego se configura un Trust Anchor en AWS que referencia esa CA — usando AWS Private CA es la ruta más integrada, aunque también puedes usar Let's Encrypt u otra CA pública si entiendes las implicaciones de a quién le estás otorgando confianza. &lt;br&gt;
Finalmente, cuando una carga de trabajo necesita credenciales, presenta su certificado firmado por la CA, firma la solicitud con su llave privada, y AWS devuelve credenciales temporales para el IAM Role asociado. &lt;br&gt;
Todo esto orquestado por el aws_signing_helper, el credential helper oficial que hace que el proceso sea transparente para tus aplicaciones.&lt;/p&gt;

&lt;p&gt;En Terraform, la implementación se divide en tres recursos principales: la aws_acmpca_certificate_authority para levantar la CA privada, el aws_rolesanywhere_trust_anchor que la referencia y sirve como punto de confianza, y el aws_rolesanywhere_profile que define qué IAM Role puede ser asumido por qué carga de trabajo. El IAM Role en sí se configura de la misma forma que cualquier otro, con la diferencia de que el principal en el assume role policy es rolesanywhere.amazonaws.com y las acciones incluyen sts:SetSourceIdentity para mejor trazabilidad.&lt;/p&gt;

&lt;p&gt;Una de las cosas que más me gusta de esta arquitectura es que el monitoreo queda completamente integrado con los servicios nativos de AWS. CloudWatch Metrics te da visibilidad sobre expiración de certificados e intentos de login fallidos. &lt;/p&gt;

&lt;p&gt;CloudTrail registra todos los eventos relevantes de API. Y con EventBridge puedes construir automatizaciones sobre cualquiera de esos eventos — por ejemplo, una alerta cuando un certificado está próximo a expirar o cuando hay un patrón anómalo de intentos fallidos.&lt;/p&gt;

&lt;p&gt;En cuanto a mejores prácticas, la recomendación central es tratar esto exactamente como tratas cualquier estrategia de IAM: un rol por carga de trabajo, mínimo privilegio, y roles exclusivamente asumibles por rolesanywhere para no abrir superficies de ataque innecesarias. &lt;/p&gt;

&lt;p&gt;IAM Roles Anywhere como servicio no tiene costo — lo que sí tiene costo es AWS Private CA, que vale la pena evaluar contra alternativas como usar una CA pública, siempre considerando el modelo de confianza que eso implica. &lt;/p&gt;

&lt;p&gt;También es importante configurar soporte para CRL (Certificate Revocation Lists) desde el principio, porque si un certificado se pierde o se compromete, necesitas poder revocarlo sin tener que reconstruir toda la cadena de confianza.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
