<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Marco Orta — Blog</title><description>Qué se rompe al actualizar tu stack, IA para desarrolladores y tecnología para negocios en México. Verificado contra la fuente.</description><link>https://ortamarco.me/</link><language>es-MX</language><atom:link href="https://ortamarco.me/rss.xml" rel="self" type="application/rss+xml"/><item><title>Laravel Octane + FrankenPHP: qué se rompe de verdad (reproduje cada fuga)</title><link>https://ortamarco.me/blog/laravel-octane-frankenphp-que-se-rompe/</link><guid isPermaLink="true">https://ortamarco.me/blog/laravel-octane-frankenphp-que-se-rompe/</guid><description>Monté Laravel 13 en Octane con FrankenPHP y reproduje cada fuga. La advertencia famosa del singleton está mal planteada y --max-requests=0 ni siquiera arranca.</description><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Octane mantiene tu aplicación de Laravel en memoria entre peticiones. De ahí sale la velocidad, y de ahí sale todo lo que se rompe. Casi todos los artículos de «trampas de Octane» repiten la misma advertencia: un singleton que recibe la petición en su constructor se queda con la primera petición para siempre. Monté una app de Laravel 13 en Octane con FrankenPHP dentro de Docker y reproduje cada fallo con &lt;code&gt;curl&lt;/code&gt;, y esa advertencia, tal como se suele contar, es falsa. Las fugas que sí ocurren vienen de otro lado.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Tres resultados me sorprendieron. Un singleton que captura la petición y se resuelve &lt;em&gt;durante&lt;/em&gt; una petición &lt;strong&gt;no&lt;/strong&gt; filtra nada, porque Octane atiende cada petición sobre un clon de la aplicación. &lt;code&gt;--max-requests=0&lt;/code&gt;, que en Swoole y RoadRunner significa «sin límite», hace que FrankenPHP &lt;strong&gt;ni siquiera arranque&lt;/strong&gt;. Y las funciones que la documentación marca como exclusivas de Swoole no lanzan error en FrankenPHP: casi todas hacen otra cosa en silencio. Todo lo que sigue va con el comando y la salida.&lt;/p&gt;
&lt;p&gt;Si todavía no decides si Octane te conviene, empieza por &lt;a href=&quot;https://ortamarco.me/blog/optimizacion-rendimiento-proyecto-laravel-guia/&quot;&gt;la guía de rendimiento de Laravel&lt;/a&gt;; este post es para el paso siguiente, cuando ya vas a encenderlo.&lt;/p&gt;
&lt;h2 id=&quot;el-laboratorio&quot;&gt;El laboratorio&lt;/h2&gt;





























&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Componente&lt;/th&gt;&lt;th&gt;Versión&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Laravel&lt;/td&gt;&lt;td&gt;13.32.0 (el esqueleto pide PHP ^8.3)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;laravel/octane&lt;/td&gt;&lt;td&gt;2.19.1&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;FrankenPHP&lt;/td&gt;&lt;td&gt;1.12.7, PHP 8.5.10 ZTS, Caddy 2.11.4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Imagen&lt;/td&gt;&lt;td&gt;&lt;code&gt;dunglas/frankenphp:latest&lt;/code&gt; (Debian 13) + &lt;code&gt;pcntl&lt;/code&gt; + Composer&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Carga&lt;/td&gt;&lt;td&gt;&lt;code&gt;wrk&lt;/code&gt;, en CPUs separadas&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Un esqueleto limpio, nueve clases pequeñas y un archivo de rutas. Octane corre con &lt;code&gt;php artisan octane:frankenphp&lt;/code&gt;; el modo de comparación es el &lt;code&gt;php-server&lt;/code&gt; clásico de FrankenPHP, que arranca Laravel en cada petición igual que PHP-FPM.&lt;/p&gt;
&lt;h2 id=&quot;antes-de-arrancar-pcntl-zip-y-el-binario-musl&quot;&gt;Antes de arrancar: pcntl, zip y el binario musl&lt;/h2&gt;
&lt;p&gt;Tres cosas estorbaron antes de la primera petición:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;La imagen oficial de FrankenPHP no trae &lt;code&gt;pcntl&lt;/code&gt;.&lt;/strong&gt; Sin la extensión, &lt;code&gt;octane:frankenphp&lt;/code&gt; muere de inmediato:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Error  Undefined constant &amp;quot;Laravel\Octane\Commands\Concerns\SIGINT&amp;quot;
at vendor/laravel/octane/src/Commands/Concerns/InteractsWithServers.php:174
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El Dockerfile de la documentación de Octane ejecuta &lt;code&gt;install-php-extensions pcntl&lt;/code&gt; justo por eso. Copia esa línea.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tampoco trae &lt;code&gt;zip&lt;/code&gt; ni &lt;code&gt;unzip&lt;/code&gt;&lt;/strong&gt;, así que &lt;code&gt;composer require&lt;/code&gt; falla dentro de ella. Instala las dependencias en una etapa con la imagen de Composer y copia &lt;code&gt;vendor/&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;En Alpine, &lt;code&gt;octane:install&lt;/code&gt; descarga el binario musl.&lt;/strong&gt; Si no hay &lt;code&gt;frankenphp&lt;/code&gt; en el PATH, Octane lo descarga, y elige la versión glibc solo si &lt;code&gt;getconf GNU_LIBC_VERSION&lt;/code&gt; funciona. En Alpine no funciona, así que bajas los 173 MB del build musl. La guía de rendimiento de FrankenPHP dice «evita musl en producción» porque PHP es más lento sobre ella, sobre todo en modo ZTS. En mi ruta de prueba, que es trivial, no medí diferencia (5,900 peticiones por segundo contra 5,800-6,100 en glibc), pero la &lt;a href=&quot;https://frankenphp.dev/docs/known-issues/&quot;&gt;página de problemas conocidos&lt;/a&gt; lista huecos concretos, como que no existe &lt;code&gt;GLOB_BRACE&lt;/code&gt;. Usa las imágenes Debian.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;1-singletons-la-fuga-que-todos-describen-no-es-la-que-muerde&quot;&gt;1. Singletons: la fuga que todos describen no es la que muerde&lt;/h2&gt;
&lt;p&gt;El binding que todos los artículos ponen de ejemplo:&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// AppServiceProvider::register()&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;singleton&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name static-context&quot;&gt;RequestEcho&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name type-declaration&quot;&gt;Application&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RequestEcho&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;request&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Resuelto dentro de una ruta, con un solo worker para que todas las peticiones caigan en el mismo:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;?user=alice → {&amp;quot;query_user&amp;quot;:&amp;quot;alice&amp;quot;,&amp;quot;singleton_sees&amp;quot;:&amp;quot;alice&amp;quot;}
?user=bob   → {&amp;quot;query_user&amp;quot;:&amp;quot;bob&amp;quot;,&amp;quot;singleton_sees&amp;quot;:&amp;quot;bob&amp;quot;}
?user=carol → {&amp;quot;query_user&amp;quot;:&amp;quot;carol&amp;quot;,&amp;quot;singleton_sees&amp;quot;:&amp;quot;carol&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nada se filtra. La razón está en &lt;code&gt;Worker::handle()&lt;/code&gt; de Octane: cada petición corre sobre &lt;code&gt;$sandbox = clone $this-&amp;gt;app&lt;/code&gt;, y un singleton resuelto en ese clon muere con él al terminar la petición. La advertencia famosa solo es cierta en dos situaciones, y son las que tienes que buscar.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;El singleton se resuelve durante el arranque.&lt;/strong&gt; La &lt;a href=&quot;https://laravel.com/docs/13.x/octane&quot;&gt;documentación de Octane&lt;/a&gt; lo dice con precisión: el problema es cuando la instancia «se resuelve durante el proceso de arranque de la aplicación». Resolví la misma clase en &lt;code&gt;boot()&lt;/code&gt;, como haría un paquete que se carga de inmediato:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;?user=alice → {&amp;quot;query_user&amp;quot;:&amp;quot;alice&amp;quot;,&amp;quot;singleton_sees&amp;quot;:null,&amp;quot;singleton_url&amp;quot;:&amp;quot;http://localhost:8000&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No captura la primera petición. Captura la &lt;strong&gt;petición falsa de consola&lt;/strong&gt; que Laravel registra mientras arranca (la URL es &lt;code&gt;APP_URL&lt;/code&gt; y no trae datos), y se queda con ella en todas las peticiones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Algo lo resuelve a través de la aplicación base.&lt;/strong&gt; Esta sí captura la primera petición. Un listener registrado en &lt;code&gt;boot()&lt;/code&gt; que resuelve a través de &lt;code&gt;$this-&amp;gt;app&lt;/code&gt;, que es la aplicación original que Octane clona, no el clon:&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// AppServiceProvider::boot(). Aquí todavía no se resuelve nada.&lt;/span&gt;
&lt;span class=&quot;token class-name static-context&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;lab.who&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;make&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name static-context&quot;&gt;RequestEchoViaProvider&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;?user=alice → &amp;quot;alice&amp;quot;
?user=bob   → &amp;quot;alice&amp;quot;
?user=carol → &amp;quot;alice&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La primera petición resolvió el singleton sobre la app base, y ahí se quedó durante toda la vida del worker. Agregar la clase a &lt;code&gt;flush&lt;/code&gt; en &lt;code&gt;config/octane.php&lt;/code&gt; lo arregló (&lt;code&gt;alice&lt;/code&gt;, &lt;code&gt;bob&lt;/code&gt;, &lt;code&gt;carol&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;El mismo mecanismo convierte un singleton con estado en una fuga de datos entre usuarios. Un singleton &lt;code&gt;CurrentTenant&lt;/code&gt; que vive en la app base (porque algo lo resolvió al arrancar), con un middleware que lo llena solo cuando la petición trae &lt;code&gt;X-Tenant&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;-H &amp;#39;X-Tenant: acme&amp;#39; → {&amp;quot;singleton_tenant&amp;quot;:&amp;quot;acme&amp;quot;,&amp;quot;scoped_tenant&amp;quot;:&amp;quot;acme&amp;quot;}
(sin cabecera)      → {&amp;quot;x_tenant_header&amp;quot;:null,&amp;quot;singleton_tenant&amp;quot;:&amp;quot;acme&amp;quot;,&amp;quot;scoped_tenant&amp;quot;:null}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La petición anónima heredó &lt;code&gt;acme&lt;/code&gt;. Con cuatro workers es peor de depurar: solo el worker que atendió a &lt;code&gt;acme&lt;/code&gt; quedó contaminado, así que una de cada cuatro peticiones salía mal. La misma clase registrada con &lt;code&gt;scoped()&lt;/code&gt; en vez de &lt;code&gt;singleton()&lt;/code&gt; devolvió &lt;code&gt;null&lt;/code&gt; en todas las peticiones sin cabecera.&lt;/p&gt;
&lt;p&gt;Qué hacer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Usa &lt;code&gt;scoped()&lt;/code&gt; para todo lo que guarde estado de la petición (tenant, usuario actual, idioma).&lt;/li&gt;
&lt;li&gt;No resuelvas en &lt;code&gt;boot()&lt;/code&gt; servicios que dependan de la petición, y busca listeners y macros registrados en &lt;code&gt;boot()&lt;/code&gt; que llamen a &lt;code&gt;$this-&amp;gt;app-&amp;gt;make()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Lo que no puedas cambiar (un paquete), agrégalo a &lt;code&gt;flush&lt;/code&gt; en &lt;code&gt;config/octane.php&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A los servicios que necesitan la petición o la configuración, inyéctales un resolvedor, &lt;code&gt;fn () =&amp;gt; Container::getInstance()&lt;/code&gt;, en vez del objeto.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;2-el-estado-estático-crece-hasta-que-el-worker-se-reinicia&quot;&gt;2. El estado estático crece hasta que el worker se reinicia&lt;/h2&gt;
&lt;p&gt;Un arreglo estático que suma 10 KB por petición, que es lo que hace una caché en memoria ingenua o un registro de «ya procesados»:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;req#1    worker dcb87b  items 1    mem_kb 5667
req#100                 items 100  mem_kb 7001
req#300                 items 300  mem_kb 9410
req#500                 items 500  mem_kb 11810
req#501  worker c5125d  items 1    mem_kb 1253   ← reinicio en --max-requests=500
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unos 12 KB por petición, que solo se liberan cuando Octane reinicia el worker a las 500 peticiones, el valor por defecto. El reinicio no aparece en los logs y cuesta poco: 1.85 ms antes, 4.40 ms en la primera petición del worker nuevo y 2.19 ms después.&lt;/p&gt;
&lt;p&gt;Una trampa si lo pruebas tú: mi primera versión usaba &lt;code&gt;str_repeat(&amp;#39;x&amp;#39;, 10240)&lt;/code&gt; y la memoria se quedó &lt;strong&gt;plana&lt;/strong&gt; durante 500 peticiones. OPcache convierte esa expresión constante en una sola cadena compartida, así que todos los elementos del arreglo apuntaban a la misma memoria. Con &lt;code&gt;Str::random()&lt;/code&gt; sí apareció la fuga. Si tu prueba de fuga sale limpia, comprueba que los datos de verdad cambian en cada petición.&lt;/p&gt;
&lt;h2 id=&quot;3---max-requests0-no-significa-sin-límite-en-frankenphp&quot;&gt;3. &lt;code&gt;--max-requests=0&lt;/code&gt; no significa «sin límite» en FrankenPHP&lt;/h2&gt;
&lt;p&gt;En &lt;a href=&quot;https://openswoole.com/docs/modules/swoole-server/configuration&quot;&gt;OpenSwoole&lt;/a&gt;, &lt;code&gt;max_request&lt;/code&gt; en 0 significa sin límite, y en &lt;a href=&quot;https://docs.roadrunner.dev/docs/php-worker/pool&quot;&gt;RoadRunner&lt;/a&gt; «cero (o nada) significa sin límite» para &lt;code&gt;max_jobs&lt;/code&gt;. En FrankenPHP:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan octane:frankenphp --max-requests&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Error: loading initial config: … frankenphp app module: start: failed to initialize workers:
too many consecutive failures: worker public/frankenphp-worker.php has not reached frankenphp_handle_request().
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El contenedor sale con código 1. El script del worker de Octane hace &lt;code&gt;while ($requestCount &amp;lt; $maxRequests …)&lt;/code&gt;, así que con 0 nunca atiende una petición; FrankenPHP ve seis workers seguidos que no llegan a &lt;code&gt;frankenphp_handle_request()&lt;/code&gt; y se rinde. Con &lt;code&gt;octane:start --server=frankenphp --max-requests=0&lt;/code&gt; el servidor sí arranca, pero el worker recibe &lt;code&gt;MAX_REQUESTS=500&lt;/code&gt;: un &lt;code&gt;?:&lt;/code&gt; en el comando cambia el 0 por el valor de la configuración sin avisarte. Los dos pull requests que habrían hecho que 0 signifique «sin límite» se cerraron sin fusionarse.&lt;/p&gt;
&lt;p&gt;Si quieres algo casi ilimitado, pasa un número grande (&lt;code&gt;--max-requests=1000000&lt;/code&gt; midió el mismo rendimiento que 500). Pero los reinicios son lo que te protege de la sección 2, así que yo dejaría el valor por defecto.&lt;/p&gt;
&lt;h2 id=&quot;4-la-configuración-inyectada-en-un-singleton-se-queda-vieja-y-puede-envenenar-el-worker&quot;&gt;4. La configuración inyectada en un singleton se queda vieja, y puede envenenar el worker&lt;/h2&gt;
&lt;p&gt;Un singleton que recibe el repositorio de configuración en su constructor, y una petición que cambia un valor en tiempo de ejecución (configuración por tenant, por ejemplo):&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;?set=changed-in-request → {&amp;quot;config_helper&amp;quot;:&amp;quot;changed-in-request&amp;quot;,&amp;quot;injected_repo&amp;quot;:&amp;quot;original&amp;quot;,
                           &amp;quot;resolver_closure&amp;quot;:&amp;quot;changed-in-request&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El repositorio inyectado es el de la app base, así que dentro de la petición no ve el cambio. El caso peor es el contrario: una petición que &lt;strong&gt;escribe&lt;/strong&gt; a través de ese repositorio inyectado (&lt;code&gt;$service-&amp;gt;config-&amp;gt;set(...)&lt;/code&gt;) cambia la configuración de la app base, y desde entonces todas las peticiones siguientes de ese worker leen &lt;code&gt;mutated-via-service&lt;/code&gt;. En modo clásico nada de esto sobrevive a la petición. El arreglo es el que da la documentación: inyecta un closure que resuelva la configuración, o llama a &lt;code&gt;config()&lt;/code&gt; cuando necesites el valor.&lt;/p&gt;
&lt;h2 id=&quot;5-lo-que-añade-frankenphp-hilos-_env-y-exit&quot;&gt;5. Lo que añade FrankenPHP: hilos, &lt;code&gt;$_ENV&lt;/code&gt; y &lt;code&gt;exit()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Hilos, no procesos.&lt;/strong&gt; Los cuatro workers reportaron el mismo PID, y &lt;code&gt;/proc&lt;/code&gt; mostró un solo proceso &lt;code&gt;frankenphp&lt;/code&gt; con 67 hilos. La documentación de FrankenPHP lo dice («hilos en vez de procesos»), y importa por dos razones: no puedes usar extensiones que no sean &lt;em&gt;thread-safe&lt;/em&gt; (la página de problemas conocidos lista &lt;code&gt;imap&lt;/code&gt;, &lt;code&gt;newrelic&lt;/code&gt; y &lt;code&gt;pcov&lt;/code&gt;, y avisa que &lt;code&gt;imagick&lt;/code&gt; en las imágenes Docker puede tronar por sus hilos de OpenMP), y un fallo en cualquier hilo tumba el proceso entero, con todos los workers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;$_ENV&lt;/code&gt; y &lt;code&gt;putenv()&lt;/code&gt; sobreviven entre peticiones.&lt;/strong&gt; FrankenPHP reinicia &lt;code&gt;$_GET&lt;/code&gt;, &lt;code&gt;$_POST&lt;/code&gt;, &lt;code&gt;$_SERVER&lt;/code&gt; y los demás, pero su &lt;a href=&quot;https://frankenphp.dev/docs/worker/&quot;&gt;documentación del modo worker&lt;/a&gt; dice que «&lt;code&gt;$_ENV&lt;/code&gt; actualmente no se reinicia entre peticiones». Lo probé con &lt;code&gt;putenv()&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;?v=secret-from-alice → {&amp;quot;prev_getenv&amp;quot;:null}
(petición siguiente) → {&amp;quot;prev_$_ENV&amp;quot;:&amp;quot;secret-from-alice&amp;quot;,&amp;quot;prev_$_SERVER&amp;quot;:null,
                        &amp;quot;prev_getenv&amp;quot;:&amp;quot;secret-from-alice&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Con cuatro workers, solo el hilo que lo escribió vio el valor. Si algún código guarda datos de la petición en el entorno (algunos SDK ponen credenciales así), se le filtran al siguiente usuario que atienda ese hilo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;exit()&lt;/code&gt; devuelve 200.&lt;/strong&gt; &lt;code&gt;exit(0)&lt;/code&gt; y &lt;code&gt;exit(1)&lt;/code&gt; mandaron lo que llevaban de salida con &lt;strong&gt;HTTP 200&lt;/strong&gt; y reiniciaron el worker. Doce &lt;code&gt;exit(1)&lt;/code&gt; seguidos: doce 200, y el servidor aguantó. &lt;code&gt;dd()&lt;/code&gt; devolvió 500 y también reinició. Un monitoreo que solo mira el código de estado no va a ver un script que muere a la mitad.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;El límite de 30 segundos es por petición.&lt;/strong&gt; Octane fija &lt;code&gt;max_execution_time&lt;/code&gt; en 30 segundos. Dos peticiones de 20 segundos seguidas en el mismo worker devolvieron 200; una de 35 devolvió 500 a los 31.2 s, con «Maximum execution time of 30 seconds exceeded» en el log, y el worker se reinició.&lt;/p&gt;
&lt;p&gt;Issues abiertos que conviene conocer antes de desplegar (en &lt;code&gt;php/frankenphp&lt;/code&gt;, abiertos en septiembre de 2026):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/php/frankenphp/issues/2588&quot;&gt;#2588&lt;/a&gt;: segfault del worker de Octane (salida 139). Quien lo reportó lo arregló cambiando &lt;code&gt;opcache.jit&lt;/code&gt; de &lt;code&gt;tracing&lt;/code&gt; a &lt;code&gt;function&lt;/code&gt;, y lo atribuye a una regresión del JIT &lt;em&gt;tracing&lt;/em&gt; de PHP 8.5.5 en adelante.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/php/frankenphp/issues/1074&quot;&gt;#1074&lt;/a&gt;: las subidas de archivos simultáneas se quedan colgadas por HTTPS; el mantenedor lo reprodujo en julio con 30 subidas multipart sobre una sola conexión HTTP/2.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/php/frankenphp/issues/2388&quot;&gt;#2388&lt;/a&gt;: la imagen Alpine truena con cientos de variables de entorno (los &lt;em&gt;service links&lt;/em&gt; de Kubernetes son el origen típico).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Y actualiza Octane: antes de la 2.18.0, un POST &lt;code&gt;multipart/form-data&lt;/code&gt; sin &lt;em&gt;boundary&lt;/em&gt; tiraba la petición. Con el script de worker viejo me devolvió un 500 silencioso, sin nada en &lt;code&gt;laravel.log&lt;/code&gt;; con la 2.19.1 se atiende normal.&lt;/p&gt;
&lt;h2 id=&quot;6-las-funciones-exclusivas-de-swoole-fallan-en-silencio&quot;&gt;6. Las funciones exclusivas de Swoole fallan en silencio&lt;/h2&gt;
&lt;p&gt;La documentación marca las tareas concurrentes, los &lt;em&gt;ticks&lt;/em&gt;, la caché de Octane y las tablas como funciones de Swoole. Esto es lo que hacen en FrankenPHP:&lt;/p&gt;

























&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Función&lt;/th&gt;&lt;th&gt;En FrankenPHP&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Octane::concurrently()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Ejecuta las tareas &lt;strong&gt;una tras otra&lt;/strong&gt;: dos de 300 ms tardaron 600 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Cache::store(&amp;#39;octane&amp;#39;)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Funciona, pero es &lt;strong&gt;una caché distinta por worker&lt;/strong&gt;: un valor guardado en uno de cuatro workers no existía en los otros tres&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Octane::tick()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Se registra sin error y &lt;strong&gt;nunca se ejecuta&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Octane::table()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Lanza «Tables may only be accessed when using the Swoole server.»&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Solo la última te avisa. Si vienes de Swoole, busca las tres primeras con grep: el código va a seguir corriendo, solo que ya no hace lo que hacía.&lt;/p&gt;
&lt;h2 id=&quot;7-paquetes-casi-todos-bien-en-2026&quot;&gt;7. Paquetes: casi todos bien en 2026&lt;/h2&gt;
&lt;p&gt;Octane trae sus propios listeners de reinicio para Livewire, Inertia, Scout y Socialite. Las versiones actuales de los sospechosos de siempre ya lo resuelven solas: Filament reinicia sus gestores de componentes en cada petición, Telescope escucha los eventos de petición de Octane, Debugbar 4 «funciona sin configuración con Octane» (si vienes de la 3.x, quita su entrada de &lt;code&gt;flush&lt;/code&gt; en &lt;code&gt;config/octane.php&lt;/code&gt;) e Inertia 3 arregló sus herramientas de desarrollo bajo Octane. El que hay que configurar es &lt;code&gt;spatie/laravel-permission&lt;/code&gt;: su listener de reinicio para Octane viene apagado, y su documentación dice que lo enciendas si los permisos cacheados se ven viejos o se cruzan entre peticiones.&lt;/p&gt;
&lt;h2 id=&quot;vale-la-pena-los-números&quot;&gt;¿Vale la pena? Los números&lt;/h2&gt;
&lt;p&gt;Una microprueba en una laptop, no una promesa de producción: una ruta que devuelve un JSON pequeño, &lt;code&gt;APP_ENV=production&lt;/code&gt;, el servidor fijado a 4 CPUs (8 workers) y &lt;code&gt;wrk&lt;/code&gt; en CPUs aparte, 20 segundos por corrida.&lt;/p&gt;















































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Modo&lt;/th&gt;&lt;th style=&quot;text-align:right&quot;&gt;Conexiones&lt;/th&gt;&lt;th style=&quot;text-align:right&quot;&gt;Peticiones/s&lt;/th&gt;&lt;th style=&quot;text-align:right&quot;&gt;Latencia mediana&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Octane (worker de FrankenPHP)&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;16&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;~6,000&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;2.5 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;FrankenPHP clásico, con &lt;code&gt;optimize&lt;/code&gt;&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;16&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;~2,900&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;5 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;FrankenPHP clásico, &lt;strong&gt;sin&lt;/strong&gt; caché de config y rutas&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;16&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;1,345&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;11 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Octane&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;64&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;~5,150&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;12 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;FrankenPHP clásico, con &lt;code&gt;optimize&lt;/code&gt;&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;64&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;~2,490&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;24 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;php artisan serve&lt;/code&gt;&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;64&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;~315&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;204 ms&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Octane duplicó el rendimiento frente a un modo clásico bien cacheado, y lo multiplicó por 4.5 frente a uno sin &lt;code&gt;php artisan optimize&lt;/code&gt;. La comparación honesta es la primera: si tu producción no cachea configuración y rutas, arregla eso antes de meter Octane. Y en una app real la ganancia depende de cuánto pesa el arranque frente a tu I/O; una ruta que espera 200 ms a la base de datos gana poco con ahorrarse 3 ms de arranque.&lt;/p&gt;
&lt;aside class=&quot;not-prose my-8 flex flex-wrap items-center gap-4 rounded-2xl border border-neutral-100 bg-white p-5 transition-all duration-200 hover:shadow-[5px_5px_rgba(0,98,90,0.3),10px_10px_rgba(0,98,90,0.2),15px_15px_rgba(0,98,90,0.1)] dark:border-zinc-800 dark:bg-zinc-900/40&quot; style=&quot;border-left:4px solid #3b82f6&quot;&gt; &lt;span class=&quot;grid size-12 shrink-0 place-items-center rounded-xl&quot; style=&quot;background:#3b82f61a;color:#3b82f6&quot;&gt;  &lt;/span&gt; &lt;div class=&quot;flex min-w-0 flex-1 flex-col gap-0.5&quot;&gt; &lt;span class=&quot;text-xs font-semibold tracking-wide text-zinc-500 uppercase dark:text-zinc-400&quot;&gt; Herramienta gratuita &lt;/span&gt; &lt;span class=&quot;text-lg leading-snug font-bold text-blacktext dark:text-mint-50&quot;&gt; Validador de Docker Compose &lt;/span&gt; &lt;span class=&quot;text-sm text-pretty text-zinc-600 dark:text-zinc-400&quot;&gt; Valida tu compose.yaml contra el esquema oficial de Compose y detecta erratas en las claves, tipos equivocados y errores de YAML, con la línea exacta. Todo en tu navegador. &lt;/span&gt; &lt;/div&gt; &lt;a href=&quot;https://ortamarco.me/herramientas/validador-docker-compose/&quot; data-umami-event=&quot;tool_callout_click&quot; data-umami-event-tool=&quot;validador-docker-compose&quot; class=&quot;ml-auto shrink-0 rounded-xl bg-mint-600 px-4 py-2 text-sm! font-semibold text-white! no-underline! transition-colors hover:bg-mint-700 hover:text-white!&quot;&gt; Abrir herramienta → &lt;/a&gt; &lt;/aside&gt;
&lt;h2 id=&quot;la-revisión-antes-de-cambiar&quot;&gt;La revisión antes de cambiar&lt;/h2&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Servicios que guardan estado o reciben request/config en el constructor&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rnE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;singleton\(|-&amp;gt;instance\(&amp;quot;&lt;/span&gt; app/Providers/
&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rnE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;__construct\([^)]*(Request|Repository|Container)&amp;quot;&lt;/span&gt; app/

&lt;span class=&quot;token comment&quot;&gt;# Propiedades estáticas que acumulan&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rnE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;static (array|&lt;span class=&quot;token entity&quot; title=&quot;\\&quot;&gt;\\&lt;/span&gt;\$)&amp;quot;&lt;/span&gt; app/ &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;function&amp;quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Resolución a través de la app base desde boot()&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rnE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;&lt;span class=&quot;token entity&quot; title=&quot;\\&quot;&gt;\\&lt;/span&gt;\&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;-&amp;gt;app-&amp;gt;(make|get)\(&amp;quot;&lt;/span&gt; app/Providers/

&lt;span class=&quot;token comment&quot;&gt;# Lo que FrankenPHP trata distinto&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rnE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;putenv\(|&lt;span class=&quot;token entity&quot; title=&quot;\\&quot;&gt;\\&lt;/span&gt;\&lt;span class=&quot;token variable&quot;&gt;$_ENV&lt;/span&gt;\[|&lt;span class=&quot;token entity&quot; title=&quot;\b&quot;&gt;\b&lt;/span&gt;exit\(|&lt;span class=&quot;token entity&quot; title=&quot;\b&quot;&gt;\b&lt;/span&gt;die\(&amp;quot;&lt;/span&gt; app/ routes/

&lt;span class=&quot;token comment&quot;&gt;# APIs de Swoole que se degradan en silencio&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rnE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;Octane::(concurrently|tick|table)|store\(&amp;#39;octane&amp;#39;\)&amp;quot;&lt;/span&gt; app/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Después, en orden: instala &lt;code&gt;pcntl&lt;/code&gt; en la imagen, usa una imagen Debian, corre tu suite de pruebas, haz una prueba de carga contra el servidor de Octane con al menos cuatro workers (con uno solo no ves las fugas que solo pasan en uno de cada cuatro) y deja &lt;code&gt;--max-requests&lt;/code&gt; en su valor por defecto. Si además vas a subir a Laravel 13, el lado del framework está en &lt;a href=&quot;https://ortamarco.me/blog/laravel-13-breaking-changes/&quot;&gt;qué se rompe al actualizar a Laravel 13&lt;/a&gt;.&lt;/p&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Un singleton que recibe la petición se filtra entre peticiones en Laravel Octane? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No si se resuelve durante una petición: Octane atiende cada petición sobre un clon de la aplicación, y los singletons resueltos en el clon se descartan al terminar. Probado en Laravel 13.32 con Octane 2.19.1 y FrankenPHP 1.12.7. Sí se filtra cuando se resuelve durante el arranque (captura la petición de consola que Laravel registra al arrancar) o a través de la aplicación base, por ejemplo desde un listener registrado en boot() que llama a $this-&amp;gt;app-&amp;gt;make(); en ese caso se queda con la primera petición durante toda la vida del worker. Usa scoped(), agrega la clase a flush en config/octane.php o inyecta un closure que la resuelva. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué hace --max-requests=0 con Octane y FrankenPHP? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Impide que el servidor arranque. Con php artisan octane:frankenphp --max-requests=0, el script del worker nunca llega a frankenphp_handle_request() y FrankenPHP sale con «too many consecutive failures». Con octane:start el servidor arranca pero usa en silencio el valor por defecto de 500. En Swoole y RoadRunner, 0 significa sin límite. Usa un número grande si quieres evitar reinicios, pero el valor de 500 es lo que limpia las fugas de memoria. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué funciones de Octane no sirven con FrankenPHP? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Las tareas concurrentes, los ticks, la caché de Octane y las tablas son funciones de Swoole. En FrankenPHP, Octane::concurrently() ejecuta las tareas una tras otra, el almacén de caché octane es un arreglo distinto por worker, Octane::tick() se registra pero nunca se ejecuta y solo Octane::table() lanza una excepción. Las tres primeras fallan en silencio. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Por qué Laravel Octane falla con «Undefined constant SIGINT» en FrankenPHP? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Porque la imagen oficial dunglas/frankenphp no incluye la extensión pcntl, y Octane usa sus constantes de señales. Instálala con install-php-extensions pcntl, como hace el Dockerfile de la documentación de Octane. La imagen tampoco trae zip ni unzip, así que instala las dependencias de Composer en una etapa aparte. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿$_ENV se reinicia entre peticiones en el modo worker de FrankenPHP? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No. FrankenPHP reinicia $_GET, $_POST, $_COOKIE, $_FILES, $_SERVER y $_REQUEST entre peticiones, pero su documentación dice que $_ENV actualmente no se reinicia. En la prueba, un valor puesto con putenv() en una petición lo vio la siguiente petición del mismo hilo. No guardes en el entorno datos de la petición ni datos sensibles. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuánto más rápido es Laravel con Octane y FrankenPHP? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; En una microprueba en laptop con una ruta JSON mínima, Octane atendió unas 6,000 peticiones por segundo contra unas 2,900 del modo clásico de FrankenPHP con caché de configuración y rutas, más o menos el doble, y 4.5 veces más que el modo clásico sin caché. La ganancia real depende de cuánto de cada petición es arranque del framework y cuánto es esperar a la base de datos u otro I/O. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Desarrollo Web</category><category>Laravel</category><category>Octane</category><category>FrankenPHP</category><category>PHP</category><category>Rendimiento</category><category>Migración</category><author>Marco Orta</author></item><item><title>Meta prohibió los chatbots de IA genéricos en WhatsApp: qué cambia para tu negocio en México (y qué no)</title><link>https://ortamarco.me/blog/meta-prohibe-chatbots-ia-whatsapp-negocio/</link><guid isPermaLink="true">https://ortamarco.me/blog/meta-prohibe-chatbots-ia-whatsapp-negocio/</guid><description>ChatGPT ya no está en WhatsApp en México, pero el bot que atiende a tus clientes sí puede estar. Qué dice la regla, qué se permite y qué cambia el 1 de octubre.</description><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Desde el 15 de enero de 2026 ya no puedes hablar con ChatGPT, Copilot ni Perplexity desde WhatsApp en México. Mucha gente lo leyó como «Meta prohibió los bots de IA», y circulan artículos que avisan a los negocios que les van a suspender el número por usar inteligencia artificial. No es eso. La regla de Meta va contra las empresas que ofrecen un asistente de IA de propósito general &lt;em&gt;como producto&lt;/em&gt;, no contra el negocio que usa IA para atender a sus propios clientes. Eso sigue permitido, y la propia documentación de Meta lo dice.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Lo que sí cambia para un negocio mexicano son otras tres cosas, y ninguna tiene que ver con la prohibición. Desde el 1 de octubre Meta cobra las respuestas de atención, las de IA incluidas, a partir del mensaje 1,001 del mes. Meta ya vende su propio agente de IA para negocios. Y los términos se reorganizan el 23 de septiembre. Abajo va cada cosa con su fuente.&lt;/p&gt;
&lt;h2 id=&quot;qué-dice-la-regla-palabra-por-palabra&quot;&gt;Qué dice la regla, palabra por palabra&lt;/h2&gt;
&lt;p&gt;La cláusula está en los &lt;a href=&quot;https://www.whatsapp.com/legal/business-solution-terms&quot;&gt;términos de la solución empresarial de WhatsApp&lt;/a&gt; (última modificación: 6 de marzo de 2026), bajo el título «AI Providers». Traducida:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Los proveedores y desarrolladores de tecnologías de inteligencia artificial o aprendizaje automático, incluidos, entre otros, los modelos de lenguaje grandes, las plataformas de IA generativa, los asistentes de IA de propósito general o tecnologías similares según lo determine Meta a su entera discreción («Proveedores de IA»), tienen estrictamente prohibido acceder o usar la WhatsApp Business Solution, directa o indirectamente, para proveer, entregar, ofrecer, vender o poner a disposición dichas tecnologías &lt;strong&gt;cuando dichas tecnologías sean la funcionalidad principal (y no incidental o accesoria)&lt;/strong&gt; que se ofrece.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Y dos frases después viene la que importa a un negocio:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;No obstante lo anterior, &lt;strong&gt;puedes contratar a un Proveedor de IA como tu Proveedor de Servicios Externo&lt;/strong&gt; conforme a estos términos.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Es decir: OpenAI no puede usar WhatsApp para ofrecerte ChatGPT. Tu negocio sí puede contratar a OpenAI (o a quien sea) para que su modelo conteste a &lt;em&gt;tus&lt;/em&gt; clientes sobre &lt;em&gt;tu&lt;/em&gt; negocio. La &lt;a href=&quot;https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing/non-template-messages&quot;&gt;documentación de precios de Meta&lt;/a&gt; lo confirma sin rodeos: los mensajes de servicio «pueden ser atendidos por una persona, como un representante de atención al cliente, o por una solución de IA de terceros». Y la Comisión Europea, al abrir su investigación, lo resumió igual: los negocios pueden seguir usando herramientas de IA «para funciones accesorias o de soporte, como la atención automatizada al cliente».&lt;/p&gt;
&lt;p&gt;La regla aplicó a los proveedores nuevos desde el 15 de octubre de 2025 y a los que ya estaban desde el 15 de enero de 2026, según la &lt;a href=&quot;https://ec.europa.eu/commission/presscorner/api/documents?reference=IP/25/2896&amp;amp;language=en&quot;&gt;Comisión Europea&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;quién-se-fue-de-whatsapp-y-quién-volvió&quot;&gt;Quién se fue de WhatsApp (y quién volvió)&lt;/h2&gt;





























&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Asistente&lt;/th&gt;&lt;th&gt;Qué pasó&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ChatGPT&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Tenía más de 50 millones de usuarios en WhatsApp. Se fue el 15 de enero; OpenAI pidió vincular la cuenta porque las conversaciones no se transferían (&lt;a href=&quot;https://openai.com/index/chatgpt-whatsapp-transition/&quot;&gt;OpenAI&lt;/a&gt;). &lt;strong&gt;Volvió el 13 de julio, solo para números de Europa&lt;/strong&gt; (&lt;a href=&quot;https://help.openai.com/en/articles/6825453-chatgpt-release-notes&quot;&gt;notas de versión&lt;/a&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Microsoft Copilot&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Se fue el 15 de enero, sin conservar el historial&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Perplexity&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Anunció el cierre de su número de WhatsApp&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Luzia&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Muy usado en España y Latinoamérica. Movió a sus usuarios a su propia app, y el 30 de junio lanzó un asistente nuevo en WhatsApp con número español, que cabe en la excepción europea&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Meta AI&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Se queda. Desde el 15 de enero es el único asistente de IA disponible dentro de WhatsApp, según la Comisión Europea&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Si tu número es +52, hoy el único asistente de propósito general dentro de WhatsApp es el de Meta.&lt;/p&gt;
&lt;h2 id=&quot;por-qué-en-europa-y-brasil-sí-hay-chatgpt&quot;&gt;Por qué en Europa y Brasil sí hay ChatGPT&lt;/h2&gt;
&lt;p&gt;Porque los reguladores de allá obligaron a Meta a dar marcha atrás, y la versión vigente de la cláusula ya incluye la excepción: la IA general puede ofrecerse a usuarios con número de un país del Espacio Económico Europeo o de Brasil.&lt;/p&gt;

































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Fecha&lt;/th&gt;&lt;th&gt;Qué pasó&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;24 dic 2025&lt;/td&gt;&lt;td&gt;La autoridad de competencia italiana (AGCM) ordena suspender los términos en Italia (&lt;a href=&quot;https://en.agcm.it/en/media/press-releases/2025/12/A576&quot;&gt;AGCM&lt;/a&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;12 ene 2026&lt;/td&gt;&lt;td&gt;El CADE de Brasil abre una investigación y suspende los términos como medida preventiva (&lt;a href=&quot;https://www.gov.br/cade/pt-br/assuntos/noticias/cade-abre-inquerito-contra-meta-e-aplica-medida-preventiva-suspendendo-novos-termos-do-whatsapp-sobre-ia&quot;&gt;CADE&lt;/a&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;9 feb 2026&lt;/td&gt;&lt;td&gt;La Comisión Europea, que investiga desde el 4 de diciembre, envía su pliego de cargos&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;4 mar 2026&lt;/td&gt;&lt;td&gt;Meta readmite a los rivales en Europa, pero cobrándoles por mensaje&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;23 abr 2026&lt;/td&gt;&lt;td&gt;El CADE confirma una multa diaria de 250,000 reales porque Meta cobraba a los chatbots (&lt;a href=&quot;https://www.gov.br/cade/en/matters/news/cade-upholds-daily-fine-against-meta-and-whatsapp-for-failure-to-comply-with-interim-measure&quot;&gt;CADE&lt;/a&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;9 jun 2026&lt;/td&gt;&lt;td&gt;La Comisión Europea ordena restablecer el acceso gratuito en cinco días hábiles (&lt;a href=&quot;https://ec.europa.eu/commission/presscorner/api/documents?reference=IP/26/1276&amp;amp;language=en&quot;&gt;Comisión&lt;/a&gt;); Meta responde que apelará&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;En México no hay nada parecido. La Comisión Nacional Antimonopolio, que sustituyó a la Cofece en octubre de 2025, no ha publicado ninguna investigación sobre estos términos. Así que aquí la regla aplica completa.&lt;/p&gt;
&lt;h2 id=&quot;lo-que-tu-negocio-sí-puede-hacer-con-ia-en-whatsapp&quot;&gt;Lo que tu negocio sí puede hacer con IA en WhatsApp&lt;/h2&gt;
&lt;p&gt;Todo lo que es atender a tus clientes sobre tu negocio: contestar horarios y precios, recomendar productos de tu catálogo, agendar citas, tomar pedidos, mandar el link de pago, dar seguimiento. Con tres condiciones que vienen de los propios documentos de Meta:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Tiene que haber una salida a una persona.&lt;/strong&gt; La &lt;a href=&quot;https://whatsappbusiness.com/policy/&quot;&gt;política de mensajería de WhatsApp Business&lt;/a&gt; permite automatizar las respuestas, pero exige «rutas de escalamiento rápidas, claras y directas»: un agente humano en el chat, un teléfono, un correo, un formulario o una visita a la tienda. Un bot que nunca deja hablar con alguien incumple la política, aunque no tenga nada que ver con la cláusula de IA.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Las conversaciones no pueden usarse para entrenar modelos de otros.&lt;/strong&gt; Los términos prohíben que los datos de tus clientes sirvan para crear o mejorar sistemas de IA, salvo un modelo ajustado para tu uso exclusivo. Pregúntale a tu proveedor si entrena con tus datos: OpenAI, por ejemplo, dice que en su API &lt;a href=&quot;https://openai.com/enterprise-privacy/&quot;&gt;no entrena con ellos por defecto&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El bot habla de tu negocio.&lt;/strong&gt; La zona gris es un número cuya oferta principal sea «pregúntame lo que quieras», aunque lo tenga una tienda. Meta no ha publicado dónde está la línea entre lo «principal» y lo «accesorio», y se reserva decidirlo «a su entera discreción». Un bot que contesta sobre tu tienda está claramente del lado permitido; un ChatGPT con tu logotipo que igual te ayuda con la tarea, no.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Un aviso sobre lo que circula: no encontré ningún caso documentado de números de negocios suspendidos por esta cláusula. Los artículos que hablan de «suspensiones masivas» no citan ninguna.&lt;/p&gt;
&lt;p&gt;Y si lo que te interesa es que tus clientes te escriban más, eso no depende de la IA: un QR que abre tu chat con un mensaje ya escrito, pegado en el mostrador o en el ticket, funciona desde el primer día.&lt;/p&gt;
&lt;aside class=&quot;not-prose my-8 flex flex-wrap items-center gap-4 rounded-2xl border border-neutral-100 bg-white p-5 transition-all duration-200 hover:shadow-[5px_5px_rgba(0,98,90,0.3),10px_10px_rgba(0,98,90,0.2),15px_15px_rgba(0,98,90,0.1)] dark:border-zinc-800 dark:bg-zinc-900/40&quot; style=&quot;border-left:4px solid #3b82f6&quot;&gt; &lt;span class=&quot;grid size-12 shrink-0 place-items-center rounded-xl&quot; style=&quot;background:#3b82f61a;color:#3b82f6&quot;&gt;  &lt;/span&gt; &lt;div class=&quot;flex min-w-0 flex-1 flex-col gap-0.5&quot;&gt; &lt;span class=&quot;text-xs font-semibold tracking-wide text-zinc-500 uppercase dark:text-zinc-400&quot;&gt; Herramienta gratuita &lt;/span&gt; &lt;span class=&quot;text-lg leading-snug font-bold text-blacktext dark:text-mint-50&quot;&gt; Generador de Códigos QR &lt;/span&gt; &lt;span class=&quot;text-sm text-pretty text-zinc-600 dark:text-zinc-400&quot;&gt; Crea códigos QR de URL, WhatsApp, WiFi, vCard de contacto, correo, teléfono o SMS, con tu logo en el centro. PNG o SVG, sin marca de agua ni registro, y nada sale de tu navegador. &lt;/span&gt; &lt;/div&gt; &lt;a href=&quot;https://ortamarco.me/herramientas/generador-qr/&quot; data-umami-event=&quot;tool_callout_click&quot; data-umami-event-tool=&quot;generador-qr&quot; class=&quot;ml-auto shrink-0 rounded-xl bg-mint-600 px-4 py-2 text-sm! font-semibold text-white! no-underline! transition-colors hover:bg-mint-700 hover:text-white!&quot;&gt; Abrir herramienta → &lt;/a&gt; &lt;/aside&gt;
&lt;h2 id=&quot;lo-que-sí-cambia-para-ti-y-no-tiene-que-ver-con-la-prohibición&quot;&gt;Lo que sí cambia para ti (y no tiene que ver con la prohibición)&lt;/h2&gt;
&lt;h3 id=&quot;1-desde-el-1-de-octubre-las-respuestas-de-atención-se-cobran&quot;&gt;1. Desde el 1 de octubre, las respuestas de atención se cobran&lt;/h3&gt;
&lt;p&gt;Según la &lt;a href=&quot;https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing&quot;&gt;página de precios de Meta&lt;/a&gt;, a partir del 1 de octubre de 2026 los mensajes de servicio (los que respondes cuando el cliente te escribe primero) se cobran a la misma tarifa que los de utilidad, con &lt;strong&gt;1,000 gratis al mes por número&lt;/strong&gt;, sin acumular. Las respuestas de un bot de IA son mensajes de servicio, así que cuentan igual que las de una persona. Y si tu cuenta no tiene método de pago dado de alta antes del 30 de septiembre, a partir del mensaje 1,001 del mes Meta deja de entregar tus respuestas. El detalle técnico, con lo que cambia en el webhook, está en &lt;a href=&quot;https://ortamarco.me/blog/whatsapp-api-cobro-1-octubre-webhook/&quot;&gt;qué cambia en tu código el 1 de octubre&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;2-meta-ya-vende-su-propio-agente-de-ia&quot;&gt;2. Meta ya vende su propio agente de IA&lt;/h3&gt;
&lt;p&gt;El 3 de junio Meta anunció &lt;a href=&quot;https://about.fb.com/news/2026/06/meta-business-agent/&quot;&gt;Meta Business Agent&lt;/a&gt;, y lo que promete no es poco: contestar preguntas sobre tu negocio, recomendar productos de tu catálogo, &lt;strong&gt;agendar citas, calificar prospectos y cerrar ventas&lt;/strong&gt;. Empezar es gratis, pero Meta ya avisó que se accederá con suscripción. En la API se cobra desde el 1 de agosto: 2 dólares por millón de tokens, que Meta calcula en unos 4 a 5 centavos de dólar por mensaje. Y el 15 de septiembre presentó &lt;a href=&quot;https://about.fb.com/news/2026/09/introducing-meta-one-subscription-service-more-features-ai/&quot;&gt;Meta One&lt;/a&gt;, con un plan que parte de 14.99 dólares al mes e incluye más uso del agente; los precios y la disponibilidad varían por país.&lt;/p&gt;
&lt;p&gt;Según TechCrunch, Meta lleva casi dos años probando este agente en México y la India, así que es probable que te lo ofrezcan dentro de la app. La pregunta no es si es bueno, sino si te conviene que tu atención dependa de las reglas y los precios de la misma empresa que es dueña del canal, que es justo lo que están investigando en Europa.&lt;/p&gt;
&lt;h3 id=&quot;3-los-términos-se-reorganizan-el-23-de-septiembre&quot;&gt;3. Los términos se reorganizan el 23 de septiembre&lt;/h3&gt;
&lt;p&gt;WhatsApp separa ese día sus términos en dos documentos, uno para la app de WhatsApp Business y otro para la plataforma (la API). Según su &lt;a href=&quot;https://faq.whatsapp.com/1017485114093363&quot;&gt;centro de ayuda&lt;/a&gt; es una reorganización, pero el texto nuevo todavía no está publicado. Lo reviso cuando salga y actualizo aquí si la cláusula de IA cambia.&lt;/p&gt;
&lt;h2 id=&quot;el-agente-de-meta-o-uno-conectado-a-tu-negocio&quot;&gt;¿El agente de Meta o uno conectado a tu negocio?&lt;/h2&gt;
&lt;p&gt;Depende de qué necesitas que haga:&lt;/p&gt;



































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Agente de Meta&lt;/th&gt;&lt;th&gt;Agente propio sobre la API&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Puesta en marcha&lt;/td&gt;&lt;td&gt;Desde la app, en minutos&lt;/td&gt;&lt;td&gt;Días o semanas&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;De dónde saca la información&lt;/td&gt;&lt;td&gt;Tu catálogo y lo que le configures en Meta&lt;/td&gt;&lt;td&gt;Tus sistemas: inventario, calendario, cobro&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Qué modelo usa&lt;/td&gt;&lt;td&gt;El de Meta&lt;/td&gt;&lt;td&gt;El que elijas, con garantía de no entrenamiento&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Qué pagas&lt;/td&gt;&lt;td&gt;Tokens de Meta o suscripción, más los mensajes&lt;/td&gt;&lt;td&gt;Los mensajes de Meta más el modelo que uses&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Reglas y precios&lt;/td&gt;&lt;td&gt;Los decide Meta&lt;/td&gt;&lt;td&gt;Los mensajes, Meta; lo demás, tú&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Para un negocio que vende lo que tiene en el catálogo y contesta dudas repetidas, el de Meta puede bastar. Cuando la respuesta depende de algo que vive fuera de Meta (si hay existencia hoy, si el horario del doctor tiene hueco, cuánto sale el envío a tu colonia), hace falta uno conectado a tus sistemas. Así funciona &lt;a href=&quot;https://ortamarco.me/whatsapp-para-tu-negocio/&quot;&gt;el agente de WhatsApp que monto para negocios&lt;/a&gt;, y el lado técnico lo expliqué en &lt;a href=&quot;https://ortamarco.me/blog/agente-ia-whatsapp-pyme-laravel-openai/&quot;&gt;cómo montar un agente de IA en WhatsApp con Laravel y OpenAI&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;cinco-preguntas-para-tu-proveedor-de-bot&quot;&gt;Cinco preguntas para tu proveedor de bot&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;¿Mi número usa la API oficial de WhatsApp o una app no oficial? (Las no oficiales pueden ser bloqueadas por Meta aunque no usen IA.)&lt;/li&gt;
&lt;li&gt;¿El bot contesta solo sobre mi negocio, o responde cualquier cosa?&lt;/li&gt;
&lt;li&gt;¿Cómo pasa la conversación a una persona, y en cuánto tiempo?&lt;/li&gt;
&lt;li&gt;¿Mis conversaciones se usan para entrenar algún modelo?&lt;/li&gt;
&lt;li&gt;¿Mi cuenta de Meta tiene método de pago antes del 30 de septiembre, y quién paga los mensajes que pasen de 1,000 al mes?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Si alguna respuesta es «no sé», es la que hay que resolver primero.&lt;/p&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Meta prohibió los chatbots de IA en WhatsApp? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Prohibió a los proveedores de IA ofrecer asistentes de propósito general (como ChatGPT, Copilot o Perplexity) a través de la API de WhatsApp cuando la IA es la función principal del servicio. Aplica desde el 15 de enero de 2026 para los que ya estaban y desde el 15 de octubre de 2025 para los nuevos. No prohibió que los negocios usen IA para atender a sus propios clientes. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Puedo usar un bot de IA en el WhatsApp de mi negocio en 2026? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Sí. Los términos de WhatsApp permiten contratar a un proveedor de IA como proveedor de servicios, y la documentación de precios de Meta dice que los mensajes de servicio pueden ser atendidos por una persona o por una solución de IA de terceros. Las condiciones: que haya una forma clara de hablar con una persona, que las conversaciones no se usen para entrenar modelos de otros y que el bot atienda sobre tu negocio. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Por qué ChatGPT sí funciona en WhatsApp en España y no en México? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Porque la autoridad de competencia de Italia, la Comisión Europea y el CADE de Brasil ordenaron a Meta suspender la restricción, y los términos vigentes (del 6 de marzo de 2026) permiten ofrecer IA de propósito general a números de países del Espacio Económico Europeo y de Brasil. ChatGPT volvió a WhatsApp en Europa el 13 de julio de 2026. En México no hay una investigación pública y la restricción aplica completa. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Me pueden suspender el número por usar IA en WhatsApp? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Meta puede cerrar cuentas que incumplan sus términos, y la línea entre usar IA como función principal o accesoria la decide Meta a su entera discreción. Un bot que atiende sobre tu negocio está del lado permitido; un número cuya oferta principal es un asistente de preguntas generales está en riesgo. No hay casos públicos documentados de números de negocios suspendidos por esta cláusula. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuánto cuesta el agente de IA de Meta para negocios? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Meta Business Agent se anunció el 3 de junio de 2026 y empezar es gratis, pero Meta avisó que se accederá por suscripción. En la API se cobra desde el 1 de agosto de 2026 a 2 dólares por millón de tokens, unos 4 a 5 centavos de dólar por mensaje según Meta. El plan Meta One presentado el 15 de septiembre parte de 14.99 dólares al mes e incluye más uso del agente; precio y disponibilidad varían por país. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Las respuestas de un bot de IA en WhatsApp se cobran? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Desde el 1 de octubre de 2026 sí, como cualquier mensaje de servicio: Meta regala 1,000 al mes por número y cobra los siguientes a la tarifa de utilidad de cada país. Da igual si responde una persona o una IA. Si la cuenta no tiene método de pago, Meta deja de entregar las respuestas a partir de la respuesta número 1,001 del mes. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Noticias</category><category>WhatsApp</category><category>Inteligencia Artificial</category><category>Chatbots</category><category>PyMEs</category><category>Meta</category><author>Marco Orta</author></item><item><title>OpenAI apaga GPT-4, o1 y o3-mini el 23 de octubre: qué se rompe en tu código (y no es solo el nombre del modelo)</title><link>https://ortamarco.me/blog/openai-apaga-gpt-4-23-octubre-que-se-rompe/</link><guid isPermaLink="true">https://ortamarco.me/blog/openai-apaga-gpt-4-23-octubre-que-se-rompe/</guid><description>Cambiar gpt-4 por gpt-5.6-sol es lo fácil. Después se rompen los defaults de LangChain, las tools en Chat Completions, max_tokens y los fine-tunes. Con greps y precios.</description><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;El 23 de octubre de 2026 OpenAI apaga &lt;code&gt;gpt-3.5-turbo&lt;/code&gt;, &lt;code&gt;gpt-4&lt;/code&gt;, &lt;code&gt;gpt-4-turbo&lt;/code&gt;, &lt;code&gt;o1&lt;/code&gt;, &lt;code&gt;o1-pro&lt;/code&gt;, &lt;code&gt;o3-mini&lt;/code&gt;, &lt;code&gt;o4-mini&lt;/code&gt;, &lt;code&gt;gpt-4.1-nano&lt;/code&gt;, un snapshot de &lt;code&gt;gpt-4o&lt;/code&gt; y &lt;code&gt;gpt-image-1&lt;/code&gt;. La página de deprecaciones hace que el arreglo parezca cambiar un texto: poner &lt;code&gt;gpt-5.6-sol&lt;/code&gt;, &lt;code&gt;-terra&lt;/code&gt; o &lt;code&gt;-luna&lt;/code&gt; donde estaba el nombre viejo. Ese cambio es lo fácil, y en mucho código ni siquiera es donde está el problema.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Después de cambiar el nombre se rompen cuatro cosas, y una más se rompe en código donde el nombre nunca aparece: LangChain y LlamaIndex siguen usando &lt;code&gt;gpt-3.5-turbo&lt;/code&gt; por defecto, así que un proyecto que jamás escribió un nombre de modelo usa uno que muere en 32 días. Abajo va todo, verificado contra la documentación de OpenAI tal como estaba el 21 de septiembre y contra el código fuente de las librerías. Donde algo sale de reportes de usuarios y no de OpenAI, lo digo.&lt;/p&gt;
&lt;p&gt;Si lo que necesitas es el calendario completo de todos los proveedores (Anthropic, Gemini, Azure) y cómo saber si el chatbot de un negocio está afectado, está en &lt;a href=&quot;https://ortamarco.me/blog/retiran-el-modelo-de-ia-que-usa-tu-negocio/&quot;&gt;la guía de retiros de modelos de IA&lt;/a&gt;. Este post es el lado del código.&lt;/p&gt;
&lt;h2 id=&quot;qué-se-apaga-y-qué-dice-openai-que-uses&quot;&gt;Qué se apaga y qué dice OpenAI que uses&lt;/h2&gt;
&lt;p&gt;Sale de la &lt;a href=&quot;https://developers.openai.com/api/docs/deprecations&quot;&gt;página de deprecaciones&lt;/a&gt;, sección «2026-04-22: Legacy GPT model snapshots». OpenAI lo anunció por correo el 22 de abril; la columna de sustitutos se editó después, porque nombra modelos que salieron en julio. Los precios son de nivel estándar, por millón de tokens, entrada / salida, según la &lt;a href=&quot;https://developers.openai.com/api/docs/pricing&quot;&gt;página de precios&lt;/a&gt;:&lt;/p&gt;


















































































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Se apaga el 23 de octubre&lt;/th&gt;&lt;th&gt;Precio&lt;/th&gt;&lt;th&gt;Sustituto&lt;/th&gt;&lt;th&gt;Precio&lt;/th&gt;&lt;th&gt;Cambio&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;gpt-3.5-turbo&lt;/code&gt; (&lt;code&gt;-0125&lt;/code&gt;, &lt;code&gt;-completions&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;$0.50 / $1.50&lt;/td&gt;&lt;td&gt;&lt;code&gt;gpt-5.6-terra&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$2 / $12&lt;/td&gt;&lt;td&gt;&lt;strong&gt;4× / 8×&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;gpt-4&lt;/code&gt; (&lt;code&gt;-0613&lt;/code&gt;, &lt;code&gt;-completions&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;$30 / $60&lt;/td&gt;&lt;td&gt;&lt;code&gt;gpt-5.6-sol&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$4 / $20&lt;/td&gt;&lt;td&gt;más barato&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;gpt-4-turbo&lt;/code&gt; (&lt;code&gt;-2024-04-09&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;$10 / $30&lt;/td&gt;&lt;td&gt;&lt;code&gt;gpt-5.6-sol&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$4 / $20&lt;/td&gt;&lt;td&gt;más barato&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;gpt-4o-2024-05-13&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$5 / $15&lt;/td&gt;&lt;td&gt;&lt;code&gt;gpt-5.6-sol&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$4 / $20&lt;/td&gt;&lt;td&gt;≈ / 1.3×&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;gpt-4.1-nano&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$0.10 / $0.40&lt;/td&gt;&lt;td&gt;&lt;code&gt;gpt-5.6-luna&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$0.20 / $1.20&lt;/td&gt;&lt;td&gt;2× / 3×&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;o1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$15 / $60&lt;/td&gt;&lt;td&gt;&lt;code&gt;gpt-5.6-sol&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$4 / $20&lt;/td&gt;&lt;td&gt;más barato&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;o1-pro&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$150 / $600&lt;/td&gt;&lt;td&gt;&lt;code&gt;gpt-5.6-sol&lt;/code&gt; en modo pro&lt;/td&gt;&lt;td&gt;$4 / $20 + más tokens&lt;/td&gt;&lt;td&gt;más barato&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;o3-mini&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$1.10 / $4.40&lt;/td&gt;&lt;td&gt;&lt;code&gt;gpt-5.6-sol&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$4 / $20&lt;/td&gt;&lt;td&gt;3.6× / 4.5×&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;o4-mini&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$1.10 / $4.40&lt;/td&gt;&lt;td&gt;&lt;code&gt;gpt-5.6-terra&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$2 / $12&lt;/td&gt;&lt;td&gt;1.8× / 2.7×&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;gpt-image-1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$10 / $40 (tokens de imagen)&lt;/td&gt;&lt;td&gt;&lt;code&gt;gpt-image-2&lt;/code&gt;&lt;/td&gt;&lt;td&gt;$8 / $30&lt;/td&gt;&lt;td&gt;más barato&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Tres detalles que la tabla esconde:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;El alias &lt;code&gt;gpt-4o&lt;/code&gt; no está en la lista.&lt;/strong&gt; Apunta a &lt;code&gt;gpt-4o-2024-08-06&lt;/code&gt; según &lt;a href=&quot;https://developers.openai.com/api/docs/models/gpt-4o&quot;&gt;su página de modelo&lt;/a&gt;, y solo muere el snapshot de mayo de 2024. &lt;code&gt;gpt-4o-mini&lt;/code&gt; y &lt;code&gt;gpt-4.1&lt;/code&gt; (sin &lt;code&gt;-nano&lt;/code&gt;) tampoco están.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Los $4 / $20 de Sol son una promoción.&lt;/strong&gt; OpenAI dice que está «disponible al menos hasta el 21 de noviembre de 2026» y la describe como un recorte del 20 % en entrada y del 33 % en salida, lo que deja el precio normal en unos $5 / $30. Presupuesta con ese.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Los fine-tunes se van el mismo día&lt;/strong&gt;: &lt;code&gt;ft-gpt-3.5-turbo&lt;/code&gt;, &lt;code&gt;ft-gpt-4&lt;/code&gt;, &lt;code&gt;ft-gpt-4.1-nano-2025-04-14&lt;/code&gt;, &lt;code&gt;ft-babbage-002&lt;/code&gt; y &lt;code&gt;ft-davinci-002&lt;/code&gt;. Más abajo explico por qué no tienen sucesor real.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;cómo-se-ve-el-fallo&quot;&gt;Cómo se ve el fallo&lt;/h2&gt;
&lt;p&gt;Lo único oficial que dice OpenAI es que «el modelo o endpoint dejará de estar accesible». No documenta el error. Lo que reportaron usuarios tras los apagones de julio y agosto de este año (&lt;a href=&quot;https://github.com/meridianlabs-ai/actions/issues/71&quot;&gt;aquí&lt;/a&gt; y &lt;a href=&quot;https://github.com/ShenSeanChen/waku-agent/issues/132&quot;&gt;aquí&lt;/a&gt;) es consistente:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;404 invalid_request_error / model_not_found
The model `gpt-5.1-codex` has been deprecated, learn more here:
https://platform.openai.com/docs/deprecations
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El SDK de Python lo lanza como &lt;code&gt;openai.NotFoundError&lt;/code&gt;. Dos consecuencias prácticas:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;No se puede reintentar.&lt;/strong&gt; Si tu capa reintenta cualquier excepción con espera creciente, el 23 de octubre va a gastar sus reintentos en una petición que nunca va a funcionar, y tus usuarios van a esperarla. Reintenta en 429 y 5xx, no en 404.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Un health check que lista modelos te va a mentir.&lt;/strong&gt; Según esos mismos reportes, los IDs retirados siguen apareciendo en &lt;code&gt;GET /v1/models&lt;/code&gt; después de dejar de funcionar. Un chequeo de arranque que confirma «mi modelo está en la lista» pasa con un modelo muerto. La única prueba fiable es una petición real de un token.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;La Responses API lo redacta distinto («Model not found …»), así que si comparas el texto del mensaje, compara mejor &lt;code&gt;code: model_not_found&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;1-el-modelo-que-nunca-escribiste-los-defaults-de-las-librerías&quot;&gt;1. El modelo que nunca escribiste: los defaults de las librerías&lt;/h2&gt;
&lt;p&gt;Este es el que atrapa a los equipos que se creen a salvo porque &lt;code&gt;grep gpt-3.5&lt;/code&gt; no encuentra nada. Revisé el código de las librerías que usan casi todos los tutoriales de 2023 y 2024:&lt;/p&gt;






























&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Librería&lt;/th&gt;&lt;th&gt;Modelo por defecto si no pasas uno&lt;/th&gt;&lt;th&gt;Fuente&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;LangChain Python, &lt;code&gt;ChatOpenAI&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;quot;gpt-3.5-turbo&amp;quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/langchain-ai/langchain/blob/41d357287c0470f70e45e232c711013258c0ac1a/libs/partners/openai/langchain_openai/chat_models/base.py#L733&quot;&gt;&lt;code&gt;base.py#L733&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LangChain.js, &lt;code&gt;ChatOpenAI&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;quot;gpt-3.5-turbo&amp;quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/langchain-ai/langchainjs/blob/7d6e1b098723690bd1b98bc36ed18c75fa5a85ed/libs/providers/langchain-openai/src/chat_models/base.ts#L286&quot;&gt;&lt;code&gt;base.ts#L286&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LlamaIndex, LLM &lt;code&gt;OpenAI&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;quot;gpt-3.5-turbo&amp;quot;&lt;/code&gt;, y es al que recurre el core cuando no configuras ningún LLM&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/run-llama/llama_index/blob/main/llama-index-integrations/llms/llama-index-llms-openai/llama_index/llms/openai/base.py&quot;&gt;&lt;code&gt;DEFAULT_OPENAI_MODEL&lt;/code&gt;&lt;/a&gt; en &lt;code&gt;llama-index-llms-openai&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Vercel AI SDK&lt;/td&gt;&lt;td&gt;no tiene modelo por defecto, pero &lt;code&gt;openai.completion()&lt;/code&gt; «por ahora solo» admite &lt;code&gt;gpt-3.5-turbo-instruct&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://ai-sdk.dev/providers/ai-sdk-providers/openai&quot;&gt;docs del proveedor OpenAI&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Así que esto, que aparece en incontables README, es una llamada a &lt;code&gt;gpt-3.5-turbo&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; langchain_openai &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; ChatOpenAI

llm &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ChatOpenAI&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;temperature&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;# sin model= → &amp;quot;gpt-3.5-turbo&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y con LlamaIndex puede ser todavía menos visible: si nunca defines &lt;code&gt;Settings.llm&lt;/code&gt;, un query engine construye un &lt;code&gt;OpenAI()&lt;/code&gt; por su cuenta, con el mismo default. La clase &lt;code&gt;OpenAIResponses&lt;/code&gt; de LlamaIndex usa &lt;code&gt;gpt-4o-mini&lt;/code&gt;, que sobrevive.&lt;/p&gt;
&lt;p&gt;El caso de Vercel tiene fecha antes: &lt;code&gt;gpt-3.5-turbo-instruct&lt;/code&gt; se apaga el &lt;strong&gt;28 de septiembre&lt;/strong&gt;, junto con &lt;code&gt;babbage-002&lt;/code&gt; y &lt;code&gt;davinci-002&lt;/code&gt;, así que lo que use &lt;code&gt;openai.completion()&lt;/code&gt; o el endpoint heredado &lt;code&gt;/v1/completions&lt;/code&gt; tiene una semana, no un mes. Esos tres son los únicos modelos que todavía nombra la referencia del endpoint.&lt;/p&gt;
&lt;p&gt;El arreglo siempre es el mismo: pasa el modelo explícito, y léelo de la configuración para que el siguiente apagón sea una variable de entorno y no un deploy.&lt;/p&gt;
&lt;p&gt;Como contraste, &lt;code&gt;laravel/ai&lt;/code&gt; 1.x ya usa por defecto &lt;code&gt;gpt-5.6-terra&lt;/code&gt;, &lt;code&gt;-luna&lt;/code&gt;, &lt;code&gt;-sol&lt;/code&gt; y &lt;code&gt;gpt-image-2&lt;/code&gt;. Las librerías nuevas lo resolvieron; el problema es el código montado sobre las de 2023.&lt;/p&gt;
&lt;h2 id=&quot;2-las-tools-en-chat-completions-ahora-piden-el-razonamiento-apagado&quot;&gt;2. Las tools en Chat Completions ahora piden el razonamiento apagado&lt;/h2&gt;
&lt;p&gt;Todos los sustitutos de la tabla son modelos de razonamiento, y eso cambia el contrato de Chat Completions. De la &lt;a href=&quot;https://developers.openai.com/api/docs/guides/upgrading-to-gpt-5p6-sol&quot;&gt;guía para subir a GPT-5.6&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For GPT-5.6, function tools in Chat Completions are compatible only with effective reasoning &lt;code&gt;none&lt;/code&gt;. Reasoning with tools should use the Responses API.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;El esfuerzo de razonamiento por defecto es &lt;code&gt;medium&lt;/code&gt;. Así que un chatbot con function calling que funcionaba con &lt;code&gt;gpt-4-turbo&lt;/code&gt;, al que solo le cambias el nombre del modelo, falla en su primera petición. Este es el error que le salió a un equipo con &lt;code&gt;gpt-5.6-luna&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;400 - Function tools with reasoning_effort are not supported for gpt-5.6-luna
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tienes dos salidas. La mínima deja Chat Completions y apaga el razonamiento, que es lo más parecido a como se comportaba &lt;code&gt;gpt-4-turbo&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; res &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; openai&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;chat&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;completions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;gpt-5.6-sol&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;reasoning_effort&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;none&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;     &lt;span class=&quot;token comment&quot;&gt;// obligatorio si pasas tools&lt;/span&gt;
  messages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  tools&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La que recomienda OpenAI es mover esa llamada a la Responses API, donde razonamiento y tools sí funcionan juntos. No es solo renombrar: el prompt de sistema pasa a &lt;code&gt;instructions&lt;/code&gt;, las salidas estructuradas se mueven de &lt;code&gt;response_format&lt;/code&gt; a &lt;code&gt;text.format&lt;/code&gt; y las definiciones de funciones pierden un nivel de anidación:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Chat Completions&lt;/span&gt;
&lt;span class=&quot;token literal-property property&quot;&gt;tools&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;function&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;get_order&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; parameters &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Responses&lt;/span&gt;
&lt;span class=&quot;token literal-property property&quot;&gt;tools&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;function&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;get_order&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; parameters &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;También cambia el valor por defecto de la validación estricta. Según la &lt;a href=&quot;https://developers.openai.com/api/docs/guides/migrate-to-responses&quot;&gt;guía de migración&lt;/a&gt;, en Chat Completions las funciones no son estrictas por defecto, y en Responses omitir &lt;code&gt;strict&lt;/code&gt; hace que intente el modo estricto. Si tus esquemas JSON se escribieron a la ligera, define &lt;code&gt;strict&lt;/code&gt; a mano en vez de heredar el nuevo default.&lt;/p&gt;
&lt;p&gt;Chat Completions en sí no está deprecado. OpenAI dice que «sigue soportado» y que Responses «se recomienda para todos los proyectos nuevos». No tienes que migrar de endpoint el 23 de octubre, solo decidir qué haces con el razonamiento.&lt;/p&gt;
&lt;h2 id=&quot;3-max_tokens-cambia-de-nombre-y-ahora-paga-el-razonamiento&quot;&gt;3. &lt;code&gt;max_tokens&lt;/code&gt;: cambia de nombre y ahora paga el razonamiento&lt;/h2&gt;
&lt;p&gt;La referencia de Chat Completions marca &lt;code&gt;max_tokens&lt;/code&gt; como «deprecado en favor de &lt;code&gt;max_completion_tokens&lt;/code&gt;» e «incompatible con los modelos de la serie o». La documentación no dice si GPT-5.6 rechaza &lt;code&gt;max_tokens&lt;/code&gt; directamente; el equipo del issue de arriba reporta que todas sus llamadas necesitaron el nombre nuevo. Renómbralo de todas formas.&lt;/p&gt;
&lt;p&gt;Lo que de verdad cambia el comportamiento es qué cuenta el parámetro nuevo. &lt;code&gt;max_completion_tokens&lt;/code&gt; cubre «los tokens de salida visibles y los de razonamiento», y los de razonamiento «se cobran como tokens de salida» (&lt;a href=&quot;https://developers.openai.com/api/docs/guides/reasoning&quot;&gt;guía de razonamiento&lt;/a&gt;). El clásico clasificador en &lt;code&gt;gpt-3.5-turbo&lt;/code&gt; con &lt;code&gt;max_tokens: 50&lt;/code&gt; para forzar una etiqueta corta, movido a &lt;code&gt;gpt-5.6-terra&lt;/code&gt; con su esfuerzo &lt;code&gt;medium&lt;/code&gt; por defecto, puede gastarse todo el presupuesto pensando y volver cortado o vacío, y el razonamiento se cobra igual. OpenAI recomienda reservar al menos 25,000 tokens para razonamiento y salida.&lt;/p&gt;
&lt;p&gt;Para tareas cortas y deterministas el arreglo no es más presupuesto, sino nada de razonamiento:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;client&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;chat&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;completions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;create&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    model&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;gpt-5.6-terra&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    reasoning_effort&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;none&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;        &lt;span class=&quot;token comment&quot;&gt;# una etiqueta no necesita pensar&lt;/span&gt;
    max_completion_tokens&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;       &lt;span class=&quot;token comment&quot;&gt;# antes max_tokens&lt;/span&gt;
    messages&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;messages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;temperature&lt;/code&gt; y &lt;code&gt;top_p&lt;/code&gt; son una trampa parecida. Para GPT-5.2 y 5.4 la documentación dice que solo se admiten con esfuerzo &lt;code&gt;none&lt;/code&gt;; para 5.6 no encontré la regla por escrito. Si los pasas, prueba con y sin razonamiento antes del 23 en vez de descubrirlo en producción.&lt;/p&gt;
&lt;aside class=&quot;not-prose my-8 flex flex-wrap items-center gap-4 rounded-2xl border border-neutral-100 bg-white p-5 transition-all duration-200 hover:shadow-[5px_5px_rgba(0,98,90,0.3),10px_10px_rgba(0,98,90,0.2),15px_15px_rgba(0,98,90,0.1)] dark:border-zinc-800 dark:bg-zinc-900/40&quot; style=&quot;border-left:4px solid #a855f7&quot;&gt; &lt;span class=&quot;grid size-12 shrink-0 place-items-center rounded-xl&quot; style=&quot;background:#a855f71a;color:#a855f7&quot;&gt;  &lt;/span&gt; &lt;div class=&quot;flex min-w-0 flex-1 flex-col gap-0.5&quot;&gt; &lt;span class=&quot;text-xs font-semibold tracking-wide text-zinc-500 uppercase dark:text-zinc-400&quot;&gt; Herramienta gratuita &lt;/span&gt; &lt;span class=&quot;text-lg leading-snug font-bold text-blacktext dark:text-mint-50&quot;&gt; Contador de Tokens y Calculadora de Costos de IA &lt;/span&gt; &lt;span class=&quot;text-sm text-pretty text-zinc-600 dark:text-zinc-400&quot;&gt; Cuenta los tokens de tu texto o prompt y calcula cuánto costará en GPT-5, Claude, Gemini, Kimi K3, Grok y DeepSeek. Conteo exacto de OpenAI y costos por llamada y al mes, con caché de prompt. &lt;/span&gt; &lt;/div&gt; &lt;a href=&quot;https://ortamarco.me/herramientas/contador-tokens/&quot; data-umami-event=&quot;tool_callout_click&quot; data-umami-event-tool=&quot;contador-tokens&quot; class=&quot;ml-auto shrink-0 rounded-xl bg-mint-600 px-4 py-2 text-sm! font-semibold text-white! no-underline! transition-colors hover:bg-mint-700 hover:text-white!&quot;&gt; Abrir herramienta → &lt;/a&gt; &lt;/aside&gt;
&lt;h2 id=&quot;4-los-fine-tunes-no-tienen-sucesor&quot;&gt;4. Los fine-tunes no tienen sucesor&lt;/h2&gt;
&lt;p&gt;La columna de sustitutos manda &lt;code&gt;ft-gpt-3.5-turbo&lt;/code&gt; a &lt;code&gt;gpt-5.6-terra&lt;/code&gt; y &lt;code&gt;ft-gpt-4&lt;/code&gt; a &lt;code&gt;gpt-5.6-sol&lt;/code&gt;. Esos son modelos base. Las páginas de GPT-5.6 marcan el fine-tuning como no soportado, y desde el 6 de enero de 2027 los clientes existentes ya no pueden crear trabajos de fine-tuning.&lt;/p&gt;
&lt;p&gt;En la práctica, lo que tu fine-tune aprendió (el tono, el formato de salida, las etiquetas de tu dominio) tiene que pasar al prompt: ejemplos few-shot, un esquema estricto con salidas estructuradas o recuperación de contexto. Planea ese trabajo ya. Es lo único de esta lista que es un proyecto y no un parche, y los datos de entrenamiento con los que armaste el fine-tune son justo de donde vas a sacar los ejemplos.&lt;/p&gt;
&lt;h2 id=&quot;5-o1-pro-y-gpt-image-1-cambian-los-parámetros-no-solo-el-nombre&quot;&gt;5. &lt;code&gt;o1-pro&lt;/code&gt; y &lt;code&gt;gpt-image-1&lt;/code&gt;: cambian los parámetros, no solo el nombre&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;o1-pro&lt;/code&gt; → &lt;code&gt;gpt-5.6-sol&lt;/code&gt; en modo pro.&lt;/strong&gt; La guía es explícita: «no busques ni inventes un slug &lt;code&gt;gpt-5.6-pro&lt;/code&gt; aparte». El modo pro es un ajuste de razonamiento de Sol y solo funciona por Responses, no por Chat Completions. &lt;code&gt;o1-pro&lt;/code&gt; ya era solo de Responses, así que el endpoint no cambia; cambian el nombre del modelo y el ajuste de razonamiento. OpenAI avisa que el modo pro «hace más trabajo de modelo», así que el precio por token es menor que el de &lt;code&gt;o1-pro&lt;/code&gt; pero los tokens por petición suben.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;gpt-image-1&lt;/code&gt; → &lt;code&gt;gpt-image-2&lt;/code&gt;.&lt;/strong&gt; La guía de imágenes dice que omitas &lt;code&gt;input_fidelity&lt;/code&gt; con &lt;code&gt;gpt-image-2&lt;/code&gt;, así que si tus llamadas de edición lo pasan, quítalo. Y si dependes de fondos transparentes, &lt;code&gt;gpt-image-2&lt;/code&gt; solo los ofrece en preview desde el 20 de agosto.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;6-la-factura&quot;&gt;6. La factura&lt;/h2&gt;
&lt;p&gt;Para la familia GPT-4 y &lt;code&gt;o1&lt;/code&gt;, el cambio ahorra dinero, más todavía si seguías pagando $30 / $60 por &lt;code&gt;gpt-4-0613&lt;/code&gt;. Para todo lo que se eligió &lt;em&gt;por ser barato&lt;/em&gt;, cuesta más:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Un clasificador o router en &lt;code&gt;gpt-3.5-turbo&lt;/code&gt; que procesa 1 millón de tokens de entrada y 200 mil de salida al día cuesta $0.80. En &lt;code&gt;gpt-5.6-terra&lt;/code&gt; con el razonamiento apagado, $4.40. &lt;strong&gt;Son 5.5×&lt;/strong&gt; antes de contar un solo token de razonamiento.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;o3-mini&lt;/code&gt; → &lt;code&gt;gpt-5.6-sol&lt;/code&gt; son 3.6× en entrada y 4.5× en salida, y más cuando termine la promoción.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gpt-4.1-nano&lt;/code&gt; → &lt;code&gt;gpt-5.6-luna&lt;/code&gt; duplica la entrada y triplica la salida.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Si una de esas líneas te importa, prueba un nivel más barato que el que sugiere la tabla antes de aceptar el sustituto: la columna dice lo que OpenAI considera equivalente, no lo que tu tarea necesita. El costo por tarea entre proveedores lo desgloso en &lt;a href=&quot;https://ortamarco.me/blog/cuanto-cuesta-api-openai-claude-gemini-2026/&quot;&gt;cuánto cuesta la API de OpenAI, Claude y Gemini&lt;/a&gt;. Y no, la tabla no apunta a &lt;code&gt;gpt-6-astra&lt;/code&gt;, aunque la guía de razonamiento ahora diga «empieza con» él: a $10 / $50 y sin &lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;top_p&lt;/code&gt; ni &lt;code&gt;top_logprobs&lt;/code&gt; es otra decisión, que cuento en &lt;a href=&quot;https://ortamarco.me/blog/gpt-6-astra-para-desarrolladores/&quot;&gt;GPT-6 Astra para desarrolladores&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;la-auditoría-tres-greps-y-una-petición-real&quot;&gt;La auditoría: tres greps y una petición real&lt;/h2&gt;
&lt;p&gt;Desde la raíz del repositorio. El primero busca los IDs que mueren como texto exacto entre comillas, así que se salta &lt;code&gt;gpt-4o&lt;/code&gt;, &lt;code&gt;gpt-4o-mini&lt;/code&gt; y &lt;code&gt;gpt-4.1&lt;/code&gt;, que sobreviven:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 1. IDs de modelos que mueren el 23-oct (y el 28-sep), entre comillas&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rnE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;[&amp;#39;&lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&amp;quot;&lt;/span&gt;\&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;gpt-3&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;.5-turbo&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;a-z0-9-&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;*&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;gpt-4&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;gpt-4-0613&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;gpt-4-turbo&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;a-z0-9-&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;*&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;gpt-4-1106-preview&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;gpt-4o-2024-05-13&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;gpt-4&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;.1-nano&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;a-z0-9-&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;*&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;o1&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;o1-pro&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;a-z0-9-&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;*&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;o1-2024-12-17&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;o3-mini&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;a-z0-9-&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;*&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;o4-mini&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;a-z0-9-&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;*&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;gpt-image-1&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;babbage-002&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;davinci-002&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&amp;#39;&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;&amp;quot;&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt;]&amp;quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --exclude-dir&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;node_modules,vendor,.git&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 2. Lo mismo en los archivos de entorno, donde los nombres van sin comillas&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rnE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;=(gpt-3\.5|gpt-4(-|$)|o1|o3-mini|o4-mini|gpt-image-1$)&amp;quot;&lt;/span&gt; .env* &lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;2&lt;/span&gt;&amp;gt;&lt;/span&gt;/dev/null

&lt;span class=&quot;token comment&quot;&gt;# 3. Constructores que heredan el default de una librería&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rnE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;ChatOpenAI\(|llama_index\.llms\.openai|openai\.completion\(&amp;quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--include&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;*.py &lt;span class=&quot;token parameter variable&quot;&gt;--include&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;*.ts &lt;span class=&quot;token parameter variable&quot;&gt;--include&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;*.js --exclude-dir&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;node_modules &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;model&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El tercero da falsos positivos a propósito (un constructor en varias líneas con &lt;code&gt;model=&lt;/code&gt; en otra aparecerá), pero la lista es corta y vale la pena leerla a ojo.&lt;/p&gt;
&lt;p&gt;Luego la prueba que cuenta: por cada modelo que acabes usando, una petición real con tus parámetros de producción (tools, &lt;code&gt;max_completion_tokens&lt;/code&gt;, &lt;code&gt;temperature&lt;/code&gt; si lo pasas) contra el nombre nuevo. No &lt;code&gt;GET /v1/models&lt;/code&gt;, por lo explicado arriba.&lt;/p&gt;
&lt;p&gt;Los nombres de modelo configurados también viven fuera del repo: flujos de Make, Zapier o n8n, el panel del chatbot de un proveedor, una hoja de Google con un script. La &lt;a href=&quot;https://ortamarco.me/blog/retiran-el-modelo-de-ia-que-usa-tu-negocio/&quot;&gt;guía de retiros&lt;/a&gt; tiene la lista de revisión para esos casos.&lt;/p&gt;
&lt;h2 id=&quot;el-resto-del-calendario-de-openai-este-año&quot;&gt;El resto del calendario de OpenAI este año&lt;/h2&gt;
&lt;p&gt;Todo sale de la misma página de deprecaciones:&lt;/p&gt;









































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Fecha&lt;/th&gt;&lt;th&gt;Qué se apaga&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;24 sep&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Videos API, &lt;code&gt;sora-2&lt;/code&gt; y &lt;code&gt;sora-2-pro&lt;/code&gt; (sin sustituto)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;28 sep&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;gpt-3.5-turbo-instruct&lt;/code&gt;, &lt;code&gt;babbage-002&lt;/code&gt;, &lt;code&gt;davinci-002&lt;/code&gt;, &lt;code&gt;gpt-3.5-turbo-1106&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;1 oct&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;gpt-5.4-cyber&lt;/code&gt; → &lt;code&gt;gpt-5.6-cyber&lt;/code&gt; (20 días de aviso)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;23 oct&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Todo lo de este post&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;31 oct&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Evals pasa a solo lectura&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;30 nov&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;El panel y la API de Evals, la API &lt;code&gt;v1/prompts&lt;/code&gt; y los prompts reutilizables, Agent Builder&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;1 dic&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;gpt-image-1-mini&lt;/code&gt;, &lt;code&gt;gpt-image-1.5&lt;/code&gt;, &lt;code&gt;chatgpt-image-latest&lt;/code&gt; → &lt;code&gt;gpt-image-2&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;11 dic&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Los snapshots de &lt;code&gt;gpt-5&lt;/code&gt;, &lt;code&gt;gpt-5-mini&lt;/code&gt;, &lt;code&gt;gpt-5-nano&lt;/code&gt;, &lt;code&gt;gpt-5-pro&lt;/code&gt;, &lt;code&gt;o3&lt;/code&gt; y &lt;code&gt;o3-pro&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;La última fila merece una nota: las páginas de &lt;code&gt;gpt-5&lt;/code&gt; y &lt;code&gt;o3&lt;/code&gt; listan solo el snapshot que se retira, así que muy probablemente los alias mueren con ellos. El README de &lt;code&gt;openai-php/laravel&lt;/code&gt; todavía usa &lt;code&gt;&amp;#39;model&amp;#39; =&amp;gt; &amp;#39;gpt-5&amp;#39;&lt;/code&gt; como ejemplo. La Assistants API ya se apagó el 26 de agosto.&lt;/p&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué modelos de OpenAI se apagan el 23 de octubre de 2026? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; gpt-3.5-turbo (incluidos gpt-3.5-turbo-0125 y la variante -completions), gpt-4 y gpt-4-0613, gpt-4-turbo y gpt-4-turbo-2024-04-09, gpt-4-1106-preview, gpt-4o-2024-05-13, gpt-4.1-nano, o1, o1-pro, o3-mini, o4-mini y gpt-image-1, además de los modelos con fine-tuning ft-gpt-3.5-turbo, ft-gpt-4, ft-gpt-4.1-nano, ft-babbage-002 y ft-davinci-002. OpenAI lo anunció el 22 de abril de 2026. El alias gpt-4o, gpt-4o-mini y gpt-4.1 no están en la lista. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Por qué modelo cambio gpt-4 y gpt-3.5-turbo? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; OpenAI indica gpt-5.6-sol como sustituto de gpt-4, gpt-4-turbo, gpt-4o-2024-05-13, o1, o1-pro (en modo pro) y o3-mini; gpt-5.6-terra para gpt-3.5-turbo y o4-mini; gpt-5.6-luna para gpt-4.1-nano; y gpt-image-2 para gpt-image-1. Son modelos de razonamiento, así que cambiar el nombre no basta: las tools en Chat Completions piden reasoning_effort none, y max_tokens pasa a max_completion_tokens, que también cuenta los tokens de razonamiento. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué error devuelve la API de OpenAI con un modelo retirado? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; OpenAI no documenta la respuesta. Los usuarios que se toparon con los apagones de julio y agosto de 2026 reportan HTTP 404 con type invalid_request_error y code model_not_found, que el SDK de Python lanza como openai.NotFoundError. No se debe reintentar. Los IDs retirados pueden seguir apareciendo en GET /v1/models, así que la única comprobación fiable es una petición real. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿LangChain sigue usando gpt-3.5-turbo por defecto? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Sí. En septiembre de 2026, ChatOpenAI en LangChain Python y en LangChain.js usa gpt-3.5-turbo cuando no pasas un modelo, y el LLM OpenAI de LlamaIndex también (y es al que recurre LlamaIndex cuando no defines Settings.llm). El código que nunca nombra un modelo va a fallar el 23 de octubre. Pasa el modelo de forma explícita. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Por qué gpt-5.6 falla con «Function tools with reasoning_effort are not supported»? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Porque en Chat Completions GPT-5.6 solo acepta function tools cuando el esfuerzo de razonamiento efectivo es none, y el valor por defecto es medium. O añades reasoning_effort: none a la petición, que se comporta lo más parecido a gpt-4-turbo, o mueves la llamada a la Responses API, donde tools y razonamiento funcionan juntos. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Sale más caro migrar a gpt-5.6? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Depende de lo que tenías. Desde gpt-4, gpt-4-turbo u o1 sale más barato: gpt-5.6-sol cuesta $4 de entrada y $20 de salida por millón de tokens, un precio promocional disponible al menos hasta el 21 de noviembre de 2026. De gpt-3.5-turbo a gpt-5.6-terra es 4 veces el precio de entrada y 8 veces el de salida, y de o3-mini a sol unas 3.6 y 4.5 veces, antes de contar los tokens de razonamiento, que se cobran como salida. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué pasa con mi modelo gpt-3.5-turbo con fine-tuning? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Se apaga el 23 de octubre de 2026 junto con los modelos base. El sustituto indicado es gpt-5.6-terra base, y GPT-5.6 no admite fine-tuning; desde el 6 de enero de 2027 los clientes existentes no pueden crear trabajos de fine-tuning. Lo que aprendió el fine-tune tiene que pasar al prompt: ejemplos few-shot, salidas estructuradas o recuperación de contexto. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Desarrollo Web</category><category>OpenAI</category><category>IA</category><category>API</category><category>Python</category><category>JavaScript</category><category>Migración</category><author>Marco Orta</author></item><item><title>Por qué tu tienda en línea se cae en el Buen Fin (y cómo evitarlo): la guía técnica de 2026</title><link>https://ortamarco.me/blog/tienda-en-linea-se-cae-buen-fin/</link><guid isPermaLink="true">https://ortamarco.me/blog/tienda-en-linea-se-cae-buen-fin/</guid><description>Tu hosting tiene un techo escrito, tu pasarela limita peticiones y un pago en OXXO puede apartar inventario días. Qué revisar antes del 13 de noviembre.</description><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;El Buen Fin 2026 va del viernes 13 al martes 17 de noviembre, y casi todo lo que se publica sobre cómo preparar tu tienda en línea habla de banners, cupones y correos. Esta guía es de la otra mitad: qué se rompe técnicamente cuando llegan de golpe los visitantes que pagaste por atraer, y qué hacer antes. Casi todo lo que sigue está escrito en la documentación de tu hosting, tu pasarela de pagos y tu plataforma, y casi nadie lo lee hasta que la tienda ya está caída.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Los números de 2025 explican por qué vale la pena. Según la &lt;a href=&quot;https://www.gob.mx/se/prensa/supera-la-meta-la-xv-edicion-de-el-buen-fin-2025?idiom=es-MX&quot;&gt;Secretaría de Economía&lt;/a&gt;, la edición pasada vendió 219,200 millones de pesos, y el comercio digital fue el &lt;strong&gt;21 % del total, con un crecimiento anual del 31 %&lt;/strong&gt;. El &lt;a href=&quot;https://blog.amvo.org.mx/publicaciones/reporte-de-resultados-el-buen-fin-2025&quot;&gt;reporte público de la AMVO&lt;/a&gt; midió 1,686 millones de páginas visitadas en sitios de comercio electrónico y un tráfico &lt;strong&gt;56 % mayor&lt;/strong&gt; que el año anterior (el Buen Fin pasó de cuatro a cinco días). La ANTAD reporta que el último día concentró el 26.1 % de las ventas en línea. El pico no se reparte: llega, y llega junto.&lt;/p&gt;
&lt;p&gt;He construido tiendas en WooCommerce con catálogos de miles de productos, pagos con Mercado Pago y cobro por WhatsApp. Lo que sigue es lo que reviso en cada una antes de una fecha así, con la fuente de cada límite.&lt;/p&gt;
&lt;h2 id=&quot;lo-que-pasa-en-esos-cinco-días&quot;&gt;Lo que pasa en esos cinco días&lt;/h2&gt;








































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Dato del Buen Fin 2025&lt;/th&gt;&lt;th&gt;Cifra&lt;/th&gt;&lt;th&gt;Fuente&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Ventas totales&lt;/td&gt;&lt;td&gt;219,200 millones de pesos&lt;/td&gt;&lt;td&gt;Secretaría de Economía&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Ventas en línea&lt;/td&gt;&lt;td&gt;21 % del total, +31 % anual (45,900 millones según AMVO)&lt;/td&gt;&lt;td&gt;SE / AMVO&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Tráfico a tiendas en línea&lt;/td&gt;&lt;td&gt;+56 % en los 5 días, +24 % en días comparables&lt;/td&gt;&lt;td&gt;AMVO (datos de Similarweb)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Ticket promedio&lt;/td&gt;&lt;td&gt;$1,063 en línea contra $780 en general&lt;/td&gt;&lt;td&gt;ABM, citada por la SE&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cómo pagaron en línea&lt;/td&gt;&lt;td&gt;Crédito 48 %, débito 43 %, departamental 17 %, pago a plazos sin tarjeta 12 %, efectivo en tiendas 12 %&lt;/td&gt;&lt;td&gt;AMVO&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Quejas ante Profeco&lt;/td&gt;&lt;td&gt;220 inconformidades, 205 conciliadas; predominan incumplimiento de precios y negativa de entrega&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.gob.mx/profeco/prensa/profeco-intermedia-la-devolucion-de-mas-de-un-millon-de-pesos-a-personas-consumidoras-en-el-buen-fin?idiom=es-MX&quot;&gt;Profeco&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Dos datos de esa tabla importan para lo técnico. Uno de cada ocho compradores en línea pagó &lt;strong&gt;en efectivo en cadenas comerciales&lt;/strong&gt; (OXXO y similares), y eso tiene una consecuencia sobre tu inventario que explico abajo. Y el 37 % de los consumidores, según la encuesta de la AIMX que cita la SE, &lt;strong&gt;teme que se filtren sus datos&lt;/strong&gt;: un aviso de certificado vencido o una página de pago que se ve rara el día que más visitas tienes cuesta ventas aunque el sitio no se caiga.&lt;/p&gt;
&lt;p&gt;Tampoco todo lo que falla es tuyo. En el Buen Fin 2024 usuarios de Santander y BBVA reportaron fallas para pagar con tarjeta; en el Hot Sale de mayo de 2026 BBVA confirmó en X que sus sistemas estaban fallando, y Downdetector marcó más de cinco horas de reportes. Y el 18 de noviembre de 2025, un día después de que terminara el Buen Fin, Cloudflare tuvo &lt;a href=&quot;https://blog.cloudflare.com/18-november-2025-outage/&quot;&gt;una caída global&lt;/a&gt; de más de tres horas. No puedes evitar que el banco de tu cliente se caiga. Sí puedes tener un segundo método de pago listo para cuando pase.&lt;/p&gt;
&lt;h2 id=&quot;1-tu-hosting-compartido-tiene-un-techo-y-está-escrito&quot;&gt;1. Tu hosting compartido tiene un techo, y está escrito&lt;/h2&gt;
&lt;p&gt;El plan de hosting compartido que cuesta unos cientos de pesos al año no es «lento»: tiene límites exactos, y cuando los tocas no se pone lento, &lt;strong&gt;devuelve error&lt;/strong&gt;. Hostinger publica los suyos en su &lt;a href=&quot;https://www.hostinger.com/support/6976044-parameters-and-limits-of-hosting-plans-in-hostinger/&quot;&gt;página de parámetros&lt;/a&gt; y dice que alcanzarlos produce un «503 – Service temporarily unavailable»:&lt;/p&gt;

































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Plan de Hostinger&lt;/th&gt;&lt;th&gt;CPU&lt;/th&gt;&lt;th&gt;RAM&lt;/th&gt;&lt;th&gt;Procesos PHP simultáneos&lt;/th&gt;&lt;th&gt;Conexiones a MySQL&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Web Single&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;1 GB&lt;/td&gt;&lt;td&gt;20&lt;/td&gt;&lt;td&gt;25&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Web Premium&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;2 GB&lt;/td&gt;&lt;td&gt;40&lt;/td&gt;&lt;td&gt;50&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Unlimited (antes Business)&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;3 GB&lt;/td&gt;&lt;td&gt;60&lt;/td&gt;&lt;td&gt;75&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;HostGator México lo dice de otra forma en &lt;a href=&quot;https://soporte.hostgator.mx/hc/es-419/articles/28440886639635&quot;&gt;su centro de ayuda&lt;/a&gt;: un límite del 25 % de CPU sostenido por 90 segundos o más, y &lt;strong&gt;25 procesos simultáneos por cPanel&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Para ver lo que significa, haz la cuenta con tu propia tienda. Una página de producto de WooCommerce que nadie ha guardado en caché ocupa un proceso PHP mientras se arma. Si tarda un segundo y tienes 20 procesos, tu techo es de unas 20 páginas por segundo; el visitante 21 no espera en una fila, recibe un error. Por eso la caché del siguiente punto no es una optimización, es lo que decide si esos límites te alcanzan o no.&lt;/p&gt;
&lt;h2 id=&quot;2-la-caché-qué-se-puede-guardar-y-qué-no&quot;&gt;2. La caché: qué se puede guardar y qué no&lt;/h2&gt;
&lt;p&gt;La idea es sencilla: que la mayoría de los visitantes, los que solo miran, reciban una copia ya armada de la página y no le cuesten nada a tu servidor. Solo el carrito, el pago y la cuenta del cliente tienen que llegar al servidor cada vez.&lt;/p&gt;
&lt;p&gt;Lo que casi nadie sabe es que &lt;strong&gt;Cloudflare no guarda el HTML por defecto&lt;/strong&gt;, ni en el plan gratuito ni en los de pago. Su &lt;a href=&quot;https://developers.cloudflare.com/cache/concepts/default-cache-behavior/&quot;&gt;documentación&lt;/a&gt; es explícita: «The Cloudflare CDN does not cache HTML or JSON by default». Si pusiste tu tienda detrás de Cloudflare y no configuraste nada más, se guardan las imágenes, el CSS y el JavaScript, pero cada página sigue llegando a tu hosting. En el plan gratuito tienes hasta 10 Cache Rules, y con una basta. Esta es la expresión para una tienda WooCommerce en español (ajusta las rutas a las tuyas):&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;(http.host eq &amp;quot;tutienda.mx&amp;quot;
 and not starts_with(http.request.uri.path, &amp;quot;/wp-admin&amp;quot;)
 and not http.request.uri.path contains &amp;quot;/carrito&amp;quot;
 and not http.request.uri.path contains &amp;quot;/finalizar-compra&amp;quot;
 and not http.request.uri.path contains &amp;quot;/mi-cuenta&amp;quot;
 and not http.cookie contains &amp;quot;woocommerce_items_in_cart&amp;quot;
 and not http.cookie contains &amp;quot;wordpress_logged_in&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Con la acción «Eligible for cache» y un tiempo en el borde corto (5 a 10 minutos), quien solo mira recibe la copia guardada, y quien ya tiene algo en el carrito o inició sesión pasa directo a tu servidor. Dos avisos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Purga la caché cuando cambies los precios.&lt;/strong&gt; Si tus descuentos arrancan a la medianoche del 13, una copia guardada a las 11:58 sigue mostrando el precio viejo, y «el precio no era el anunciado» es de las quejas que más recibe Profeco. Purga al activar las ofertas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Una cookie en cada página rompe todo.&lt;/strong&gt; Cloudflare no guarda una respuesta que trae &lt;code&gt;Set-Cookie&lt;/code&gt;, y algunos plugins (conversores de moneda, geolocalización, popups) la mandan en cada visita. Compruébalo así:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-sI&lt;/span&gt; https://tutienda.mx/producto/tu-mas-vendido/ &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-iE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;set-cookie|cf-cache-status&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lo que quieres ver, a partir de la segunda petición, es &lt;code&gt;cf-cache-status: HIT&lt;/code&gt; y ningún &lt;code&gt;set-cookie&lt;/code&gt;. Si ves &lt;code&gt;DYNAMIC&lt;/code&gt; o &lt;code&gt;BYPASS&lt;/code&gt;, esa página sigue llegando a tu servidor.&lt;/p&gt;
&lt;p&gt;Dos cosas que parecen red de seguridad y no lo son. &lt;strong&gt;Always Online&lt;/strong&gt; está en todos los planes, pero solo se activa con errores 52x, sirve copias del Internet Archive y &lt;a href=&quot;https://developers.cloudflare.com/cache/how-to/always-online/&quot;&gt;no puede servir contenido dinámico&lt;/a&gt;: nadie compra en una copia de archivo. Y la &lt;strong&gt;sala de espera&lt;/strong&gt; de Cloudflare (Waiting Room), la fila virtual que usan las grandes tiendas, &lt;a href=&quot;https://developers.cloudflare.com/waiting-room/plans/&quot;&gt;solo existe en los planes Business y Enterprise&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Si tu tienda es WooCommerce, revisa también los &lt;em&gt;cart fragments&lt;/em&gt;: el script que actualiza el contador del carrito haciendo una petición al servidor en cada página. &lt;a href=&quot;https://developer.woocommerce.com/2023/06/16/best-practices-for-the-use-of-the-cart-fragments-api/&quot;&gt;WooCommerce reconoce&lt;/a&gt; que en tiendas con mucho tráfico «puede afectar severamente la carga del servidor», y desde la versión 7.8 ya no se carga en todas las páginas, pero muchos temas y plugins lo vuelven a activar. Abre una página de producto con las herramientas de desarrollo del navegador y busca una petición a &lt;code&gt;?wc-ajax=get_refreshed_fragments&lt;/code&gt;. Si aparece en páginas sin mini carrito, es una petición al servidor por visita que la caché no te quita.&lt;/p&gt;
&lt;p&gt;Y aligera las fotos de producto: son la mayor parte del peso de una tienda, y en celular, que es donde compra casi todo el mundo, cada megabyte se nota.&lt;/p&gt;
&lt;aside class=&quot;not-prose my-8 flex flex-wrap items-center gap-4 rounded-2xl border border-neutral-100 bg-white p-5 transition-all duration-200 hover:shadow-[5px_5px_rgba(0,98,90,0.3),10px_10px_rgba(0,98,90,0.2),15px_15px_rgba(0,98,90,0.1)] dark:border-zinc-800 dark:bg-zinc-900/40&quot; style=&quot;border-left:4px solid #06b6d4&quot;&gt; &lt;span class=&quot;grid size-12 shrink-0 place-items-center rounded-xl&quot; style=&quot;background:#06b6d41a;color:#06b6d4&quot;&gt;  &lt;/span&gt; &lt;div class=&quot;flex min-w-0 flex-1 flex-col gap-0.5&quot;&gt; &lt;span class=&quot;text-xs font-semibold tracking-wide text-zinc-500 uppercase dark:text-zinc-400&quot;&gt; Herramienta gratuita &lt;/span&gt; &lt;span class=&quot;text-lg leading-snug font-bold text-blacktext dark:text-mint-50&quot;&gt; Compresor de Imágenes &lt;/span&gt; &lt;span class=&quot;text-sm text-pretty text-zinc-600 dark:text-zinc-400&quot;&gt; Comprime y optimiza imágenes JPG, PNG o WebP sin perder calidad visible, con control de calidad, cambio de formato y redimensionado. Todo en tu navegador. &lt;/span&gt; &lt;/div&gt; &lt;a href=&quot;https://ortamarco.me/herramientas/compresor-imagenes/&quot; data-umami-event=&quot;tool_callout_click&quot; data-umami-event-tool=&quot;compresor-imagenes&quot; class=&quot;ml-auto shrink-0 rounded-xl bg-mint-600 px-4 py-2 text-sm! font-semibold text-white! no-underline! transition-colors hover:bg-mint-700 hover:text-white!&quot;&gt; Abrir herramienta → &lt;/a&gt; &lt;/aside&gt;
&lt;h2 id=&quot;3-tu-pasarela-de-pagos-también-tiene-límite&quot;&gt;3. Tu pasarela de pagos también tiene límite&lt;/h2&gt;
&lt;p&gt;El servidor no es lo único que puede decir «ya no». Stripe publica sus &lt;a href=&quot;https://docs.stripe.com/rate-limits&quot;&gt;límites&lt;/a&gt;: &lt;strong&gt;100 peticiones por segundo&lt;/strong&gt; en modo real, y la mayoría de sus endpoints por separado a 25. Y lo dice sin rodeos: «un aumento repentino en el volumen de cobros, como una venta flash, puede provocar limitación». Si esperas un pico, su recomendación es escribirle a soporte &lt;strong&gt;antes&lt;/strong&gt;. Mercado Pago no publica una cifra, pero su API de órdenes responde &lt;code&gt;429 Too Many Requests&lt;/code&gt; con un encabezado &lt;code&gt;Retry-After&lt;/code&gt; cuando la pasas.&lt;/p&gt;
&lt;p&gt;Para una PyME, 100 cobros por segundo sobran. El problema aparece cuando la integración hace varias llamadas por compra (crear cliente, crear intento de pago, consultar estado) o reintenta sin esperar: un error 429 mal manejado se convierte en un «no se pudo procesar tu pago» para un cliente que sí tenía fondos.&lt;/p&gt;
&lt;p&gt;Y el pago con verificación del banco (3-D Secure, la pantalla de «confirma en tu app») cambia el flujo. En México no es obligatorio por regulación, pero Mercado Pago &lt;a href=&quot;https://www.mercadopago.com.mx/developers/es/docs/checkout-api-payments/how-tos/integrate-3ds&quot;&gt;recomienda activarlo&lt;/a&gt; en modo opcional, y cuando el banco pide el desafío el pago queda en estado &lt;code&gt;pending&lt;/code&gt; con detalle &lt;code&gt;pending_challenge&lt;/code&gt;. Si tu integración trata «pendiente» como «rechazado», estás cancelando compras buenas. La AMVO pone el «rechazo del método de pago» entre las barreras para comprar en línea.&lt;/p&gt;
&lt;h2 id=&quot;4-el-problema-que-nadie-te-cuenta-el-pago-en-oxxo-aparta-tu-inventario&quot;&gt;4. El problema que nadie te cuenta: el pago en OXXO aparta tu inventario&lt;/h2&gt;
&lt;p&gt;Esto es lo que más me preocupa de una tienda con inventario limitado, y no lo vi en ninguna de las guías del Buen Fin que revisé. Cuando alguien elige pagar en efectivo, la pasarela genera una ficha con fecha de vencimiento, y mientras no pague, la pieza queda en el limbo:&lt;/p&gt;

























&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Pasarela&lt;/th&gt;&lt;th&gt;Vigencia de la ficha&lt;/th&gt;&lt;th&gt;Qué pasa si paga tarde&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Stripe (OXXO)&lt;/td&gt;&lt;td&gt;5 días por defecto, configurable de 1 a 7; la confirmación llega al siguiente día hábil&lt;/td&gt;&lt;td&gt;La ficha vence (&lt;a href=&quot;https://docs.stripe.com/payments/oxxo&quot;&gt;docs&lt;/a&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Mercado Pago (OXXO, Paycash)&lt;/td&gt;&lt;td&gt;Configurable de 1 a 30 días; recomiendan 3. La acreditación tarda hasta 2 horas hábiles&lt;/td&gt;&lt;td&gt;«El valor será devuelto» (&lt;a href=&quot;https://www.mercadopago.com.mx/developers/es/docs/checkout-api-orders/payment-integration/other-payment-methods&quot;&gt;docs&lt;/a&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Conekta&lt;/td&gt;&lt;td&gt;La fija tu integración con &lt;code&gt;expires_at&lt;/code&gt;; avisa con el webhook &lt;code&gt;order.expired&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Depende de tu integración (&lt;a href=&quot;https://developers.conekta.com/docs/order&quot;&gt;docs&lt;/a&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Ahora el lado de WooCommerce. Su opción «Retener inventario» aparta la pieza de los pedidos &lt;strong&gt;pendientes de pago&lt;/strong&gt; durante el tiempo que configures (60 minutos es el valor recomendado) y luego los cancela, según &lt;a href=&quot;https://woocommerce.com/document/managing-orders/troubleshooting-orders/&quot;&gt;su guía&lt;/a&gt;. Pero la misma guía dice que los métodos de pago diferido dejan el pedido &lt;strong&gt;en espera&lt;/strong&gt; (On hold), y ahí esa cancelación automática no aplica. Si el plugin de tu pasarela no cancela el pedido cuando vence la ficha, la pieza queda apartada días por alguien que quizá nunca va a pagar, mientras le dices «agotado» a quien sí traía la tarjeta en la mano.&lt;/p&gt;
&lt;p&gt;Qué hacer antes del 13:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Acorta la vigencia de la ficha durante la campaña.&lt;/strong&gt; Uno o dos días en lugar de cinco o treinta.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Comprueba que tu plugin cancela el pedido al vencer.&lt;/strong&gt; Haz una compra de prueba en efectivo con la vigencia mínima, no la pagues y revisa al día siguiente que el pedido pasó a cancelado y el inventario volvió.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Si vendes por WhatsApp con links de cobro&lt;/strong&gt;, dales vencimiento corto. Clip, por ejemplo, permite que el link &lt;a href=&quot;https://developer.clip.mx/discuss/6480ac8b182697101358f435&quot;&gt;venza el mismo día&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;5-sobreventa-dos-personas-compran-la-última-pieza&quot;&gt;5. Sobreventa: dos personas compran la última pieza&lt;/h2&gt;
&lt;p&gt;WooCommerce tiene desde la versión 4.3 una reserva de inventario para evitar que dos compras simultáneas se lleven la misma pieza (&lt;a href=&quot;https://github.com/woocommerce/woocommerce/pull/26395&quot;&gt;PR #26395&lt;/a&gt;). Aun así, el &lt;a href=&quot;https://github.com/woocommerce/woocommerce/issues/44273&quot;&gt;issue #44273&lt;/a&gt;, donde el equipo confirma que «una condición de carrera como esta es posible», sigue abierto hoy. En días normales casi nunca pasa. En el Buen Fin, con cientos de personas sobre tu producto estrella, es cuando pasa.&lt;/p&gt;
&lt;p&gt;Para los productos con pocas piezas: deja un margen (publica 18 si tienes 20), limita la cantidad por cliente y decide antes qué haces si ocurre, con un mensaje de disculpa y un reembolso rápido. La «negativa de entrega» fue de las quejas que predominaron ante Profeco en 2025. Shopify, en su &lt;a href=&quot;https://help.shopify.com/en/manual/promoting-marketing/sales/flash-sales&quot;&gt;guía para ventas flash&lt;/a&gt;, recomienda por lo mismo cambiar a captura manual de pagos, para cobrar solo lo que de verdad puedes entregar.&lt;/p&gt;
&lt;h2 id=&quot;6-lo-que-trabaja-en-segundo-plano&quot;&gt;6. Lo que trabaja en segundo plano&lt;/h2&gt;
&lt;p&gt;Los correos de confirmación, los webhooks de pago y la actualización de inventario en WooCommerce pasan por Action Scheduler, que por defecto &lt;a href=&quot;https://actionscheduler.org/perf/&quot;&gt;procesa lotes de 25 tareas&lt;/a&gt; durante 30 segundos, disparado por WP-Cron. Y WP-Cron solo se ejecuta cuando alguien visita el sitio. Con la caché bien puesta, casi nadie llega al servidor, y las tareas pueden acumularse. El resultado: clientes que pagaron y no reciben su confirmación, y te escriben preguntando si su compra entró.&lt;/p&gt;
&lt;p&gt;La solución es un cron real del servidor en lugar del de visitas: define &lt;code&gt;DISABLE_WP_CRON&lt;/code&gt; en &lt;code&gt;wp-config.php&lt;/code&gt; y programa una llamada a &lt;code&gt;wp-cron.php&lt;/code&gt; cada minuto desde el panel de tu hosting. Casi todos los hostings compartidos lo permiten.&lt;/p&gt;
&lt;h2 id=&quot;7-pruébalo-antes-con-tráfico-falso&quot;&gt;7. Pruébalo antes con tráfico falso&lt;/h2&gt;
&lt;p&gt;Todo lo anterior se puede comprobar sin esperar al día 13. Una prueba de carga simula a muchos visitantes a la vez y te dice en qué punto empiezan los errores. Dos herramientas gratuitas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;k6&lt;/strong&gt;, de Grafana: gratis en tu computadora, y su nube gratuita incluye &lt;a href=&quot;https://grafana.com/pricing/&quot;&gt;500 horas de usuario virtual al mes&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Loader.io&lt;/strong&gt;: el plan gratuito permite &lt;a href=&quot;https://loader.io/pricing&quot;&gt;10,000 clientes por prueba&lt;/a&gt; durante un minuto, lanzados desde servidores en Estados Unidos.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Un guion mínimo de k6 que visita la portada y tu producto más vendido:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot; data-language=&quot;javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; http &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;k6/http&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; check&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sleep &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;k6&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; options &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;stages&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;2m&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// sube a 50 visitantes simultáneos&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;5m&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// se sostiene&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;1m&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;thresholds&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;http_req_failed&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;rate&amp;lt;0.01&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;     &lt;span class=&quot;token comment&quot;&gt;// menos del 1 % de errores&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;http_req_duration&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;p(95)&amp;lt;2000&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// el 95 % en menos de 2 segundos&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;check&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;https://tutienda.mx/&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;portada&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;check&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;https://tutienda.mx/producto/tu-mas-vendido/&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;producto&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Se corre con &lt;code&gt;k6 run prueba.js&lt;/code&gt;. Tres reglas: prueba contra páginas, &lt;strong&gt;nunca contra tu pasarela de pagos&lt;/strong&gt;; si estás en hosting compartido, avisa antes a tu proveedor, porque una prueba de carga se parece mucho a un ataque; y apunta a tres a cinco veces tu hora pico normal. No hay una cifra pública de cuánto sube el tráfico de una tienda chica en el Buen Fin frente a un día cualquiera; si tu analítica guarda el del año pasado, usa ese. Si la prueba falla con 50 visitantes, ya sabes lo que hay que arreglar, y tienes siete semanas para hacerlo.&lt;/p&gt;
&lt;h2 id=&quot;si-tu-tienda-está-en-shopify-o-tiendanube&quot;&gt;Si tu tienda está en Shopify o Tiendanube&lt;/h2&gt;
&lt;p&gt;La infraestructura no es tu problema: Shopify atendió &lt;a href=&quot;https://www.shopify.com/investors/press-releases/shopify-merchants-achieve-record-breaking-14-6-billion-in-black-friday-cyber-monday-sales&quot;&gt;489 millones de peticiones por minuto&lt;/a&gt; en su borde durante el Black Friday y el Cyber Monday de 2025, y su guía dice que la preparación especial solo hace falta si esperas «decenas de miles de clientes» empezando a pagar en pocos minutos. Lo tuyo son las otras cuatro secciones: el peso del tema y las apps que cargas en cada página, la pasarela y sus estados pendientes, la vigencia de las fichas en efectivo y el margen de inventario.&lt;/p&gt;
&lt;h2 id=&quot;la-lista-para-dos-semanas-antes&quot;&gt;La lista para dos semanas antes&lt;/h2&gt;





















































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Qué&lt;/th&gt;&lt;th&gt;Cómo lo compruebas&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Caché de HTML activa para visitantes&lt;/td&gt;&lt;td&gt;&lt;code&gt;cf-cache-status: HIT&lt;/code&gt; en la segunda petición a una página de producto&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Ninguna página manda &lt;code&gt;Set-Cookie&lt;/code&gt; a visitantes&lt;/td&gt;&lt;td&gt;El mismo &lt;code&gt;curl -sI&lt;/code&gt; de arriba&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Carrito, pago y cuenta excluidos de la caché&lt;/td&gt;&lt;td&gt;Agrega algo al carrito en otra ventana y verifica que el contador se actualiza&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Precios nuevos visibles&lt;/td&gt;&lt;td&gt;Purga la caché al activar las ofertas y revisa en modo incógnito&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fichas de efectivo con vigencia corta&lt;/td&gt;&lt;td&gt;Compra de prueba en OXXO que no pagas: al vencer, pedido cancelado e inventario de vuelta&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Pagos «pendientes» tratados como pendientes&lt;/td&gt;&lt;td&gt;Compra de prueba con 3-D Secure que no completas&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cron real del servidor&lt;/td&gt;&lt;td&gt;&lt;code&gt;DISABLE_WP_CRON&lt;/code&gt; en &lt;code&gt;wp-config.php&lt;/code&gt; y tarea programada cada minuto&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Margen en productos estrella&lt;/td&gt;&lt;td&gt;Inventario publicado por debajo del real&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Certificado SSL vigente más allá del 17 de noviembre&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://ortamarco.me/herramientas/consulta-ssl/&quot;&gt;Consulta SSL&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Prueba de carga pasada&lt;/td&gt;&lt;td&gt;k6 o Loader.io sin errores a tres veces tu pico&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Plan B de cobro&lt;/td&gt;&lt;td&gt;Link de pago y WhatsApp listos si la pasarela o el banco fallan&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Si todavía no tienes tienda o sitio y quieres llegar al Buen Fin con algo que aguante, el último arranque para &lt;a href=&quot;https://ortamarco.me/sitio-para-tu-negocio/&quot;&gt;mi paquete de sitio para negocio&lt;/a&gt; con entrega antes del 13 de noviembre es el 20 de octubre. Y si lo que necesitas es cobrar y responder por WhatsApp sin que tu equipo conteste mensaje por mensaje, está &lt;a href=&quot;https://ortamarco.me/whatsapp-para-tu-negocio/&quot;&gt;el agente de WhatsApp para negocios&lt;/a&gt;.&lt;/p&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuándo es el Buen Fin 2026? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Del viernes 13 al martes 17 de noviembre de 2026. Es la edición 16 y dura cinco días porque incluye el lunes 16 de noviembre, día de descanso por el aniversario de la Revolución. El registro de negocios es gratuito en elbuenfin.org, del 8 de septiembre al 12 de noviembre. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Por qué se cae una tienda en línea en el Buen Fin? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Casi siempre porque cada visita llega al servidor sin caché y se agotan los procesos que permite el hosting. Un plan compartido tiene límites fijos (en Hostinger, 20 procesos PHP en el plan más básico; en HostGator México, 25 procesos por cPanel) y al pasarlos devuelve error 503. También fallan la pasarela de pagos cuando limita peticiones, o el banco del cliente, que no depende de ti. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cloudflare gratis protege mi tienda del tráfico del Buen Fin? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Ayuda, pero no solo con activarlo. Cloudflare no guarda el HTML por defecto, así que sin una Cache Rule cada página sigue llegando a tu hosting. En el plan gratuito puedes crear hasta 10 reglas y con una basta para guardar las páginas de visitantes excluyendo carrito, pago y cuenta. La sala de espera virtual (Waiting Room) solo existe en los planes Business y Enterprise. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Un pago en OXXO pendiente aparta mi inventario? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Puede hacerlo por días. La ficha de Stripe vence en 5 días por defecto (configurable de 1 a 7) y la de Mercado Pago entre 1 y 30. En WooCommerce, los pedidos de pago diferido quedan en espera y la retención de inventario de 60 minutos solo cancela los pendientes de pago, así que la pieza queda apartada hasta que el plugin de la pasarela cancele el pedido al vencer la ficha. Acorta la vigencia durante la campaña y pruébalo con una compra que no pagues. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cómo hago una prueba de carga de mi tienda en línea? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Con k6, gratuito y de código abierto, o con Loader.io, cuyo plan gratuito lanza hasta 10,000 clientes en una prueba de un minuto. Simula visitantes sobre la portada y tus productos más vendidos, nunca sobre la pasarela de pagos, apunta a tres a cinco veces tu hora pico y avisa antes a tu proveedor de hosting compartido, porque una prueba de carga se parece a un ataque. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué pasa si se cae el banco de mi cliente durante el Buen Fin? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Tu tienda funciona pero el cobro falla, y no puedes evitarlo: en el Buen Fin 2024 hubo reportes de fallas de Santander y BBVA, y en el Hot Sale 2026 BBVA confirmó una falla de varias horas. Lo que sí puedes hacer es ofrecer más de un método de pago (tarjeta, transferencia, efectivo en tiendas) y tener listo un link de cobro para mandar por WhatsApp. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Desarrollo Web</category><category>Buen Fin</category><category>Comercio electrónico</category><category>WooCommerce</category><category>Hosting</category><category>Pagos en línea</category><category>PyMEs</category><author>Marco Orta</author></item><item><title>Chrome quita XSLT: qué se rompe en sitemaps, feeds y visores de CFDI</title><link>https://ortamarco.me/blog/chrome-quita-xslt-que-se-rompe/</link><guid isPermaLink="true">https://ortamarco.me/blog/chrome-quita-xslt-que-se-rompe/</guid><description>Chrome 158 deja de ejecutar XSLT el 17-nov. Tu sitemap sobrevive; el código que arma la cadena original del CFDI en el navegador, no. Cómo detectarlo y migrar.</description><pubDate>Sun, 13 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Chrome 158 sale a estable el 17 de noviembre de 2026 y deja de ejecutar XSLT: se van &lt;code&gt;XSLTProcessor&lt;/code&gt; y el procesamiento de &lt;code&gt;&amp;lt;?xml-stylesheet type=&amp;quot;text/xsl&amp;quot;?&amp;gt;&lt;/code&gt;. En la mayoría de los sitios eso cambia el adorno y nada más. El sitemap o el feed que un humano veía como tabla vuelve a verse como XML crudo, sin error y sin tocar el archivo. Donde sí se rompe código es en un rincón muy mexicano: los visores y validadores de CFDI que arman la cadena original en el navegador con la hoja XSLT del SAT. Ahí la función truena con &lt;code&gt;ReferenceError: XSLTProcessor is not defined&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Las dos cosas las probé en Chrome 152 con el interruptor que apaga XSLT antes de tiempo. Para la parte del CFDI fui más lejos: generé la cadena original de un comprobante de prueba con cinco motores distintos y validé cada resultado contra el sello digital del propio XML. Una librería XSLT escrita en JavaScript da una cadena con la que el sello no verifica, el polyfill que recomienda Chrome falla si la hoja conserva sus includes, y la hoja del SAT tal como se publica ni siquiera corre en el navegador.&lt;/p&gt;
&lt;h2 id=&quot;las-fechas&quot;&gt;Las fechas&lt;/h2&gt;
&lt;p&gt;Todo sale del &lt;a href=&quot;https://developer.chrome.com/docs/web-platform/deprecating-xslt&quot;&gt;anuncio de Chrome for Developers&lt;/a&gt; y de la &lt;a href=&quot;https://chromestatus.com/feature/4709671889534976&quot;&gt;entrada en Chrome Platform Status&lt;/a&gt;, con las fechas de estable cotejadas en &lt;a href=&quot;https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=158&quot;&gt;Chromium Dash&lt;/a&gt;:&lt;/p&gt;








































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Hito&lt;/th&gt;&lt;th&gt;Versión&lt;/th&gt;&lt;th&gt;Fecha&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Avisos en la consola&lt;/td&gt;&lt;td&gt;Chrome 142&lt;/td&gt;&lt;td&gt;28-oct-2025&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Deprecación oficial (consola y Lighthouse)&lt;/td&gt;&lt;td&gt;Chrome 143&lt;/td&gt;&lt;td&gt;2-dic-2025&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Arranca el Origin Trial para seguir usándolo&lt;/td&gt;&lt;td&gt;Chrome 152&lt;/td&gt;&lt;td&gt;25-ago-2026&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Beta de la versión que lo quita&lt;/td&gt;&lt;td&gt;Chrome 158&lt;/td&gt;&lt;td&gt;28-oct-2026&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;XSLT deja de funcionar en estable&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Chrome 158&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;17-nov-2026&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Se apagan el Origin Trial y la política empresarial &lt;code&gt;XSLTEnabled&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Chrome 176&lt;/td&gt;&lt;td&gt;17-ago-2027&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Hay un detalle que acorta el plazo más de lo que parece: &lt;a href=&quot;https://developer.chrome.com/blog/chrome-two-week-start&quot;&gt;desde Chrome 153 (8 de septiembre) sale una versión estable cada dos semanas&lt;/a&gt;, no cada cuatro. Entre hoy y la 158 hay nueve semanas y cinco versiones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¿Y Firefox y Safari?&lt;/strong&gt; Van en la misma dirección, sin fecha. Mozilla tomó &lt;a href=&quot;https://github.com/mozilla/standards-positions/issues/1287&quot;&gt;postura positiva&lt;/a&gt; y en su &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1990759&quot;&gt;bug de seguimiento&lt;/a&gt; ya cerró el aviso en consola (Firefox 147) y una política empresarial (Firefox 151), pero apagarlo por defecto sigue abierto. WebKit se declaró &lt;a href=&quot;https://github.com/whatwg/html/issues/11523#issuecomment-3149280766&quot;&gt;“cautelosamente a favor”&lt;/a&gt; y dijo que probablemente esperaría a que un motor lo quite del todo. El estándar HTML ya lo &lt;a href=&quot;https://github.com/whatwg/html/pull/12805&quot;&gt;marcó como deprecado&lt;/a&gt; el 25 de agosto de 2026. En la práctica, lo que diga Chrome 158 es lo que verá la mayoría de tus usuarios en noviembre.&lt;/p&gt;
&lt;h2 id=&quot;qué-se-rompe-qué-ves-y-cómo-se-arregla&quot;&gt;Qué se rompe, qué ves y cómo se arregla&lt;/h2&gt;








































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Qué usas&lt;/th&gt;&lt;th&gt;Qué pasa con Chrome 158&lt;/th&gt;&lt;th&gt;Arreglo&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Sitemap XML con hoja XSL (WordPress core, Yoast, Rank Math, &lt;code&gt;xslURL&lt;/code&gt; de &lt;code&gt;@astrojs/sitemap&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;El humano ve el visor de XML de Chrome en vez de la tabla. No hay error y el archivo servido no cambia&lt;/td&gt;&lt;td&gt;Nada, si nadie lo mira. Si importa, quita la instrucción o sirve una versión HTML aparte&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Feed RSS/Atom con hoja XSL (&lt;code&gt;stylesheet&lt;/code&gt; con &lt;code&gt;.xsl&lt;/code&gt; en &lt;code&gt;@astrojs/rss&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Abierto en el navegador, se ve XML crudo. Los lectores de feeds no lo notan&lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;lt;link rel=&amp;quot;alternate&amp;quot; type=&amp;quot;application/rss+xml&amp;quot;&amp;gt;&lt;/code&gt; en tu HTML, o el polyfill de una línea&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;JavaScript que llama &lt;code&gt;new XSLTProcessor()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;ReferenceError: XSLTProcessor is not defined&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Mover la transformación al servidor, o un motor XSLT en JS/WASM&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Visor o validador de CFDI que arma la cadena original en el navegador&lt;/td&gt;&lt;td&gt;La cadena no se genera, así que tampoco se puede verificar el sello&lt;/td&gt;&lt;td&gt;Servidor con ext-xsl, xslt-polyfill con la hoja aplanada, o SaxonJS (detalle abajo)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;lt;?xml-stylesheet type=&amp;quot;text/css&amp;quot;?&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Nada: sigue soportado&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;XSLT en tu servidor (PHP ext-xsl, Saxon, xsltproc)&lt;/td&gt;&lt;td&gt;Nada: esto solo afecta al navegador&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;h2 id=&quot;sitemaps-y-feeds-se-rompe-el-adorno-no-el-archivo&quot;&gt;Sitemaps y feeds: se rompe el adorno, no el archivo&lt;/h2&gt;
&lt;p&gt;Abrí el índice de sitemaps de &lt;code&gt;yoast.com&lt;/code&gt; en Chrome 152 dos veces. Con XSLT activo, Chrome aplica la hoja y pinta una página titulada “XML Sitemap” con su tabla. Con &lt;code&gt;--disable-features=XSLT&lt;/code&gt;, el mismo URL cae en el visor de XML de Chrome: el árbol de &lt;code&gt;&amp;lt;sitemap&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; y &lt;code&gt;&amp;lt;lastmod&amp;gt;&lt;/code&gt; con la instrucción &lt;code&gt;xml-stylesheet&lt;/code&gt; a la vista, sin estilos y sin error. Según el anuncio, Chrome además muestra un aviso que enlaza a extensiones cuando XSLT está apagado; en modo headless no lo pude ver.&lt;/p&gt;
&lt;p&gt;Esto le va a pasar a mucha gente, porque la hoja XSL viene de fábrica en casi todo el ecosistema:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;WordPress core&lt;/strong&gt; agrega &lt;code&gt;&amp;lt;?xml-stylesheet type=&amp;quot;text/xsl&amp;quot; href=&amp;quot;.../wp-sitemap.xsl&amp;quot; ?&amp;gt;&lt;/code&gt; a &lt;code&gt;wp-sitemap.xml&lt;/code&gt; desde la 5.5. El &lt;a href=&quot;https://core.trac.wordpress.org/ticket/65593&quot;&gt;ticket #65593&lt;/a&gt; para quitarla está en el milestone de la 7.2, con el &lt;a href=&quot;https://github.com/WordPress/wordpress-develop/pull/12448&quot;&gt;PR #12448&lt;/a&gt; abierto y otra propuesta (&lt;a href=&quot;https://github.com/WordPress/wordpress-develop/pull/12584&quot;&gt;#12584&lt;/a&gt;) que renderiza la versión HTML en el servidor. Al 13 de septiembre ninguno está fusionado.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Yoast SEO&lt;/strong&gt; la sigue emitiendo en su &lt;code&gt;trunk&lt;/code&gt;, que el changelog marca como 28.5 para el 15 de septiembre, y el sitemap del propio &lt;code&gt;yoast.com&lt;/code&gt; la trae hoy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rank Math&lt;/strong&gt; la sigue emitiendo en su &lt;code&gt;trunk&lt;/code&gt;, y el sitemap de &lt;code&gt;rankmath.com&lt;/code&gt; también la trae hoy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Astro&lt;/strong&gt;: &lt;code&gt;@astrojs/sitemap&lt;/code&gt; tiene la opción &lt;code&gt;xslURL&lt;/code&gt; y &lt;code&gt;@astrojs/rss&lt;/code&gt; pone &lt;code&gt;type=&amp;quot;text/xsl&amp;quot;&lt;/code&gt; si le pasas un &lt;code&gt;stylesheet&lt;/code&gt; que termina en &lt;code&gt;.xsl&lt;/code&gt;. Ninguna viene activa por defecto.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;y-google&quot;&gt;¿Y Google?&lt;/h3&gt;
&lt;p&gt;No voy a escribir “Googlebot no se ve afectado” como si Google lo hubiera dicho: no encontré ninguna declaración de Google Search sobre XSLT y sitemaps. Lo que sí es verificable es que el cambio vive en el navegador. El servidor manda exactamente los mismos bytes antes y después del 17 de noviembre, y el anuncio de Chrome aclara que no retiran XML, solo XSLT. Un rastreador que ya leía tu sitemap sin ejecutar la hoja lo va a seguir recibiendo igual.&lt;/p&gt;
&lt;h3 id=&quot;qué-hacer-con-el-sitemap&quot;&gt;Qué hacer con el sitemap&lt;/h3&gt;
&lt;p&gt;Si ningún humano abre tu sitemap, no hagas nada. Si lo usas como página de consulta o te lo piden clientes, tienes tres caminos:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Quitar la instrucción.&lt;/strong&gt; Te ahorras la petición al &lt;code&gt;.xsl&lt;/code&gt; y los avisos de consola. En WordPress core hay filtros oficiales: si devuelven un valor falso, según el propio código, “se mostrará el XML crudo”:&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// En un mu-plugin o en functions.php&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;add_filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;wp_sitemaps_stylesheet_url&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;__return_false&amp;#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;add_filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;wp_sitemaps_stylesheet_index_url&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;__return_false&amp;#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Yoast: el filtro se llama &amp;quot;url&amp;quot;, pero recibe la declaración completa&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;add_filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;wpseo_stylesheet_url&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;__return_empty_string&amp;#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En Rank Math el filtro va por tipo de sitemap (&lt;code&gt;rank_math/sitemap/{tipo}_stylesheet_url&lt;/code&gt;). En Astro basta con quitar &lt;code&gt;xslURL&lt;/code&gt; o &lt;code&gt;stylesheet&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Servir HTML aparte.&lt;/strong&gt; Es lo que propone el PR #12584 de WordPress: la misma transformación, pero hecha en el servidor, publicada en una URL &lt;code&gt;.html&lt;/code&gt;. El &lt;code&gt;.xml&lt;/code&gt; queda para las máquinas.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CSS en lugar de XSL.&lt;/strong&gt; &lt;code&gt;&amp;lt;?xml-stylesheet type=&amp;quot;text/css&amp;quot;?&amp;gt;&lt;/code&gt; sigue soportado, pero en el ticket de WordPress apuntaron el límite: con CSS le das estilo al XML, pero los &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; no se vuelven enlaces en los que se pueda hacer clic.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Para feeds, la recomendación de Chrome es anunciar el feed con &lt;code&gt;&amp;lt;link rel=&amp;quot;alternate&amp;quot;&amp;gt;&lt;/code&gt; en el HTML en vez de enlazar el &lt;code&gt;.xml&lt;/code&gt; a la vista, o agregar al feed una sola línea con el polyfill.&lt;/p&gt;
&lt;aside class=&quot;not-prose my-8 flex flex-wrap items-center gap-4 rounded-2xl border border-neutral-100 bg-white p-5 transition-all duration-200 hover:shadow-[5px_5px_rgba(0,98,90,0.3),10px_10px_rgba(0,98,90,0.2),15px_15px_rgba(0,98,90,0.1)] dark:border-zinc-800 dark:bg-zinc-900/40&quot; style=&quot;border-left:4px solid #f43f5e&quot;&gt; &lt;span class=&quot;grid size-12 shrink-0 place-items-center rounded-xl&quot; style=&quot;background:#f43f5e1a;color:#f43f5e&quot;&gt;  &lt;/span&gt; &lt;div class=&quot;flex min-w-0 flex-1 flex-col gap-0.5&quot;&gt; &lt;span class=&quot;text-xs font-semibold tracking-wide text-zinc-500 uppercase dark:text-zinc-400&quot;&gt; Herramienta gratuita &lt;/span&gt; &lt;span class=&quot;text-lg leading-snug font-bold text-blacktext dark:text-mint-50&quot;&gt; Formateador y Validador de XML &lt;/span&gt; &lt;span class=&quot;text-sm text-pretty text-zinc-600 dark:text-zinc-400&quot;&gt; Indenta, minifica y valida XML en tu navegador. Conserva comentarios, CDATA y la declaración, y te señala el error con línea y columna. &lt;/span&gt; &lt;/div&gt; &lt;a href=&quot;https://ortamarco.me/herramientas/formateador-xml/&quot; data-umami-event=&quot;tool_callout_click&quot; data-umami-event-tool=&quot;formateador-xml&quot; class=&quot;ml-auto shrink-0 rounded-xl bg-mint-600 px-4 py-2 text-sm! font-semibold text-white! no-underline! transition-colors hover:bg-mint-700 hover:text-white!&quot;&gt; Abrir herramienta → &lt;/a&gt; &lt;/aside&gt;
&lt;h2 id=&quot;cfdi-donde-sí-truena-código&quot;&gt;CFDI: donde sí truena código&lt;/h2&gt;
&lt;p&gt;Contexto para quien no factura en México: el &lt;strong&gt;CFDI&lt;/strong&gt; es la factura electrónica del SAT, un XML firmado. La firma (el atributo &lt;code&gt;Sello&lt;/code&gt;) no se calcula sobre el XML entero, sino sobre la &lt;strong&gt;cadena original&lt;/strong&gt;: los valores del comprobante en un orden fijo, separados por &lt;code&gt;|&lt;/code&gt;. Para verificar un sello necesitas esa cadena. Para mostrar la del timbre en una representación impresa, también.&lt;/p&gt;
&lt;p&gt;El SAT publica la secuencia de esa cadena como hoja XSLT: en su &lt;a href=&quot;http://omawww.sat.gob.mx/tramitesyservicios/Paginas/anexo_20.htm&quot;&gt;página del Anexo 20&lt;/a&gt;, el documento “Secuencia de cadena original” enlaza a &lt;a href=&quot;http://www.sat.gob.mx/sitio_internet/cfd/4/cadenaoriginal_4_0/cadenaoriginal_4_0.xslt&quot;&gt;&lt;code&gt;cadenaoriginal_4_0.xslt&lt;/code&gt;&lt;/a&gt;, con fecha de última modificación del 17 de abril de 2026. El timbre tiene la suya, &lt;code&gt;cadenaoriginal_TFD_1_1.xslt&lt;/code&gt;. Tres detalles de ese archivo importan aquí:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Declara &lt;code&gt;version=&amp;quot;2.0&amp;quot;&lt;/code&gt;, pero los navegadores solo implementan XSLT 1.0. Aun así funciona con procesadores 1.0: la documentación de &lt;a href=&quot;https://github.com/eclipxe13/CfdiUtils&quot;&gt;CfdiUtils&lt;/a&gt; lo dice de PHP, que usa el mismo motor que Chrome (libxslt), y lo confirmé abajo en el navegador.&lt;/li&gt;
&lt;li&gt;Trae &lt;strong&gt;33 &lt;code&gt;xsl:include&lt;/code&gt;&lt;/strong&gt; (las utilerías y los complementos), todos con URL absoluta a &lt;code&gt;http://www.sat.gob.mx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;La salida es texto plano (&lt;code&gt;method=&amp;quot;text&amp;quot;&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Por eso generar la cadena en el navegador era tentador: &lt;code&gt;XSLTProcessor&lt;/code&gt; venía incluido y el XML nunca salía de la máquina del usuario. Busqué código real que lo hace y encontré dos patrones:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Una librería npm de validadores y catálogos mexicanos, con versión publicada a finales de agosto de 2026, cuya función de cadena original usa &lt;code&gt;new XSLTProcessor()&lt;/code&gt; si detecta &lt;code&gt;window&lt;/code&gt;, y en Node recurre a un motor en JS. En Chrome 158 la detección sigue siendo verdadera (&lt;code&gt;DOMParser&lt;/code&gt; no se va), así que entra por la rama del navegador y lanza el &lt;code&gt;ReferenceError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Un repositorio público de firma electrónica en JavaScript que carga &lt;code&gt;cadenaoriginal_3_3.xslt&lt;/code&gt; desde un campo de archivo, lo guarda en &lt;code&gt;localStorage&lt;/code&gt; y arma la cadena con &lt;code&gt;XSLTProcessor.transformToDocument&lt;/code&gt; para verificar el sello de un CFDI 3.3.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;la-hoja-del-sat-no-corre-tal-cual-en-el-navegador&quot;&gt;La hoja del SAT no corre tal cual en el navegador&lt;/h3&gt;
&lt;p&gt;Esto lo encontré al probar y no lo vi documentado en ningún lado. Si le pasas a &lt;code&gt;XSLTProcessor&lt;/code&gt; la hoja oficial, Chrome se niega a cargar los 33 includes (el URL de abajo es mi página de prueba):&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Unsafe attempt to load URL http://www.sat.gob.mx/sitio_internet/cfd/2/cadenaoriginal_2_0/utilerias.xslt
from frame with URL http://localhost:8765/native.html. Domains, protocols and ports must match.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El resultado es &lt;code&gt;null&lt;/code&gt;. O sea: quien hoy arma la cadena en el navegador con &lt;code&gt;XSLTProcessor&lt;/code&gt; tiene que servir la hoja y sus includes desde su propio origen, o aplanados en un solo archivo. Eso ayuda a detectarlo, porque esa copia vive en tu repositorio y basta buscar &lt;code&gt;cadenaoriginal&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;por-qué-el-visor-de-cfdi-de-este-sitio-no-se-rompe&quot;&gt;Por qué el visor de CFDI de este sitio no se rompe&lt;/h3&gt;
&lt;p&gt;El &lt;a href=&quot;https://www.siemprecontable.net/herramientas/visor-cfdi&quot;&gt;visor de XML de CFDI 4.0&lt;/a&gt; que escribí para este sitio, y que hoy vive en Siempre Contable, no usa XSLT, y no por suerte. Lee el XML con &lt;code&gt;DOMParser&lt;/code&gt;, recorre &lt;code&gt;Comprobante&lt;/code&gt;, &lt;code&gt;Emisor&lt;/code&gt;, &lt;code&gt;Receptor&lt;/code&gt;, &lt;code&gt;Conceptos&lt;/code&gt;, &lt;code&gt;Impuestos&lt;/code&gt; y el &lt;code&gt;TimbreFiscalDigital&lt;/code&gt;, traduce los códigos del catálogo y comprueba la aritmética. No arma la cadena original ni valida el sello, y la herramienta lo dice en pantalla. &lt;code&gt;DOMParser&lt;/code&gt; es una de las APIs que Chrome cita como el camino moderno para leer XML, así que no está en riesgo. Hice &lt;code&gt;grep&lt;/code&gt; a todo el repositorio y no hay ni un &lt;code&gt;XSLTProcessor&lt;/code&gt; ni un &lt;code&gt;xml-stylesheet&lt;/code&gt;. Los sitemaps y el RSS del sitio tampoco llevan hoja. El servidor MCP &lt;a href=&quot;https://ortamarco.me/portfolio/mx-fiscal-mcp/&quot;&gt;mx-fiscal-mcp-server&lt;/a&gt;, que lee CFDI desde agentes de IA, corre en Node con &lt;code&gt;@xmldom/xmldom&lt;/code&gt; y tampoco usa XSLT.&lt;/p&gt;
&lt;h2 id=&quot;las-alternativas-probadas-contra-un-sello-real&quot;&gt;Las alternativas, probadas contra un sello real&lt;/h2&gt;
&lt;p&gt;Afirmar que “SaxonJS lo resuelve” es fácil. Para comprobarlo hice lo siguiente. Tomé &lt;code&gt;cfdi40-valid.xml&lt;/code&gt; de la suite de pruebas de CfdiUtils, un CFDI 4.0 firmado con un certificado de pruebas del SAT. Generé la cadena con cada motor y la di por buena solo si el &lt;code&gt;Sello&lt;/code&gt; del XML verifica contra ella con la llave pública de su propio &lt;code&gt;Certificado&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# cert.pem = el atributo Certificado en base64, envuelto como PEM&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# sello.bin = el atributo Sello decodificado de base64&lt;/span&gt;
openssl x509 &lt;span class=&quot;token parameter variable&quot;&gt;-in&lt;/span&gt; cert.pem &lt;span class=&quot;token parameter variable&quot;&gt;-pubkey&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-noout&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; pub.pem
openssl dgst &lt;span class=&quot;token parameter variable&quot;&gt;-sha256&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-verify&lt;/span&gt; pub.pem &lt;span class=&quot;token parameter variable&quot;&gt;-signature&lt;/span&gt; sello.bin cadena.txt
&lt;span class=&quot;token comment&quot;&gt;# Verified OK  → la cadena es la correcta&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La referencia fue Saxon-HE 12.5 (Java, licencia MPL 2.0) con la hoja oficial: &lt;code&gt;Verified OK&lt;/code&gt;. Lo demás:&lt;/p&gt;















































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Motor&lt;/th&gt;&lt;th&gt;Dónde corre&lt;/th&gt;&lt;th&gt;Licencia&lt;/th&gt;&lt;th&gt;Resultado con la hoja del SAT&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;XSLTProcessor&lt;/code&gt; nativo, hoja oficial&lt;/td&gt;&lt;td&gt;Navegador&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;Falla: bloquea los includes de otro dominio&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;XSLTProcessor&lt;/code&gt; nativo, copia local&lt;/td&gt;&lt;td&gt;Navegador&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;&lt;code&gt;Verified OK&lt;/code&gt;. Deja de existir en Chrome 158&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;xslt-polyfill 1.0.28, hoja aplanada en un solo archivo&lt;/td&gt;&lt;td&gt;Navegador (WASM)&lt;/td&gt;&lt;td&gt;BSD-3-Clause&lt;/td&gt;&lt;td&gt;&lt;code&gt;Verified OK&lt;/code&gt; con XSLT nativo apagado&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;xslt-polyfill 1.0.28, copia con includes&lt;/td&gt;&lt;td&gt;Navegador (WASM)&lt;/td&gt;&lt;td&gt;BSD-3-Clause&lt;/td&gt;&lt;td&gt;Falla: &lt;code&gt;Error: Unknown mime type undefined&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SaxonJS 2.7 (&lt;code&gt;saxon-js&lt;/code&gt; + &lt;code&gt;xslt3&lt;/code&gt;), hoja compilada a SEF&lt;/td&gt;&lt;td&gt;Node; el SEF también corre en navegador según su documentación&lt;/td&gt;&lt;td&gt;Gratuita, no es open source&lt;/td&gt;&lt;td&gt;&lt;code&gt;Verified OK&lt;/code&gt; en Node&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;xslt-processor 5.1.2&lt;/td&gt;&lt;td&gt;JS puro&lt;/td&gt;&lt;td&gt;LGPL-3.0&lt;/td&gt;&lt;td&gt;Cadena distinta y el sello no verifica&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;La cadena de xslt-processor empieza con &lt;code&gt;|||||&lt;/code&gt; (cinco barras en vez de dos). Con la hoja aplanada, la salida se reduce a tres barras. No lo descarto para otros usos, pero para la cadena original del SAT no sirve tal cual.&lt;/p&gt;
&lt;p&gt;El fallo del polyfill con includes no es un bug suelto, está en su README: resuelve &lt;code&gt;xsl:include&lt;/code&gt; con &lt;code&gt;fetch()&lt;/code&gt;, que es asíncrono, y los métodos de &lt;code&gt;XSLTProcessor&lt;/code&gt; son síncronos, así que fallan cuando la hoja tiene includes. Aplanar la hoja lo resuelve: sustituyes cada &lt;code&gt;&amp;lt;xsl:include&amp;gt;&lt;/code&gt; por el contenido de la hoja incluida, en el mismo orden. En la hoja 4.0 actual no hace falta agregar espacios de nombres, porque la raíz principal ya declara todos los que usan los includes.&lt;/p&gt;
&lt;h3 id=&quot;opción-1-en-el-servidor-la-más-aburrida-y-por-eso-la-mejor&quot;&gt;Opción 1: en el servidor (la más aburrida, y por eso la mejor)&lt;/h3&gt;
&lt;p&gt;ext-xsl de PHP sigue en el &lt;a href=&quot;https://www.php.net/manual/en/book.xsl.php&quot;&gt;manual de PHP&lt;/a&gt;, montada sobre libxslt. La retirada de Chrome no la toca. Con CfdiUtils (&lt;code&gt;eclipxe/cfdiutils&lt;/code&gt;, MIT, versión 3.0.4 del 11 de septiembre de 2026):&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;token package&quot;&gt;CfdiUtils&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;XmlResolver&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;XmlResolver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;token package&quot;&gt;CfdiUtils&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;CadenaOrigen&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;DOMBuilder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// XmlResolver descarga los XSLT del SAT y reescribe sus dependencias a copias locales&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$resolver&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;XmlResolver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$location&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$resolver&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resolveCadenaOrigenLocation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;4.0&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token variable&quot;&gt;$cadena&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DOMBuilder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$xmlContent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$location&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Un aviso sobre algo que circula: &lt;strong&gt;CfdiUtils no construye la cadena sin XSLT.&lt;/strong&gt; &lt;code&gt;DOMBuilder&lt;/code&gt; es un &lt;code&gt;XSLTProcessor&lt;/code&gt; de ext-xsl; también trae &lt;code&gt;GenkgoXslBuilder&lt;/code&gt; (XSLT 2.0 en PHP) y &lt;code&gt;SaxonbCliBuilder&lt;/code&gt;. Su autor explica que armarla sin XSLT se puede, pero es “mucho código para fabricar y testear” que además cambia con cada complemento.&lt;/p&gt;
&lt;p&gt;Desde el navegador queda en una petición:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; res &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;/api/cfdi/cadena-original&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;POST&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;&amp;#39;Content-Type&amp;#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;application/xml&amp;#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; xmlText&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cadena &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El precio es de privacidad: si tu visor prometía “el XML no sale de tu navegador”, con esto deja de ser cierto y tienes que cambiar el texto.&lt;/p&gt;
&lt;h3 id=&quot;opción-2-seguir-en-el-navegador-con-el-polyfill&quot;&gt;Opción 2: seguir en el navegador con el polyfill&lt;/h3&gt;
&lt;p&gt;Si la promesa de procesar en local es el producto, &lt;a href=&quot;https://github.com/mfreed7/xslt_polyfill&quot;&gt;xslt-polyfill&lt;/a&gt; reemplaza &lt;code&gt;XSLTProcessor&lt;/code&gt; con libxslt compilado a WebAssembly. Pesa unos 1.4 MB minificado. Con la hoja aplanada, tu código existente no cambia:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot; data-language=&quot;html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;quot;&lt;/span&gt;/vendor/xslt-polyfill.min.js&lt;span class=&quot;token punctuation&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token language-javascript&quot;&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; xsl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DOMParser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parseFromString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;hojaAplanada&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;application/xml&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cfdi &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DOMParser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parseFromString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;xmlText&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;application/xml&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; p &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;XSLTProcessor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// el del polyfill si el navegador ya no trae XSLT&lt;/span&gt;
  p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;importStylesheet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;xsl&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cadena &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;transformToFragment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cfdi&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mientras el navegador todavía tenga XSLT nativo, el polyfill no se instala. Si quieres probarlo hoy en tu Chrome actual, pon &lt;code&gt;window.xsltUsePolyfillAlways = true&lt;/code&gt; antes de cargar el script.&lt;/p&gt;
&lt;h3 id=&quot;opción-3-saxonjs&quot;&gt;Opción 3: SaxonJS&lt;/h3&gt;
&lt;p&gt;SaxonJS implementa las partes obligatorias de XSLT 3.0, que es de sobra para una hoja que declara 2.0. El flujo documentado es compilar la hoja a un archivo SEF y ejecutar ese SEF:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx xslt3 &lt;span class=&quot;token parameter variable&quot;&gt;-xsl:cadenaoriginal_4_0_plana.xslt&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-export:cadena.sef.json&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-nogo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; SaxonJS &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;saxon-js&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;principalResult&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; cadena &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; SaxonJS&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;stylesheetFileName&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;cadena.sef.json&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;sourceText&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; xmlText&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;serialized&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En mi prueba compilar tardó 2.4 s y el SEF pesó 3.65 MB. Ese tamaño cuenta si lo vas a mandar al navegador. Revisa la licencia antes de redistribuirlo: Saxonica la ofrece gratis, pero no es open source.&lt;/p&gt;
&lt;h2 id=&quot;cómo-saber-si-dependes-de-xslt&quot;&gt;Cómo saber si dependes de XSLT&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. Busca en el código.&lt;/strong&gt; Los aciertos en PHP, Java o .NET de servidor no te afectan; los de JavaScript que corre en el navegador sí:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rnE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;XSLTProcessor|transformToFragment|transformToDocument|importStylesheet|xml-stylesheet|cadenaoriginal&amp;quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--include&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;*.&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;js,mjs,ts,tsx,jsx,vue,svelte,astro,html,xml,php&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --exclude-dir&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;node_modules,vendor,.git&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;*.xsl&amp;quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;*.xslt&amp;quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; node_modules
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Busca en lo que compilaste.&lt;/strong&gt; La librería npm del ejemplo esconde &lt;code&gt;XSLTProcessor&lt;/code&gt; dentro de &lt;code&gt;node_modules&lt;/code&gt;, así que el grep de arriba no la ve. Corre el mismo patrón sobre la carpeta de salida (&lt;code&gt;dist/&lt;/code&gt;, &lt;code&gt;build/&lt;/code&gt;, &lt;code&gt;.next/&lt;/code&gt;, &lt;code&gt;public/build/&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Revisa lo que sirves.&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; https://tusitio.com/wp-sitemap.xml &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;300&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;xml-stylesheet[^?]*&amp;#39;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; https://tusitio.com/feed/ &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;300&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;xml-stylesheet[^?]*&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. Pruébalo con XSLT apagado.&lt;/strong&gt; No hace falta esperar a noviembre ni instalar Canary. En Chrome estable entra a &lt;code&gt;chrome://flags/#xslt&lt;/code&gt; y ponlo en &lt;em&gt;Disabled&lt;/em&gt;, o arranca Chrome con &lt;code&gt;--disable-features=XSLT&lt;/code&gt; (lo verifiqué en Chrome 152). Para una comprobación sin interfaz:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;chrome &lt;span class=&quot;token parameter variable&quot;&gt;--headless&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;new --disable-features&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;XSLT --dump-dom &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&amp;#39;data:text/html,&amp;lt;script&amp;gt;document.write(typeof XSLTProcessor)&amp;lt;/script&amp;gt;&amp;#39;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# undefined  → así se verá tu app en Chrome 158&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;5. Deja que producción te avise.&lt;/strong&gt; Chrome reporta la deprecación por la Reporting API con el id &lt;code&gt;XSLT&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ReportingObserver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;reports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; r &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; reports&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;XSLT&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sendBeacon&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;/telemetria/xslt&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sourceFile &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;types&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;deprecation&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;buffered&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;observe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y abre la consola: desde Chrome 143 cada uso imprime “XSLTProcessor and XSLT Processing Instructions have been deprecated by all browsers”.&lt;/p&gt;
&lt;h2 id=&quot;quién-tiene-que-actuar-de-verdad&quot;&gt;Quién tiene que actuar de verdad&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Tienes un visor, validador o generador de PDF de CFDI que arma la cadena original en el navegador:&lt;/strong&gt; eres el público de este post. Tienes hasta el 17 de noviembre. Decide entre servidor (y cambia el aviso de privacidad) o polyfill con hoja aplanada, y valida contra un sello real como arriba antes de publicar.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Usas &lt;code&gt;XSLTProcessor&lt;/code&gt; para cualquier otra cosa en el front:&lt;/strong&gt; mismo plazo, mismas opciones. Si no puedes migrar a tiempo, el &lt;a href=&quot;https://developer.chrome.com/docs/web-platform/origin-trials&quot;&gt;Origin Trial&lt;/a&gt; te da hasta agosto de 2027. Si lo que usa XSLT es un documento XML, no hay &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; donde poner la meta, así que el token va en la cabecera HTTP &lt;code&gt;Origin-Trial&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tienes un sitio WordPress, Yoast, Rank Math o Astro con sitemap estilizado:&lt;/strong&gt; no se rompe nada que importe para indexar. Quita la instrucción si te molesta el aviso, o sirve HTML aparte si alguien consulta ese sitemap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tu empresa usa una app interna o un equipo que sirve XML con XSL y no puedes tocar:&lt;/strong&gt; la política empresarial &lt;code&gt;XSLTEnabled&lt;/code&gt; y la extensión que recomienda Chrome son para ti. La política caduca en agosto de 2027. La extensión aplica el polyfill, así que no depende del XSLT nativo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solo generas la cadena en PHP, Java o .NET en el servidor:&lt;/strong&gt; no te toca. Esto es del navegador.&lt;/p&gt;
&lt;p&gt;Para seguir leyendo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/portfolio/mx-fiscal-mcp/&quot;&gt;mx-fiscal-mcp-server&lt;/a&gt;: la ficha del servidor MCP open source para leer CFDI 4.0 y validar RFC, CURP y CLABE desde agentes de IA.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/mejores-servidores-mcp-seo-2026/&quot;&gt;Mejores servidores MCP para SEO en 2026&lt;/a&gt;: si auditas sitemaps con agentes, ahí está qué herramienta hace qué.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/tecnicas-y-consejos-utiles-de-seo-para-sitios-de-wordpress/&quot;&gt;SEO para WordPress en 2026&lt;/a&gt;: la guía completa de Yoast y Rank Math, donde vive el sitemap del que hablamos.&lt;/li&gt;
&lt;/ul&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuándo quita Chrome el soporte de XSLT? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; En Chrome 158, que sale a estable el 17 de noviembre de 2026. Desde esa versión dejan de funcionar XSLTProcessor y el procesamiento de las instrucciones xml-stylesheet con type=&amp;quot;text/xsl&amp;quot;, salvo para sitios inscritos en el Origin Trial y equipos con la política empresarial XSLTEnabled. Ambas excepciones se apagan en Chrome 176, el 17 de agosto de 2027. La beta de Chrome 158 sale el 28 de octubre de 2026. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Se rompe mi sitemap XML con hoja XSL en Chrome 158? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; El archivo no. Lo que cambia es cómo lo ve una persona en Chrome: en vez de la tabla generada por la hoja XSL aparece el visor de XML crudo del navegador, sin error. Lo probé en Chrome 152 con XSLT apagado sobre el índice de sitemaps de yoast.com. El servidor sigue mandando exactamente los mismos bytes. Google no ha publicado nada específico sobre XSLT y sitemaps, pero el cambio es solo del navegador y no del documento XML. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cómo quito la hoja XSL del sitemap de WordPress? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; En WordPress core, con los filtros wp_sitemaps_stylesheet_url y wp_sitemaps_stylesheet_index_url devolviendo false, por ejemplo con __return_false; el propio código dice que así se muestra el XML crudo. En Yoast, el filtro wpseo_stylesheet_url recibe la declaración completa y puedes devolver una cadena vacía. En Rank Math el filtro va por tipo: rank_math/sitemap/{tipo}_stylesheet_url. WordPress core tiene abierto el ticket 65593 para quitar la hoja en la versión 7.2. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Por qué se rompen los visores de CFDI que usan XSLT en el navegador? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Porque la cadena original del CFDI 4.0 la define el SAT con la hoja cadenaoriginal_4_0.xslt, y la forma cómoda de ejecutarla en el navegador era XSLTProcessor. En Chrome 158 esa clase deja de existir y el código lanza ReferenceError: XSLTProcessor is not defined. Sin cadena original no se puede verificar el sello digital del comprobante. Leer el XML con DOMParser, como hace el visor de CFDI de este sitio, sigue funcionando. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué alternativa genera bien la cadena original del SAT sin XSLT del navegador? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Probé varias contra el sello de un CFDI 4.0 de prueba. Dieron la cadena correcta Saxon-HE en Java, SaxonJS 2.7 con la hoja compilada a SEF y xslt-polyfill 1.0.28 en el navegador con la hoja aplanada en un solo archivo. xslt-polyfill falla si la hoja conserva sus 33 xsl:include, y xslt-processor 5.1.2 produjo una cadena distinta con la que el sello no verifica. En servidor, PHP con ext-xsl, que es lo que usa CfdiUtils, no se ve afectado. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿CfdiUtils genera la cadena original sin XSLT? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No. Su DOMBuilder usa XSLTProcessor de la extensión ext-xsl de PHP con la hoja del SAT, y también ofrece GenkgoXslBuilder y SaxonbCliBuilder. Su documentación aclara que la hoja declara XSLT 2.0 pero la transformación con PHP da el resultado esperado. Como corre en el servidor, la retirada de XSLT en Chrome no le afecta. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cómo pruebo hoy mi sitio como si ya fuera Chrome 158? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; En Chrome estable, entra a chrome://flags/#xslt y ponlo en Disabled, o arranca el navegador con --disable-features=XSLT. Con eso typeof XSLTProcessor devuelve undefined y los XML con hoja XSL se muestran en crudo. Para detectar usos en producción, registra un ReportingObserver de tipo deprecation y filtra los reportes con id XSLT. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Desarrollo Web</category><category>Chrome</category><category>XSLT</category><category>XML</category><category>CFDI</category><category>SEO</category><category>Migración</category><author>Marco Orta</author></item><item><title>GitHub Actions en otoño de 2026: qué se rompe (y por qué node20 no es el problema)</title><link>https://ortamarco.me/blog/github-actions-que-se-rompe-otono-2026/</link><guid isPermaLink="true">https://ortamarco.me/blog/github-actions-que-se-rompe-otono-2026/</guid><description>El 23-sep GitHub quita Node 20, pero una action node20 no falla: ya corre en Node 24. Lo que se rompe es la salida de emergencia. Fechas y greps para auditar.</description><pubDate>Sun, 13 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;El 23 de septiembre de 2026 GitHub quita Node 20 de los runners de Actions, y casi todo el mundo lo está leyendo como «mis actions con &lt;code&gt;using: node20&lt;/code&gt; van a fallar». No van a fallar. Desde el 16 de junio el runner ya reescribe &lt;code&gt;node20&lt;/code&gt; a &lt;code&gt;node24&lt;/code&gt; antes de ejecutar, y lo mismo hace con &lt;code&gt;node12&lt;/code&gt; y &lt;code&gt;node16&lt;/code&gt;. Lo que se quita el 23 es la salida de emergencia: la variable &lt;code&gt;ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION&lt;/code&gt; deja de funcionar.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Eso invierte quién está en riesgo. Si tu CI lleva verde desde junio sin esa variable, el 23 no te cambia nada en los runners de GitHub. Los que se rompen son justo los que &lt;em&gt;ya se toparon&lt;/em&gt; con una action que no aguanta Node 24, pusieron la variable para seguir avanzando y dejaron el arreglo para después. Y alrededor de esa fecha hay otras cuatro con consecuencias reales: la versión mínima de los runners self-hosted (25 de septiembre), la retención de checks y ejecuciones (1 de octubre), los apagones de &lt;code&gt;macos-14&lt;/code&gt; (octubre) y su retiro (2 de noviembre).&lt;/p&gt;
&lt;p&gt;Lo verifiqué contra el código fuente del runner y contra el CI de este mismo sitio, que usa dos actions &lt;code&gt;node20&lt;/code&gt;. Abajo va todo con su fuente.&lt;/p&gt;
&lt;h2 id=&quot;el-calendario-por-fecha&quot;&gt;El calendario, por fecha&lt;/h2&gt;

































































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Fecha&lt;/th&gt;&lt;th&gt;Qué cambia&lt;/th&gt;&lt;th&gt;Qué se rompe&lt;/th&gt;&lt;th&gt;Cómo lo detectas&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;14, 16 y 18 sep&lt;/strong&gt; (11:00-15:00 ET, 9:00-13:00 en CDMX)&lt;/td&gt;&lt;td&gt;Apagones previos a la versión mínima de runners self-hosted&lt;/td&gt;&lt;td&gt;Los runners desactualizados &lt;strong&gt;no se registran ni ejecutan jobs&lt;/strong&gt; durante la ventana&lt;/td&gt;&lt;td&gt;API de deprecaciones del runner (abajo)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;17 sep&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;ubuntu-22.04&lt;/code&gt; y &lt;code&gt;ubuntu-22.04-arm&lt;/code&gt; entran en deprecación&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Nada falla todavía&lt;/strong&gt;; puede haber colas más largas&lt;/td&gt;&lt;td&gt;&lt;code&gt;grep&lt;/code&gt; de la etiqueta&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;21-30 sep&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;windows-11-arm&lt;/code&gt; pasa a la imagen con Visual Studio 2026&lt;/td&gt;&lt;td&gt;Cambia la cadena de herramientas por debajo&lt;/td&gt;&lt;td&gt;&lt;code&gt;grep windows-11-arm&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;23 sep&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Node 20 sale de los runners&lt;/td&gt;&lt;td&gt;&lt;code&gt;ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION&lt;/code&gt; se ignora; runners Linux ARM32 sin soporte&lt;/td&gt;&lt;td&gt;&lt;code&gt;grep&lt;/code&gt; de la variable&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;25 sep&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Se aplica la versión mínima de runners self-hosted&lt;/td&gt;&lt;td&gt;Registrar exige ≥ 2.329.0; un runner que no instala cada versión en 30 días &lt;strong&gt;deja de recibir jobs&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;API de deprecaciones&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;1 oct&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;La retención de Actions pasa a cubrir checks, ejecuciones y statuses&lt;/td&gt;&lt;td&gt;Lo que pase tu periodo de retención (90 días por defecto) &lt;strong&gt;se borra&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Ajuste de retención del repo/org&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;5-31 oct&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Ocho apagones de &lt;code&gt;macos-14&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Los jobs programados en la ventana &lt;strong&gt;fallan&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;grep macos-14&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;2 nov&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;macos-14&lt;/code&gt; retirado&lt;/td&gt;&lt;td&gt;Los jobs con esa etiqueta &lt;strong&gt;terminan con error&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;grep macos-14&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;17 abr 2027&lt;/td&gt;&lt;td&gt;&lt;code&gt;ubuntu-22.04&lt;/code&gt; retirado (apagones antes, en marzo y abril)&lt;/td&gt;&lt;td&gt;Los jobs terminan con error&lt;/td&gt;&lt;td&gt;&lt;code&gt;grep&lt;/code&gt; de la etiqueta&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;h2 id=&quot;1-node-20-qué-pasa-de-verdad-el-23-de-septiembre&quot;&gt;1. Node 20: qué pasa de verdad el 23 de septiembre&lt;/h2&gt;
&lt;h3 id=&quot;lo-que-dice-el-aviso&quot;&gt;Lo que dice el aviso&lt;/h3&gt;
&lt;p&gt;El &lt;a href=&quot;https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/&quot;&gt;changelog de GitHub del 19 de septiembre de 2025&lt;/a&gt;, con una nota del editor del 25 de agosto de 2026, fija dos fechas: desde el &lt;strong&gt;16 de junio de 2026&lt;/strong&gt; los runners usan Node 24 por defecto, y la variable &lt;code&gt;ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true&lt;/code&gt; para volver a Node 20 «solo funcionará hasta que actualicemos el runner y quitemos Node20 el &lt;strong&gt;23 de septiembre de 2026&lt;/strong&gt;». Añade dos límites de plataforma: Node 24 es incompatible con macOS 13.4 y anteriores, y no tiene soporte oficial para ARM32.&lt;/p&gt;
&lt;p&gt;Lo que el aviso no explica es qué le pasa a una action cuyo &lt;code&gt;action.yml&lt;/code&gt; sigue diciendo &lt;code&gt;using: node20&lt;/code&gt;. Eso está en el código.&lt;/p&gt;
&lt;h3 id=&quot;lo-que-dice-el-código-del-runner&quot;&gt;Lo que dice el código del runner&lt;/h3&gt;
&lt;p&gt;En &lt;a href=&quot;https://github.com/actions/runner/blob/main/src/Runner.Worker/Handlers/HandlerFactory.cs&quot;&gt;&lt;code&gt;HandlerFactory.cs&lt;/code&gt;&lt;/a&gt; (igual en la etiqueta v2.337.0, la versión actual), el runner hace dos cosas antes de lanzar una action de JavaScript:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Si la action declara &lt;code&gt;node12&lt;/code&gt; o &lt;code&gt;node16&lt;/code&gt;, la sube a &lt;code&gt;node20&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Si queda en &lt;code&gt;node20&lt;/code&gt;, le pregunta a &lt;a href=&quot;https://github.com/actions/runner/blob/main/src/Runner.Common/Util/NodeUtil.cs&quot;&gt;&lt;code&gt;NodeUtil.DetermineActionsNodeVersion&lt;/code&gt;&lt;/a&gt; qué versión usar.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Y esa función tiene tres fases, controladas por &lt;em&gt;feature flags&lt;/em&gt; del lado de GitHub:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot; data-language=&quot;csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Phase 3: Always use Node 24 regardless of environment variables&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;requireNode24&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Constants&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Runner&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;NodeMigration&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Node24&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Phase 2: Node 24 is the default&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;useNode24ByDefault&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;allowUnsecureNode&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Constants&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Runner&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;NodeMigration&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Node20&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Constants&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Runner&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;NodeMigration&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Node24&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El &lt;a href=&quot;https://github.com/actions/runner/pull/3948&quot;&gt;PR #3948&lt;/a&gt; que introdujo esto lo describe sin rodeos: en la fase 3, «todas las actions usan Node 24» y «no hay opciones de exclusión». Ninguna rama devuelve un error por declarar &lt;code&gt;node20&lt;/code&gt;. La action se ejecuta con otro Node, que es muy distinto de no ejecutarse.&lt;/p&gt;
&lt;p&gt;Fíjate también en el orden: &lt;code&gt;node12&lt;/code&gt; → &lt;code&gt;node20&lt;/code&gt; → &lt;code&gt;node24&lt;/code&gt;. Una action vieja con &lt;code&gt;using: node16&lt;/code&gt; (por ejemplo &lt;code&gt;actions/checkout@v3&lt;/code&gt;) acaba igualmente en Node 24.&lt;/p&gt;
&lt;h3 id=&quot;la-prueba-el-ci-de-este-sitio&quot;&gt;La prueba: el CI de este sitio&lt;/h3&gt;
&lt;p&gt;El workflow de CI de este blog usa &lt;code&gt;actions/checkout@v4&lt;/code&gt; y &lt;code&gt;actions/setup-node@v4&lt;/code&gt;. Los &lt;code&gt;action.yml&lt;/code&gt; oficiales de esas etiquetas declaran &lt;code&gt;using: node20&lt;/code&gt;; los de &lt;code&gt;v5&lt;/code&gt; en adelante declaran &lt;code&gt;node24&lt;/code&gt; (lo comprobé en las etiquetas &lt;code&gt;v4&lt;/code&gt; a &lt;code&gt;v7&lt;/code&gt; de las dos). La ejecución del 11 de septiembre, en el runner 2.337.0 sobre la imagen &lt;code&gt;ubuntu-24.04&lt;/code&gt;, terminó en verde con esta anotación:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Node.js 20 is deprecated. The following actions target Node.js 20 but are being
forced to run on Node.js 24: actions/checkout@v4, actions/setup-node@v4.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y en el log de los pasos de esas dos actions, esta línea:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Node 20 is being deprecated. This workflow is running with Node 24 by default.
If you need to temporarily use Node 20, you can set the
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La misma anotación sale desde la primera ejecución de ese workflow, el 23 de agosto. Es decir: mis dos actions &lt;code&gt;node20&lt;/code&gt; nunca han corrido en Node 20 en este CI, y yo no toqué nada para eso. El 23 de septiembre no les cambia el runtime, porque el cambio ya ocurrió en junio. Actualizarlas a &lt;code&gt;v5&lt;/code&gt; o superior quita la anotación, y conviene hacerlo, pero no hay reloj.&lt;/p&gt;
&lt;h3 id=&quot;cómo-se-ve-cuando-sí-se-rompe&quot;&gt;Cómo se ve cuando sí se rompe&lt;/h3&gt;
&lt;p&gt;Hay tres formas reales de fallar, y ninguna es un mensaje de GitHub que diga «Node 20 ya no existe».&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tenías la variable puesta.&lt;/strong&gt; Alguna action tuya tropezó con Node 24 después del 16 de junio, pusiste &lt;code&gt;ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true&lt;/code&gt; y siguió en Node 20. En la fase 3 el runner &lt;strong&gt;ignora la variable sin avisar&lt;/strong&gt;: la rama de &lt;code&gt;requireNode24&lt;/code&gt; devuelve &lt;code&gt;null&lt;/code&gt; como mensaje. La action vuelve a Node 24 y el error que ves es el de su propio código, el mismo que viste la primera vez. Una pista de que la fase 3 ya está activa: con el código actual del runner, la línea «If you need to temporarily use Node 20…» deja de salir en el log, porque solo se imprime cuando Node 24 es el valor por defecto pero todavía no es obligatorio.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La action usa algo que Node 24 eliminó.&lt;/strong&gt; Según la &lt;a href=&quot;https://nodejs.org/docs/latest-v24.x/api/deprecations.html&quot;&gt;tabla de deprecaciones de Node 24&lt;/a&gt; y las &lt;a href=&quot;https://nodejs.org/en/blog/release/v24.0.0&quot;&gt;notas de la 24.0.0&lt;/a&gt;, llegaron a fin de vida en esa versión:&lt;/p&gt;






























&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;API&lt;/th&gt;&lt;th&gt;Estado en Node 24&lt;/th&gt;&lt;th&gt;Cómo falla&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;tls.createSecurePair()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Eliminada (DEP0064)&lt;/td&gt;&lt;td&gt;&lt;code&gt;TypeError: ... is not a function&lt;/code&gt; al llamarla&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;fs.truncate()&lt;/code&gt; con un descriptor de archivo&lt;/td&gt;&lt;td&gt;Eliminado (DEP0081)&lt;/td&gt;&lt;td&gt;Lanza &lt;code&gt;ERR_INVALID_ARG_TYPE&lt;/code&gt;: espera una ruta. Usa &lt;code&gt;fs.ftruncate()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;dirent.path&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Eliminada (DEP0178)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;No lanza por sí sola&lt;/strong&gt;: la propiedad ya no existe y lees &lt;code&gt;undefined&lt;/code&gt;. Usa &lt;code&gt;dirent.parentPath&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;OutgoingMessage.prototype._headers&lt;/code&gt; / &lt;code&gt;_headerNames&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Eliminadas (DEP0066)&lt;/td&gt;&lt;td&gt;Lees &lt;code&gt;undefined&lt;/code&gt;. Usa &lt;code&gt;getHeaders()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;El caso de &lt;code&gt;dirent.path&lt;/code&gt; es el más traicionero de la lista, porque leerla no lanza nada: el fallo aparece más adelante, donde se use esa ruta, o nunca, si solo se concatena en un texto y sale &lt;code&gt;undefined/archivo&lt;/code&gt;. Si mantienes una action que recorre directorios, busca esa propiedad antes de fiarte del check verde.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tu runner self-hosted no puede ejecutar Node 24.&lt;/strong&gt; En Linux ARM32, el mismo &lt;code&gt;NodeUtil.cs&lt;/code&gt; tiene un interruptor de apagado que hace fallar el paso con:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Linux ARM32 runners are no longer supported. Please migrate to a supported platform.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Antes de que se active, el runner mantiene esas actions en Node 20 con un aviso que usa como fecha por defecto la de la retirada de Node 20: «Linux ARM32 runners are deprecated and will no longer be supported after September 23rd, 2026». GitHub no ha publicado el día exacto en que se activa; el changelog dice que ARM32 deja de tener soporte tras la deprecación de Node 20. En macOS, el &lt;a href=&quot;https://github.com/nodejs/node/blob/v24.x/BUILDING.md&quot;&gt;&lt;code&gt;BUILDING.md&lt;/code&gt; de Node 24&lt;/a&gt; exige macOS 13.5 o superior.&lt;/p&gt;
&lt;h2 id=&quot;2-la-auditoría-qué-buscar-y-dónde&quot;&gt;2. La auditoría: qué buscar y dónde&lt;/h2&gt;
&lt;h3 id=&quot;en-un-repositorio&quot;&gt;En un repositorio&lt;/h3&gt;
&lt;p&gt;Corre esto desde la raíz:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 1. El interruptor que deja de funcionar el 23 de septiembre&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rn&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION&amp;#39;&lt;/span&gt; .github/

&lt;span class=&quot;token comment&quot;&gt;# 2. Etiquetas de imagen con fecha. Sin &amp;quot;runs-on:&amp;quot; a propósito:&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#    así también aparecen las matrices (os: [ubuntu-22.04, ...])&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rnE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;ubuntu-22\.04|macos-14|windows-11-arm&amp;#39;&lt;/span&gt; .github/

&lt;span class=&quot;token comment&quot;&gt;# 3. Actions propias o locales que declaran un Node viejo&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rnE&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--include&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;action.yml &lt;span class=&quot;token parameter variable&quot;&gt;--include&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;action.yaml &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&amp;quot;using:[[:space:]]*[&amp;#39;&lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&amp;quot;&lt;/span&gt;]?node(12|16|20)&amp;quot;&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La búsqueda 2 tiene un punto ciego: &lt;code&gt;runs-on: ${{ vars.RUNNER }}&lt;/code&gt; o una etiqueta que llega como input a un workflow reutilizable no aparece en ningún &lt;code&gt;grep&lt;/code&gt;. Para esos casos, la fuente de verdad es el log. El paso «Set up job» imprime la imagen real:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;gh run view &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;run-id&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--log&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-m1&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;Image:&amp;#39;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Image: ubuntu-24.04&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si la matriz es larga o viene anidada, pasarla a JSON y filtrarla con &lt;code&gt;jq&lt;/code&gt; suele ser más rápido que leer el YAML a ojo:&lt;/p&gt;
&lt;aside class=&quot;not-prose my-8 flex flex-wrap items-center gap-4 rounded-2xl border border-neutral-100 bg-white p-5 transition-all duration-200 hover:shadow-[5px_5px_rgba(0,98,90,0.3),10px_10px_rgba(0,98,90,0.2),15px_15px_rgba(0,98,90,0.1)] dark:border-zinc-800 dark:bg-zinc-900/40&quot; style=&quot;border-left:4px solid #f43f5e&quot;&gt; &lt;span class=&quot;grid size-12 shrink-0 place-items-center rounded-xl&quot; style=&quot;background:#f43f5e1a;color:#f43f5e&quot;&gt;  &lt;/span&gt; &lt;div class=&quot;flex min-w-0 flex-1 flex-col gap-0.5&quot;&gt; &lt;span class=&quot;text-xs font-semibold tracking-wide text-zinc-500 uppercase dark:text-zinc-400&quot;&gt; Herramienta gratuita &lt;/span&gt; &lt;span class=&quot;text-lg leading-snug font-bold text-blacktext dark:text-mint-50&quot;&gt; Conversor de JSON y YAML &lt;/span&gt; &lt;span class=&quot;text-sm text-pretty text-zinc-600 dark:text-zinc-400&quot;&gt; Convierte JSON a YAML y YAML a JSON al instante, con indentación configurable, descarga y copia. Todo en tu navegador, sin subir nada. &lt;/span&gt; &lt;/div&gt; &lt;a href=&quot;https://ortamarco.me/herramientas/conversor-json-yaml/&quot; data-umami-event=&quot;tool_callout_click&quot; data-umami-event-tool=&quot;conversor-json-yaml&quot; class=&quot;ml-auto shrink-0 rounded-xl bg-mint-600 px-4 py-2 text-sm! font-semibold text-white! no-underline! transition-colors hover:bg-mint-700 hover:text-white!&quot;&gt; Abrir herramienta → &lt;/a&gt; &lt;/aside&gt;
&lt;p&gt;Para saber qué runtime declara cada action de terceros que usas, incluidas las fijadas por SHA, hay que leer su &lt;code&gt;action.yml&lt;/code&gt; en esa referencia exacta. Este script lo hace con &lt;code&gt;gh&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rhoE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;uses:[[:space:]]*[&amp;#39;&lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&amp;quot;&lt;/span&gt;]?[^[:space:]&amp;#39;&lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&amp;quot;&lt;/span&gt;#]+@[^[:space:]&amp;#39;&lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&amp;quot;&lt;/span&gt;#]+&amp;quot;&lt;/span&gt; .github/ &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;s/uses:[[:space:]]*[&amp;#39;&lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&amp;quot;&lt;/span&gt;]?//&amp;quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-u&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; ref&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;token assign-left variable&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;${ref&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;@*}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;ver&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;${ref&lt;span class=&quot;token operator&quot;&gt;#&lt;/span&gt;*@}&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;&lt;span class=&quot;token variable&quot;&gt;$spec&lt;/span&gt;&amp;quot;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; ./*&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;docker://*&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;continue&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;esac&lt;/span&gt;
      &lt;span class=&quot;token assign-left variable&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;cut&lt;/span&gt; -d/ -f1-2 &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;&lt;span class=&quot;token variable&quot;&gt;$spec&lt;/span&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;sub&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;cut&lt;/span&gt; -d/ -f3- &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;&lt;span class=&quot;token variable&quot;&gt;$spec&lt;/span&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token for-or-select variable&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; action.yml action.yaml&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
        &lt;span class=&quot;token assign-left variable&quot;&gt;rt&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;gh api &lt;span class=&quot;token string&quot;&gt;&amp;quot;repos/&lt;span class=&quot;token variable&quot;&gt;$repo&lt;/span&gt;/contents/&lt;span class=&quot;token variable&quot;&gt;${sub&lt;span class=&quot;token operator&quot;&gt;:+&lt;/span&gt;$sub&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;}&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$f&lt;/span&gt;?ref=&lt;span class=&quot;token variable&quot;&gt;$ver&lt;/span&gt;&amp;quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--jq&lt;/span&gt; .content &lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;2&lt;/span&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
             &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; base64 &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;^[[:space:]]*using:&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tr&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot; &amp;#39;&lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&amp;quot;&lt;/span&gt;&amp;quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cut&lt;/span&gt; -d: &lt;span class=&quot;token parameter variable&quot;&gt;-f2&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;&lt;span class=&quot;token variable&quot;&gt;$rt&lt;/span&gt;&amp;quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;&lt;span class=&quot;token variable&quot;&gt;$rt&lt;/span&gt;  &lt;span class=&quot;token variable&quot;&gt;$ref&lt;/span&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En este repo imprime:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;node20  actions/checkout@v4
node20  actions/setup-node@v4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Soporta subrutas (&lt;code&gt;github/codeql-action/init@v3&lt;/code&gt;) y SHAs. Un SHA que apunta a una versión &lt;code&gt;node20&lt;/code&gt; &lt;strong&gt;no falla el 23&lt;/strong&gt; por lo mismo que no falla &lt;code&gt;@v4&lt;/code&gt;: el runner la fuerza a Node 24. Lo que cambia con un SHA es cómo lo actualizas: no recibe nada solo, tiene que moverlo Dependabot o una persona.&lt;/p&gt;
&lt;p&gt;Un aviso antes de subir de major: lee las notas de versión. &lt;code&gt;actions/setup-node&lt;/code&gt; v5 &lt;a href=&quot;https://github.com/actions/setup-node/releases/tag/v5.0.0&quot;&gt;activó caché automática&lt;/a&gt; cuando &lt;code&gt;package.json&lt;/code&gt; trae el campo &lt;code&gt;packageManager&lt;/code&gt;, y la v6 &lt;a href=&quot;https://github.com/actions/setup-node/releases/tag/v6.0.0&quot;&gt;la limitó a npm&lt;/a&gt;. Si ya pasas &lt;code&gt;cache: npm&lt;/code&gt; explícito, como en mi CI, no te afecta.&lt;/p&gt;
&lt;h3 id=&quot;en-una-organización&quot;&gt;En una organización&lt;/h3&gt;
&lt;p&gt;La búsqueda de código de GitHub llega a todos los repos de una vez:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;gh search code &lt;span class=&quot;token parameter variable&quot;&gt;--owner&lt;/span&gt; TU_ORG &lt;span class=&quot;token string&quot;&gt;&amp;#39;ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION&amp;#39;&lt;/span&gt;
gh search code &lt;span class=&quot;token parameter variable&quot;&gt;--owner&lt;/span&gt; TU_ORG &lt;span class=&quot;token string&quot;&gt;&amp;#39;ubuntu-22.04 path:.github/workflows&amp;#39;&lt;/span&gt;
gh search code &lt;span class=&quot;token parameter variable&quot;&gt;--owner&lt;/span&gt; TU_ORG &lt;span class=&quot;token string&quot;&gt;&amp;#39;macos-14 path:.github/workflows&amp;#39;&lt;/span&gt;
gh search code &lt;span class=&quot;token parameter variable&quot;&gt;--owner&lt;/span&gt; TU_ORG node20 &lt;span class=&quot;token parameter variable&quot;&gt;--filename&lt;/span&gt; action.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dos límites de la &lt;a href=&quot;https://docs.github.com/en/rest/search/search#search-code&quot;&gt;API de búsqueda de código&lt;/a&gt; que te van a morder: &lt;strong&gt;solo busca en la rama por defecto&lt;/strong&gt; (un workflow en una rama de release no sale) y permite &lt;strong&gt;10 peticiones por minuto&lt;/strong&gt;. Preparando este post me topé con &lt;code&gt;HTTP 403: API rate limit exceeded&lt;/code&gt; en cuanto encadené unas cuantas búsquedas; mete un &lt;code&gt;sleep&lt;/code&gt; entre ellas si lo vas a automatizar.&lt;/p&gt;
&lt;h2 id=&quot;3-runners-self-hosted-dos-fechas-en-tres-días&quot;&gt;3. Runners self-hosted: dos fechas en tres días&lt;/h2&gt;
&lt;p&gt;Si solo usas runners de GitHub, salta esta sección. Si tienes los tuyos, el 25 de septiembre pesa más que el 23.&lt;/p&gt;
&lt;p&gt;El &lt;a href=&quot;https://github.blog/changelog/2026-06-12-github-actions-minimum-version-enforcement-timeline-for-self-hosted-runners&quot;&gt;changelog del 12 de junio&lt;/a&gt; retoma la aplicación de dos reglas en github.com:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Para registrar&lt;/strong&gt; (o volver a registrar) un runner hace falta la versión &lt;strong&gt;2.329.0&lt;/strong&gt; o superior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Para seguir ejecutando jobs&lt;/strong&gt;, el runner tiene que instalar cada versión nueva &lt;strong&gt;en menos de 30 días&lt;/strong&gt; desde su publicación. «Un runner fijado en 2.329.0 que nunca se actualiza no recibirá jobs».&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La aplicación completa empieza el &lt;strong&gt;25 de septiembre de 2026&lt;/strong&gt; (la tabla del aviso rotula esa fecha como GitHub Enterprise Cloud, y el mismo aviso aclara que el cambio aplica a github.com), con apagones antes: los días 14, 16 y 18 de septiembre, de 11:00 a 15:00 hora del Este (9:00 a 13:00 en la Ciudad de México), los runners fuera de versión ni se registran ni ejecutan. Lo que ves, según el aviso, es que el workflow «puede quedarse en cola o fallar».&lt;/p&gt;
&lt;p&gt;La forma de saber cuándo caduca tu versión es un endpoint que GitHub &lt;a href=&quot;https://github.blog/changelog/2026-09-03-github-actions-early-september-2026-updates&quot;&gt;añadió el 3 de septiembre&lt;/a&gt;. Lo consulté hoy:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;gh api repos/OWNER/REPO/actions/runners/deprecations/2.335.1
&lt;span class=&quot;token comment&quot;&gt;# {&amp;quot;runner_version&amp;quot;:&amp;quot;2.335.1&amp;quot;,&amp;quot;runtime_deprecates_at&amp;quot;:&amp;quot;2026-09-24T15:30:55Z&amp;quot;}&lt;/span&gt;

gh api repos/OWNER/REPO/actions/runners/deprecations/2.337.0
&lt;span class=&quot;token comment&quot;&gt;# {&amp;quot;runner_version&amp;quot;:&amp;quot;2.337.0&amp;quot;,&amp;quot;runtime_deprecates_at&amp;quot;:null}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Un runner en 2.335.1 queda fuera de plazo el 24 de septiembre, así que cuando empiece la aplicación completa, el 25, ya no recibe jobs. La versión instalada la sacas en la propia máquina con &lt;code&gt;./config.sh --version&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;El checklist para self-hosted, en orden:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Actualiza el runner&lt;/strong&gt; a la versión actual y deja activada la actualización automática. Si la tienes apagada, necesitas una cadencia manual de menos de 30 días.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rehaz las imágenes, plantillas de VM y contenedores&lt;/strong&gt; con los que creas runners. El changelog pide rehacer los que salen de imágenes o plantillas viejas: si la imagen trae un runner anterior a 2.329.0, no se registra.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux ARM32:&lt;/strong&gt; migra a arm64. Node 24 no tiene soporte oficial ahí, el paquete del runner para &lt;code&gt;linux-arm&lt;/code&gt; solo trae el binario de Node 20 (&lt;a href=&quot;https://github.com/actions/runner/blob/main/src/Misc/externals.sh&quot;&gt;&lt;code&gt;externals.sh&lt;/code&gt;&lt;/a&gt;) y el código que hace fallar el paso ya está en el runner.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;macOS 13.4 o anterior:&lt;/strong&gt; actualiza el sistema operativo. GitHub declara Node 24 incompatible con esas versiones.&lt;/li&gt;
&lt;li&gt;Las actions que ya declaran &lt;code&gt;node24&lt;/code&gt; (como &lt;code&gt;actions/checkout@v5&lt;/code&gt;) piden como mínimo el runner &lt;a href=&quot;https://github.com/actions/checkout/releases/tag/v5.0.0&quot;&gt;v2.327.1&lt;/a&gt;. Si cumples el punto 1, esto ya lo tienes.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;4-retención-1-de-octubre-el-historial-se-borra&quot;&gt;4. Retención, 1 de octubre: el historial se borra&lt;/h2&gt;
&lt;p&gt;Este cambio no rompe ningún build, pero borra datos. Según el &lt;a href=&quot;https://github.blog/changelog/2026-08-27-actions-retention-will-cover-checks-workflow-runs-and-statuses&quot;&gt;changelog del 27 de agosto&lt;/a&gt;, desde el &lt;strong&gt;1 de octubre de 2026&lt;/strong&gt; los checks, las ejecuciones de workflows y los statuses siguen el mismo ajuste de retención que ya controlaba los artefactos y los logs, &lt;strong&gt;90 días por defecto&lt;/strong&gt;. Hasta ahora se guardaban «400+ días» sin importar lo que configuraras.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;En repositorios públicos el máximo es &lt;strong&gt;90 días&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;El cambio &lt;strong&gt;no es retroactivo&lt;/strong&gt;: subir la retención después no recupera nada.&lt;/li&gt;
&lt;li&gt;Los metadatos de checks y ejecuciones no se cobran como almacenamiento; los artefactos y logs asociados sí.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lo que se rompe es todo lo que lea ese historial más allá de tu periodo: métricas de despliegue calculadas desde &lt;code&gt;gh run list&lt;/code&gt;, auditorías que enlazan a una ejecución concreta o reportes trimestrales. Exporta lo que necesites antes del 1 de octubre.&lt;/p&gt;
&lt;p&gt;Lo que el changelog &lt;strong&gt;no&lt;/strong&gt; dice: qué pasa con un pull request abierto más de 90 días cuyo check requerido caduca. No lo encontré documentado. Si tienes PRs de larga vida con protección de rama, revísalos después del 1 de octubre antes de dar nada por hecho.&lt;/p&gt;
&lt;h2 id=&quot;5-imágenes-macos-14-sí-tiene-fecha-ubuntu-2204-todavía-no&quot;&gt;5. Imágenes: macos-14 sí tiene fecha, ubuntu-22.04 todavía no&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;macos-14&lt;/code&gt;&lt;/strong&gt; (&lt;a href=&quot;https://github.com/actions/runner-images/issues/13518&quot;&gt;actions/runner-images#13518&lt;/a&gt;): la deprecación empezó el 6 de julio y el soporte termina el &lt;strong&gt;2 de noviembre de 2026&lt;/strong&gt;. Antes hay ocho apagones en los que los jobs fallan: 5, 12, 16, 19, 23, 26, 29 y 30 de octubre, cada uno de 14:00 UTC a 00:00 UTC del día siguiente. Afecta a &lt;code&gt;macos-14&lt;/code&gt;, &lt;code&gt;macos-14-large&lt;/code&gt; y &lt;code&gt;macos-14-xlarge&lt;/code&gt;. El reemplazo es &lt;code&gt;macos-15&lt;/code&gt;, &lt;code&gt;macos-26&lt;/code&gt; o &lt;code&gt;macos-latest&lt;/code&gt;, que con la migración iniciada el 15 de junio &lt;a href=&quot;https://github.blog/changelog/2026-05-14-github-actions-upcoming-image-migrations&quot;&gt;pasó a apuntar a macOS 26&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ubuntu-22.04&lt;/code&gt;&lt;/strong&gt; (&lt;a href=&quot;https://github.com/actions/runner-images/issues/14254&quot;&gt;actions/runner-images#14254&lt;/a&gt;): el 17 de septiembre &lt;strong&gt;solo empieza la deprecación&lt;/strong&gt;. El issue avisa de colas más largas en horas pico, no de fallos. El retiro es el &lt;strong&gt;17 de abril de 2027&lt;/strong&gt;, con apagones previos que el issue fecha en marzo y abril (sin año, pero caen antes del retiro). Afecta también a &lt;code&gt;ubuntu-22.04-arm&lt;/code&gt;. El reemplazo es &lt;code&gt;ubuntu-24.04&lt;/code&gt;, &lt;code&gt;ubuntu-26.04&lt;/code&gt; o &lt;code&gt;ubuntu-latest&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Si alguien te dice que &lt;code&gt;ubuntu-22.04&lt;/code&gt; se rompe esta semana, no es cierto. Migra con calma, pero no lo dejes para marzo.&lt;/p&gt;
&lt;h2 id=&quot;lo-que-sale-en-el-changelog-y-no-rompe-nada&quot;&gt;Lo que sale en el changelog y no rompe nada&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cache-mode&lt;/code&gt;&lt;/strong&gt; (&lt;a href=&quot;https://github.blog/changelog/2026-09-10-control-github-actions-cache-access-with-cache-mode&quot;&gt;10 de septiembre&lt;/a&gt;): una clave nueva para limitar el acceso a la caché por workflow o job. Es opcional: los workflows que no la declaran «siguen usando los valores seguros existentes».&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;La ruta separada de Code Quality&lt;/strong&gt; (&lt;a href=&quot;https://github.blog/changelog/2026-08-20-separate-github-actions-path-for-github-code-quality&quot;&gt;20 de agosto&lt;/a&gt;): solo te afecta si tienes reportes que filtran por &lt;code&gt;dynamic/github-code-scanning/codeql&lt;/code&gt; o por el actor &lt;code&gt;github-advanced-security&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;actions/checkout&lt;/code&gt; y &lt;code&gt;pull_request_target&lt;/code&gt;&lt;/strong&gt; (&lt;a href=&quot;https://github.blog/changelog/2026-06-18-safer-pull_request_target-defaults-for-github-actions-checkout&quot;&gt;18 de junio&lt;/a&gt;): esto sí rompe, pero ya pasó. Desde el 20 de julio las etiquetas flotantes, &lt;code&gt;@v4&lt;/code&gt; incluida, rechazan hacer checkout del código de un fork en &lt;code&gt;pull_request_target&lt;/code&gt;. Si tu workflow dependía de eso, ya está en rojo. Es la misma familia de riesgo que conté en &lt;a href=&quot;https://ortamarco.me/blog/npm-trusted-publishing-no-basta/&quot;&gt;el gusano que se coló por el trusted publishing de npm&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;qué-hago-con-mi-ci&quot;&gt;Qué hago con mi CI&lt;/h2&gt;
&lt;p&gt;Nada urgente. &lt;code&gt;actions/checkout@v4&lt;/code&gt; y &lt;code&gt;actions/setup-node@v4&lt;/code&gt; corren en Node 24 desde junio, el workflow no usa la variable de exclusión, corre en &lt;code&gt;ubuntu-latest&lt;/code&gt; (hoy &lt;code&gt;ubuntu-24.04&lt;/code&gt;) y en runners de GitHub. El 23 de septiembre no le cambia nada.&lt;/p&gt;
&lt;p&gt;Sí voy a subir las dos actions a su major actual, por dos razones que no tienen que ver con la fecha: la anotación amarilla entrena a ignorar las anotaciones, y una action que su autor ya no prueba en el runtime donde de verdad se ejecuta es deuda. Si además estás pensando en el Node de tu propio proyecto, no el de las actions, lo tengo contado en &lt;a href=&quot;https://ortamarco.me/blog/node-26-lts-que-se-rompe/&quot;&gt;qué se rompe al actualizar a Node 26&lt;/a&gt;.&lt;/p&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué pasa con GitHub Actions el 23 de septiembre de 2026? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; GitHub quita Node 20 de los runners. Desde el 16 de junio de 2026 los runners ya ejecutaban las actions con Node 24 por defecto, y la variable ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true permitía volver a Node 20. A partir del 23 de septiembre esa variable deja de funcionar y todas las actions de JavaScript corren en Node 24. Los runners self-hosted en Linux ARM32 pierden soporte, porque Node 24 no tiene soporte oficial para esa plataforma. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Falla una action que declara using: node20 después del 23 de septiembre? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No por declararlo. El código del runner (HandlerFactory.cs y NodeUtil.cs en actions/runner) reescribe node12 y node16 a node20, y node20 a node24; en la fase final ignora las variables de entorno y siempre elige Node 24, sin devolver error. La action falla si su código usa algo que Node 24 eliminó, como tls.createSecurePair o fs.truncate con un descriptor, o si corre en un runner self-hosted Linux ARM32 o con macOS 13.4 o anterior. En los logs aparece la anotación: The following actions target Node.js 20 but are being forced to run on Node.js 24. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Tengo que actualizar actions/checkout@v4 y actions/setup-node@v4? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Conviene, pero no hay fecha que lo obligue. Las etiquetas v4 de las dos declaran node20 y las v5 en adelante declaran node24. En runners de GitHub, las v4 ya corren en Node 24 desde junio y siguen funcionando. Al subir setup-node, revisa las notas de versión: la v5 activó caché automática cuando package.json tiene packageManager y la v6 la limitó a npm. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cómo encuentro en mi organización los workflows afectados? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Con la búsqueda de código de GitHub: gh search code --owner TU_ORG con ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION, con ubuntu-22.04 path:.github/workflows, con macos-14 path:.github/workflows, y con node20 --filename action.yml para tus actions propias. La API solo busca en la rama por defecto y permite 10 peticiones por minuto. Las etiquetas que llegan por variables o expresiones no aparecen en ninguna búsqueda: para esas, revisa la línea Image: del paso Set up job en el log. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Se rompe ubuntu-22.04 el 17 de septiembre de 2026? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No. El 17 de septiembre solo empieza la deprecación y el aviso habla de colas más largas en horas pico. Los jobs empiezan a fallar en los apagones que el issue 14254 de actions/runner-images programa para marzo y abril, antes de que la imagen se retire el 17 de abril de 2027. La que sí tiene fecha este otoño es macos-14: apagones en octubre y retiro el 2 de noviembre de 2026. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué tienen que hacer los runners self-hosted antes del 25 de septiembre? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Actualizar el runner. Desde el 25 de septiembre de 2026 GitHub exige la versión 2.329.0 para registrar un runner y que se instale cada versión nueva en menos de 30 días para seguir recibiendo jobs. Hay apagones previos el 14, 16 y 18 de septiembre, de 11:00 a 15:00 hora del Este. El endpoint GET /repos/OWNER/REPO/actions/runners/deprecations/VERSION devuelve cuándo deja de ejecutar tu versión. Rehaz también las imágenes con las que creas runners. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué cambia con la retención de GitHub Actions el 1 de octubre de 2026? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Los checks, las ejecuciones de workflows y los statuses pasan a seguir el ajuste de retención de artefactos y logs, que por defecto es de 90 días; antes se guardaban más de 400 días. En repositorios públicos el máximo es 90 días y el cambio no es retroactivo. Si tienes métricas o auditorías que leen ejecuciones antiguas, exporta los datos antes de esa fecha. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Desarrollo Web</category><category>GitHub Actions</category><category>CI/CD</category><category>DevOps</category><category>Node.js</category><category>Migración</category><category>Upgrade</category><author>Marco Orta</author></item><item><title>Portainer 3.0: qué cambia si administras Docker (y a qué alternativa pasarte)</title><link>https://ortamarco.me/blog/portainer-3-alternativas-docker/</link><guid isPermaLink="true">https://ortamarco.me/blog/portainer-3-alternativas-docker/</guid><description>Portainer 3 es Kubernetes-first y sin CE: lo gratis es una licencia de 3 nodos que caduca. Qué se rompe y cuándo elegir Coolify, Komodo, Dockge o Dockhand.</description><pubDate>Sun, 13 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Portainer 3.0 no rompe tu servidor Docker a fin de mes. Lo que se acaba es otra cosa: no habrá Community Edition de la rama 3.x.&lt;/strong&gt; Lo escribió el propio CEO, Neil Cresswell, en el &lt;a href=&quot;https://www.portainer.io/blog/portainer-3-0-is-coming&quot;&gt;anuncio oficial del 11 de septiembre de 2026&lt;/a&gt;: CE “seguirá en la base de código 2.x” y “no recibirá los cambios de 3.x”. La 3.x será gratis para la comunidad solo mediante el programa &lt;strong&gt;3 Nodes Free&lt;/strong&gt;, que es una licencia de Business Edition. Y 3.x es, en sus palabras, una base de código “Kubernetes-first”.&lt;/p&gt;
&lt;p&gt;Si administras Docker —no Kubernetes— con Portainer CE, la decisión no corre prisa, pero hay que tomarla: quedarte en 2.x mientras dure, pedir la licencia gratuita de tres nodos y aceptar un producto pensado para Kubernetes, o salirte. Aquí van los hechos verificados en fuente primaria, qué se rompe en cada camino y qué elegir según tu caso. Yo corro Coolify en producción en dos VPS, así que al final también cuento lo que me ha mordido.&lt;/p&gt;
&lt;h2 id=&quot;lo-que-dice-el-anuncio-sin-adornos&quot;&gt;Lo que dice el anuncio, sin adornos&lt;/h2&gt;









































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Tema&lt;/th&gt;&lt;th&gt;Lo que publicó Portainer&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Última 2.x&lt;/td&gt;&lt;td&gt;&lt;strong&gt;2.45 LTS&lt;/strong&gt; (release &lt;code&gt;2.45.0&lt;/code&gt; en GitHub, 27 de agosto de 2026) es la última versión de la rama 2.x&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;3.0&lt;/td&gt;&lt;td&gt;Sale como &lt;strong&gt;3.0.0 STS “desde finales de este mes”&lt;/strong&gt;; la siguiente LTS será la &lt;strong&gt;3.3.0, en diciembre&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Enfoque&lt;/td&gt;&lt;td&gt;3.x es Kubernetes-first: mantener Docker/Podman, Swarm y Kubernetes a la par en una sola base de código “ya no es viable”&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Entornos Docker, Swarm y Podman nativos&lt;/td&gt;&lt;td&gt;Se pueden seguir agregando y “siguen funcionando”, pero quedan en segundo plano en la interfaz y &lt;strong&gt;no recibirán capacidades nuevas&lt;/strong&gt; del motor de políticas, de GitOps ni de observabilidad&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Productos nuevos&lt;/td&gt;&lt;td&gt;Portainer-Run, IDP, Command y AiGrid son &lt;strong&gt;solo para Kubernetes&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Si te quedas en 2.x&lt;/td&gt;&lt;td&gt;“No cambia nada”: siguen las actualizaciones de seguridad, la corrección de bugs y &lt;em&gt;back-ports&lt;/em&gt; selectivos de 3.x&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Community Edition&lt;/td&gt;&lt;td&gt;Sigue en 2.x. &lt;strong&gt;No habrá build CE de 3.x&lt;/strong&gt;; la 3.x es gratis vía &lt;strong&gt;3 Nodes Free&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Recomendación oficial&lt;/td&gt;&lt;td&gt;Migrar de Docker a &lt;strong&gt;D2K&lt;/strong&gt; (un Docker sintético sobre Kubernetes) o a Kubernetes nativo; KubeSolo para un solo nodo&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;h3 id=&quot;lo-de-reddit-desaparece-ce-y-solo-queda-business-gratis-hasta-3-nodos&quot;&gt;Lo de Reddit: “desaparece CE y solo queda Business gratis hasta 3 nodos”&lt;/h3&gt;
&lt;p&gt;Esa lectura circuló en r/selfhosted. &lt;strong&gt;Es correcta para la rama 3.x y se queda corta en todo lo demás.&lt;/strong&gt; Esto es lo que confirma la fuente primaria:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cierto:&lt;/strong&gt; no habrá edición CE de 3.x, y la forma gratuita de usar 3.x es 3 Nodes Free. La &lt;a href=&quot;https://www.portainer.io/take-3&quot;&gt;página del programa&lt;/a&gt; dice que es una &lt;strong&gt;licencia de Business Edition&lt;/strong&gt;, que la mandan tras llenar un formulario y que está “limitada a una licencia por organización”.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Incompleto:&lt;/strong&gt; CE no desaparece mañana. Sigue en la rama 2.x, el repositorio &lt;a href=&quot;https://github.com/portainer/portainer&quot;&gt;&lt;code&gt;portainer/portainer&lt;/code&gt;&lt;/a&gt; mantiene su licencia zlib y la &lt;a href=&quot;https://docs.portainer.io/start/lifecycle&quot;&gt;política de ciclo de vida&lt;/a&gt; da a la 2.45 LTS mantenimiento &lt;strong&gt;hasta mayo de 2027&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El matiz que casi nadie menciona:&lt;/strong&gt; la licencia de tres nodos &lt;strong&gt;caduca y hay que renovarla&lt;/strong&gt;. La &lt;a href=&quot;https://docs.portainer.io/faqs/licensing/how-do-i-renew-my-3-nodes-free-license&quot;&gt;documentación&lt;/a&gt; dice que la llave nueva no llega hasta 14 días antes de que venza la actual. CE no pedía formulario ni renovación.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Qué cuenta como nodo:&lt;/strong&gt; según la &lt;a href=&quot;https://docs.portainer.io/faqs/licensing/what-is-a-node-for-licensing-purposes&quot;&gt;FAQ de licencias&lt;/a&gt;, en Docker cuenta cualquier servidor que ejecute Portainer Server o el Agent. Tres hosts Docker (uno con el server y dos con agente) son tres nodos: ya agotaste la cuota gratuita.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Un aviso sobre esa fecha de mayo de 2027: la misma página de ciclo de vida todavía anuncia una 2.46 STS para septiembre y una 2.51 LTS para febrero de 2027, algo que el anuncio contradice, porque la 2.45 es la última 2.x. La tabla no se ha actualizado desde el anuncio, así que toma mayo de 2027 como la fecha publicada, no como una garantía. El anuncio no le pone fecha de fin a la rama 2.x.&lt;/p&gt;
&lt;h2 id=&quot;qué-se-rompe-y-qué-no-en-cada-camino&quot;&gt;Qué se rompe (y qué no) en cada camino&lt;/h2&gt;
&lt;h3 id=&quot;camino-1-te-quedas-en-portainer-ce-245&quot;&gt;Camino 1: te quedas en Portainer CE 2.45&lt;/h3&gt;
&lt;p&gt;Hoy no se rompe nada. Pero hay dos cosas que conviene hacer esta semana.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fija la versión exacta de la imagen.&lt;/strong&gt; En Docker Hub, las etiquetas &lt;code&gt;lts&lt;/code&gt;, &lt;code&gt;sts&lt;/code&gt; y &lt;code&gt;latest&lt;/code&gt; de &lt;code&gt;portainer/portainer-ce&lt;/code&gt; se actualizaron por última vez el 27 de agosto, con la 2.45.0. No encontré documentado a qué van a apuntar cuando salga la 3.0 STS, así que si usas Watchtower o algo parecido, no dejes que una etiqueta flotante decida por ti:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; data-language=&quot;yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;portainer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; portainer/portainer&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ce&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;2.45.0   &lt;span class=&quot;token comment&quot;&gt;# no :latest ni :sts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Asegúrate de estar en 2.45.0 (o en 2.39.7 si sigues en la LTS anterior).&lt;/strong&gt; Las &lt;a href=&quot;https://github.com/portainer/portainer/releases/tag/2.45.0&quot;&gt;notas de la 2.45.0&lt;/a&gt; corrigen un &lt;em&gt;bypass&lt;/em&gt; crítico de autorización en el proxy de Docker: los prefijos de versión de API no reconocidos, como &lt;code&gt;/v1.47.0/&lt;/code&gt;, se saltaban el control de acceso y dejaban a usuarios sin permisos de administrador hablar directo con la API de Docker. La 2.39.7 LTS recibió el mismo arreglo el mismo día.&lt;/p&gt;
&lt;p&gt;Para ver qué imagen estás corriendo:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ps&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--filter&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;portainer &lt;span class=&quot;token parameter variable&quot;&gt;--format&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;{{.Image}}&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si te quedas, pierdes las funciones nuevas y ganas tiempo para decidir.&lt;/p&gt;
&lt;h3 id=&quot;camino-2-pasas-a-3x-con-la-licencia-de-3-nodos&quot;&gt;Camino 2: pasas a 3.x con la licencia de 3 nodos&lt;/h3&gt;
&lt;p&gt;Tus entornos Docker nativos siguen funcionando; en eso el anuncio es explícito. Lo que cambia son las condiciones: una licencia Business que caduca, un tope de tres nodos, un producto cuya hoja de ruta ya no incluye a Docker y una 3.0 que sale como STS (la LTS llega en diciembre con la 3.3.0). Si administras más de tres hosts, este camino es de pago.&lt;/p&gt;
&lt;p&gt;Tiene un lado bueno si tu Portainer lo usan varias personas: el control de acceso por roles (RBAC) es de Business Edition, según la &lt;a href=&quot;https://docs.portainer.io/admin/user/roles&quot;&gt;documentación de roles&lt;/a&gt;, así que con 3 Nodes Free lo tendrías.&lt;/p&gt;
&lt;h3 id=&quot;camino-3-sigues-la-recomendación-de-portainer-y-migras-a-d2k&quot;&gt;Camino 3: sigues la recomendación de Portainer y migras a D2K&lt;/h3&gt;
&lt;p&gt;Aquí sí se rompen cosas, y no por un bug, sino por diseño. &lt;a href=&quot;https://github.com/portainer/d2k&quot;&gt;D2K&lt;/a&gt; (licencia MIT) expone la API de Docker y traduce cada llamada a objetos de Kubernetes dentro de un namespace. Su propio README enumera lo que no soporta y aclara que “no se añadirá”:&lt;/p&gt;









































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Lo que usas hoy en Docker&lt;/th&gt;&lt;th&gt;Qué pasa en D2K&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;build:&lt;/code&gt; en tu compose&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Se ignora&lt;/strong&gt;: las imágenes tienen que llegar ya construidas desde un registro&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;docker build&lt;/code&gt;, &lt;code&gt;docker load&lt;/code&gt;, &lt;code&gt;docker save&lt;/code&gt;, &lt;code&gt;buildx&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;No soportados&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Redes &lt;code&gt;macvlan&lt;/code&gt; / &lt;code&gt;ipvlan&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;No soportadas&lt;/strong&gt;: esas cargas “deben quedarse en un host Docker nativo”&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--network host&lt;/code&gt;, &lt;code&gt;--ip&lt;/code&gt;, &lt;code&gt;--mac-address&lt;/code&gt;, &lt;code&gt;--link&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Sin equivalente&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Redes separadas por stack&lt;/td&gt;&lt;td&gt;&lt;code&gt;docker network create&lt;/code&gt; es sintético: &lt;strong&gt;no se aplica aislamiento de red&lt;/strong&gt; y todos los pods comparten la red del namespace&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;Bind mounts&lt;/em&gt; como &lt;code&gt;./data:/app/data&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Se convierten en &lt;code&gt;hostPath&lt;/code&gt;, “poco fiables” en clústeres de varios nodos&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--device&lt;/code&gt; (por ejemplo &lt;code&gt;/dev/video0&lt;/code&gt;), &lt;code&gt;--ulimit&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Sin traducción&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;docker stats&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Necesita metrics-server para devolver datos reales&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Si tu compose construye la imagen en el mismo servidor, si aíslas la base de datos en una red interna o si tienes algo con &lt;code&gt;network_mode: host&lt;/code&gt;, D2K no es una migración: es un rediseño. Y debajo sigue habiendo un Kubernetes que operar.&lt;/p&gt;
&lt;h2 id=&quot;las-alternativas-verificadas&quot;&gt;Las alternativas, verificadas&lt;/h2&gt;
&lt;p&gt;Revisé el repositorio, la licencia y el último release de cada una el 13 de septiembre de 2026.&lt;/p&gt;
&lt;h3 id=&quot;coolify-si-lo-que-haces-con-portainer-es-desplegar-tus-propias-apps&quot;&gt;Coolify: si lo que haces con Portainer es desplegar tus propias apps&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Licencia:&lt;/strong&gt; Apache-2.0. Unas 61 700 estrellas en GitHub.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Madurez:&lt;/strong&gt; la v4.0.0 estable salió el 27 de abril de 2026 y el último release es la &lt;strong&gt;v4.3.19, del 10 de septiembre&lt;/strong&gt;. Entre el 3 y el 10 de septiembre publicó cuatro versiones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Qué es:&lt;/strong&gt; un PaaS self-hosted, no una UI de contenedores. Despliega desde git (Nixpacks, Railpack, Dockerfile, Docker Compose o sitio estático), pone el proxy inverso (Traefik o Caddy) con certificados, programa respaldos de bases de datos, trae más de 280 servicios de un clic y administra varios servidores por SSH.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Usuarios:&lt;/strong&gt; equipos, 2FA y OAuth con Azure, Bitbucket, Clerk, Discord, GitHub, GitLab, Google, Authentik, Infomaniak y Zitadel (la lista sale del código fuente).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Requisitos:&lt;/strong&gt; 2 núcleos, 2 GB de RAM y 10 GB de disco, según su &lt;a href=&quot;https://coolify.io/docs/get-started/installation&quot;&gt;documentación&lt;/a&gt;, que además recomienda un servidor limpio para evitar conflictos con lo que ya corre.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Qué no es:&lt;/strong&gt; un panel para contenedores que levantaste a mano. Su modelo es que el despliegue lo haga él.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Swarm:&lt;/strong&gt; &lt;strong&gt;deprecado.&lt;/strong&gt; El aviso del propio código dice que se elimina en Coolify v5 y que no recomiendan montar despliegues Swarm nuevos.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;komodo-el-reemplazo-más-directo-de-portainer-para-varios-servidores&quot;&gt;Komodo: el reemplazo más directo de Portainer para varios servidores&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Licencia:&lt;/strong&gt; GPL-3.0. Unas 12 200 estrellas. Último release: &lt;strong&gt;v2.3.3, del 1 de septiembre de 2026&lt;/strong&gt;; la v2.0.0 salió el 24 de marzo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Qué hace:&lt;/strong&gt; conecta servidores (con un agente llamado Periphery en cada uno), despliega contenedores y &lt;strong&gt;stacks de Compose definidos en la UI, en el host o en un repo de git, con redespliegue automático por webhook&lt;/strong&gt;. También construye imágenes, corre automatizaciones programadas y registra quién hizo cada cambio y cuándo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Swarm:&lt;/strong&gt; soportado desde la v2.0.0 (clústeres, nodos, servicios, stacks, configs y secrets).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Usuarios:&lt;/strong&gt; usuario y contraseña con 2FA, OAuth con GitHub, Google y OIDC genérico, y permisos granulares por roles.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Límite de servidores:&lt;/strong&gt; ninguno, “y nunca lo habrá”, según su README.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Requisitos:&lt;/strong&gt; Docker, más &lt;strong&gt;MongoDB o FerretDB&lt;/strong&gt; (sobre Postgres) para el Core. De esta lista, es la que más infraestructura te pide, y su documentación da por hecho que el proxy inverso lo pones tú.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;dockge-si-solo-quieres-editar-tus-composeyaml-desde-el-navegador&quot;&gt;Dockge: si solo quieres editar tus &lt;code&gt;compose.yaml&lt;/code&gt; desde el navegador&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Licencia:&lt;/strong&gt; MIT. Unas 24 300 estrellas. Es del mismo autor que Uptime Kuma.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Madurez:&lt;/strong&gt; último release &lt;strong&gt;1.5.0, del 30 de marzo de 2025&lt;/strong&gt;; el último commit en &lt;code&gt;master&lt;/code&gt; es de abril de 2026. El mantenimiento va lento.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Qué hace:&lt;/strong&gt; crea, edita, arranca y actualiza stacks de Compose, con terminal web, y convierte comandos &lt;code&gt;docker run&lt;/code&gt; a compose. Tus archivos se quedan en disco (&lt;code&gt;/opt/stacks&lt;/code&gt; por defecto) y los puedes seguir usando con &lt;code&gt;docker compose&lt;/code&gt;. Desde la 1.4.0 maneja varios hosts mediante agentes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Qué no hace:&lt;/strong&gt; &lt;strong&gt;tiene una sola cuenta.&lt;/strong&gt; El setup se niega a crear un segundo usuario, y el PR que añadía usuarios y grupos (#891) se cerró sin fusionar en julio de 2026. No hay SSO ni roles, y el README no menciona despliegue desde git.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;dockhand-la-ui-más-completa-con-letra-pequeña-en-la-licencia&quot;&gt;Dockhand: la UI más completa, con letra pequeña en la licencia&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Existe y avanza rápido:&lt;/strong&gt; el repositorio &lt;a href=&quot;https://github.com/Finsys/dockhand&quot;&gt;&lt;code&gt;Finsys/dockhand&lt;/code&gt;&lt;/a&gt; se creó en diciembre de 2025, tiene unas 6200 estrellas y su último release es la &lt;strong&gt;v1.0.47, del 12 de septiembre de 2026&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Qué hace:&lt;/strong&gt; contenedores, stacks de Compose con editor visual, &lt;strong&gt;despliegue desde git con webhooks&lt;/strong&gt;, varios hosts (por socket, por TCP con TLS o con Hawser, su agente MIT, que se conecta hacia afuera sin abrir puertos), escaneo de vulnerabilidades con Grype y Trivy, respaldos (en beta) y OIDC/SSO gratis. Usa SQLite por defecto o PostgreSQL, y dicen que corre en una Raspberry Pi 4.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Qué no hace:&lt;/strong&gt; Swarm; hay un &lt;a href=&quot;https://github.com/Finsys/dockhand/issues/58&quot;&gt;issue abierto&lt;/a&gt; pidiéndolo. RBAC, LDAP/Active Directory y la auditoría para cumplimiento son de la edición Enterprise (1499 dólares por host al año).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;La licencia se contradice.&lt;/strong&gt; El &lt;code&gt;LICENSE.txt&lt;/code&gt; del repositorio es la &lt;strong&gt;Business Source License 1.1&lt;/strong&gt; con una concesión adicional que permite explícitamente el “uso interno de negocio” en producción, siempre que no lo ofrezcas como SaaS de gestión de Docker; pasa a Apache-2.0 el 1 de enero de 2029. Pero los &lt;a href=&quot;https://dockhand.pro/license.html&quot;&gt;términos de licencia de su sitio&lt;/a&gt; dicen que un usuario comercial necesita licencia de pago para entornos de producción o instancias compartidas, y su tabla de precios coloca la “licencia de uso comercial” en el plan SMB (499 dólares por host al año). En un homelab no hay duda. Si eres empresa, pídeles la aclaración por escrito antes de meterlo a producción.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;lazydocker-y-yacht-para-completar&quot;&gt;Lazydocker y Yacht, para completar&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/jesseduffield/lazydocker&quot;&gt;Lazydocker&lt;/a&gt;&lt;/strong&gt; (MIT, unas 52 800 estrellas, v0.25.2 de abril de 2026) es una interfaz de terminal, no un servidor web: no hay usuarios, ni agentes, ni nada que exponer. Si usabas Portainer solo para ver logs y reiniciar contenedores, te basta.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/Yacht-sh/Yacht&quot;&gt;Yacht&lt;/a&gt;&lt;/strong&gt; volvió a publicar en septiembre de 2026 (release &lt;code&gt;1.1&lt;/code&gt;) después de años sin releases; el anterior, &lt;code&gt;v0.0.7-alpha&lt;/code&gt;, es de 2021. Es muy pronto para confiarle producción.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;la-tabla-de-decisión&quot;&gt;La tabla de decisión&lt;/h2&gt;























































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Tu caso&lt;/th&gt;&lt;th&gt;Elige&lt;/th&gt;&lt;th&gt;Por qué&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Homelab con 1 a 3 hosts; usas Portainer CE para mirar y reiniciar&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Quédate en CE 2.45.0&lt;/strong&gt; y decide antes de mayo de 2027&lt;/td&gt;&lt;td&gt;Hoy no se rompe nada y tienes margen&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Solo editas &lt;code&gt;compose.yaml&lt;/code&gt; y eres la única persona que entra&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Dockge&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;MIT, mínimo, y tus archivos se quedan en disco&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Quieres una UI tipo Portainer, moderna, para uso personal&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Dockhand&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;La más completa: git, escaneo de vulnerabilidades y SSO gratis&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Empresa con varios servidores, varias personas y stacks en git&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Komodo&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;GPL, sin límite de servidores, roles y OIDC sin pagar licencia&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Usas Docker Swarm&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Komodo&lt;/strong&gt;, o quedarte en Portainer 2.45&lt;/td&gt;&lt;td&gt;Coolify lo deprecó; Dockge y Dockhand no lo soportan&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Despliegas tus propias apps desde git a uno o varios VPS&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Coolify&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Build, proxy, certificados y respaldos incluidos&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Ya pagas Portainer Business, o te bastan 3 nodos con RBAC y vas hacia Kubernetes&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Portainer 3.x&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Es exactamente el usuario para el que está pensado&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Solo abres Portainer para ver logs&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Lazydocker&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Nada que exponer, nada que licenciar&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Tus stacks construyen la imagen en el servidor o usan &lt;code&gt;network_mode: host&lt;/code&gt; o &lt;code&gt;macvlan&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Cualquiera menos D2K&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;D2K ignora &lt;code&gt;build:&lt;/code&gt; y no soporta esas redes&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;h2 id=&quot;lo-que-he-aprendido-corriendo-coolify-en-dos-vps&quot;&gt;Lo que he aprendido corriendo Coolify en dos VPS&lt;/h2&gt;
&lt;p&gt;Tengo Coolify en producción en dos servidores: uno con este sitio y aplicaciones de clientes, y otro con herramientas internas. Hay tres cosas que ninguna comparativa te cuenta, y aplican a cualquier herramienta que hable con el socket de Docker, Portainer, Dockge y Dockhand incluidos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Después de actualizar &lt;code&gt;docker-ce&lt;/code&gt;, reinicia los contenedores que montan el socket.&lt;/strong&gt; En mayo de 2026 actualicé Docker de la 29.5.0 a la 29.5.2 con &lt;code&gt;live-restore&lt;/code&gt; activado. Las apps siguieron vivas, pero el proxy de Coolify (Traefik con el proveedor de Docker) se quedó en un bucle intentando reconectarse a &lt;code&gt;/var/run/docker.sock&lt;/code&gt;. Resultado: &lt;strong&gt;502 en todas las apps&lt;/strong&gt; que enruta a través de Docker, mientras el dashboard, que va por archivo de configuración, seguía respondiendo, y eso despista. &lt;code&gt;live-restore&lt;/code&gt; protege a los contenedores, no las conexiones que tenían abiertas contra el socket. Desde entonces, después de cada actualización corro:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; restart coolify-proxy coolify-sentinel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;O, en general, reinicio todo contenedor que monte el socket:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token for-or-select variable&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ps&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--format&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;{{.Names}}&amp;#39;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; inspect &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;{{range .Mounts}}{{if eq .Source &amp;quot;/var/run/docker.sock&amp;quot;}}{{$.Name}}{{end}}{{end}}&amp;#39;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;&lt;span class=&quot;token variable&quot;&gt;$c&lt;/span&gt;&amp;quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;^$&amp;#39;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;xargs&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n1&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ojo: desde la v4.3.19 Sentinel es obligatorio en los servidores normales de Coolify, así que el reinicio de &lt;code&gt;coolify-sentinel&lt;/code&gt; ya aplica a todas las instalaciones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Docker se salta UFW.&lt;/strong&gt; Cuando instalé Coolify en el segundo VPS, los puertos del dashboard y del servicio de tiempo real quedaron abiertos a internet aunque UFW no los permitía: los puertos que publica un contenedor pasan por reglas de iptables que Docker gestiona antes de que UFW los vea. Lo resolví con reglas &lt;code&gt;DROP&lt;/code&gt; en la cadena &lt;code&gt;DOCKER-USER&lt;/code&gt;, filtrando por el puerto de destino original de la conexión (&lt;code&gt;conntrack --ctorigdstport&lt;/code&gt;, porque cuando el paquete llega a esa cadena el DNAT ya reescribió el puerto), y dejando públicos solo el 80 y el 443. Si instalas cualquiera de estas UIs publicando un puerto, &lt;strong&gt;compruébalo desde fuera del servidor&lt;/strong&gt;; no te fíes de &lt;code&gt;ufw status&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Las reglas de firewall atadas a la IP de un contenedor se rompen al reiniciar.&lt;/strong&gt; En el servidor de producción, tras un reinicio, todos los dominios daban &lt;em&gt;timeout&lt;/em&gt; aunque Traefik estaba sano. Las reglas de &lt;code&gt;ufw-docker&lt;/code&gt; apuntaban a la IP interna vieja del proxy, y el proxy arrancó con otra. El arreglo que aguanta fue permitir el 80 y el 443 hacia el rango de redes de Docker, no hacia una IP concreta. Reiniciar Docker no lo arregla, porque las reglas son de UFW.&lt;/p&gt;
&lt;p&gt;Y una que viene en la propia documentación de Coolify y que confirmo: crea la cuenta de administrador en cuanto termine la instalación. Quien llegue primero a la página de registro se queda con el control del servidor.&lt;/p&gt;
&lt;p&gt;Si vas a copiar tus stacks de Portainer a otra herramienta, valida cada &lt;code&gt;compose.yaml&lt;/code&gt; contra el esquema oficial antes del primer despliegue:&lt;/p&gt;
&lt;aside class=&quot;not-prose my-8 flex flex-wrap items-center gap-4 rounded-2xl border border-neutral-100 bg-white p-5 transition-all duration-200 hover:shadow-[5px_5px_rgba(0,98,90,0.3),10px_10px_rgba(0,98,90,0.2),15px_15px_rgba(0,98,90,0.1)] dark:border-zinc-800 dark:bg-zinc-900/40&quot; style=&quot;border-left:4px solid #3b82f6&quot;&gt; &lt;span class=&quot;grid size-12 shrink-0 place-items-center rounded-xl&quot; style=&quot;background:#3b82f61a;color:#3b82f6&quot;&gt;  &lt;/span&gt; &lt;div class=&quot;flex min-w-0 flex-1 flex-col gap-0.5&quot;&gt; &lt;span class=&quot;text-xs font-semibold tracking-wide text-zinc-500 uppercase dark:text-zinc-400&quot;&gt; Herramienta gratuita &lt;/span&gt; &lt;span class=&quot;text-lg leading-snug font-bold text-blacktext dark:text-mint-50&quot;&gt; Validador de Docker Compose &lt;/span&gt; &lt;span class=&quot;text-sm text-pretty text-zinc-600 dark:text-zinc-400&quot;&gt; Valida tu compose.yaml contra el esquema oficial de Compose y detecta erratas en las claves, tipos equivocados y errores de YAML, con la línea exacta. Todo en tu navegador. &lt;/span&gt; &lt;/div&gt; &lt;a href=&quot;https://ortamarco.me/herramientas/validador-docker-compose/&quot; data-umami-event=&quot;tool_callout_click&quot; data-umami-event-tool=&quot;validador-docker-compose&quot; class=&quot;ml-auto shrink-0 rounded-xl bg-mint-600 px-4 py-2 text-sm! font-semibold text-white! no-underline! transition-colors hover:bg-mint-700 hover:text-white!&quot;&gt; Abrir herramienta → &lt;/a&gt; &lt;/aside&gt;
&lt;h2 id=&quot;veredicto&quot;&gt;Veredicto&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Si usas Portainer CE con Docker, no migres este mes.&lt;/strong&gt; La 2.45 LTS sigue mantenida, la 3.0 sale como STS y la primera LTS de 3.x no llega hasta diciembre. Fija la imagen en &lt;code&gt;2.45.0&lt;/code&gt; y usa los próximos meses para probar la alternativa que te toque en un servidor que no sea el de producción.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pero no esperes una CE 3.x, porque no va a existir.&lt;/strong&gt; El Portainer que conocías —gratis, sin registro, sin tope de nodos y con Docker como prioridad— se queda congelado en 2.x.&lt;/p&gt;
&lt;p&gt;Para elegir, pregúntate qué haces de verdad con Portainer. Si &lt;strong&gt;despliegas tus propias apps&lt;/strong&gt;, el salto natural es Coolify, y es lo que yo uso. Si &lt;strong&gt;administras stacks en varios servidores con un equipo&lt;/strong&gt;, Komodo es lo más parecido a lo que era Portainer, sin tope de nodos y sin licencia comercial. Si &lt;strong&gt;eres tú solo con tu homelab&lt;/strong&gt;, Dockge si te basta lo mínimo, o Dockhand si lo quieres todo. Y si ya ibas camino a Kubernetes, Portainer 3.x está hecho para ti; D2K solo tiene sentido si tus stacks no construyen imágenes ni dependen de redes especiales.&lt;/p&gt;
&lt;p&gt;Para seguir leyendo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/guia-completa-docker-2026/&quot;&gt;Guía completa de Docker en 2026&lt;/a&gt;: Compose v5, Engine 29 y la comparación entre Compose, Swarm y Kubernetes para producción.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/kubernetes-1-37-que-se-rompe/&quot;&gt;Kubernetes 1.37: qué se rompe al actualizar&lt;/a&gt;: si al final te vas a Kubernetes, con D2K o sin él, esto es lo que hoy bloquea una actualización.&lt;/li&gt;
&lt;/ul&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Portainer Community Edition desaparece con la versión 3.0? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No de inmediato, pero no tendrá versión 3.x. Según el anuncio oficial del 11 de septiembre de 2026, CE sigue en la base de código 2.x y no recibirá los cambios de 3.x. Portainer no va a publicar una build CE de 3.x: la forma gratuita de usar 3.x es el programa 3 Nodes Free, que es una licencia de Business Edition. La política de ciclo de vida de Portainer da a la 2.45 LTS mantenimiento hasta mayo de 2027. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué es Portainer 3 Nodes Free y qué limita? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Es una licencia de Portainer Business Edition gratuita para hasta tres nodos, limitada a una licencia por organización y que se pide con un formulario. La licencia caduca y hay que renovarla: la llave nueva se envía hasta 14 días antes del vencimiento. En Docker cuenta como nodo cualquier servidor que ejecute Portainer Server o el Portainer Agent, así que tres hosts Docker ya agotan la cuota. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Mis entornos Docker dejan de funcionar si actualizo a Portainer 3.x? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No. El anuncio dice que los entornos Docker, Swarm y Podman nativos se pueden seguir agregando y siguen funcionando desde la interfaz. Lo que cambia es que quedan en segundo plano y no recibirán capacidades nuevas del motor de políticas, de GitOps ni de observabilidad. Los productos nuevos, Portainer-Run, IDP, Command y AiGrid, son solo para Kubernetes. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Hasta cuándo recibe parches Portainer 2.45 LTS? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; La política de ciclo de vida publicada en la documentación de Portainer marca mayo de 2027 como fin de soporte y mantenimiento de la 2.45 LTS. El anuncio de 3.0 dice que la rama 2.x seguirá recibiendo actualizaciones de seguridad, corrección de bugs y back-ports selectivos, sin dar fecha de fin. Esa tabla de ciclo de vida aún lista versiones 2.46 a 2.51 que el anuncio contradice, así que conviene tomar mayo de 2027 como la fecha publicada y no como garantía. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué es Portainer D2K y qué no soporta? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; D2K es un traductor de Docker a Kubernetes con licencia MIT: expone la API de Docker y convierte cada llamada en objetos de Kubernetes dentro de un namespace. Según su README, ignora la directiva build de Compose, no soporta docker build, load, save ni buildx, no soporta redes macvlan ni ipvlan, no tiene equivalente para network host, IP o MAC fijas, no aplica aislamiento de red entre stacks y convierte los bind mounts en hostPath, que no son fiables en clústeres de varios nodos. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuál es la mejor alternativa gratuita a Portainer para Docker? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Depende de para qué lo usas. Para varios servidores y varias personas, Komodo (GPL-3.0) no tiene límite de servidores e incluye roles y OIDC, aunque necesita MongoDB o FerretDB. Para una sola persona que edita archivos compose, Dockge (MIT) es lo más simple, pero admite una sola cuenta. Para desplegar tus propias apps desde git con proxy y certificados, Coolify (Apache-2.0). Dockhand es la interfaz más completa, pero su licencia comercial es ambigua. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Coolify sirve como reemplazo de Portainer? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Solo si lo que haces con Portainer es desplegar tus propias aplicaciones. Coolify es un PaaS self-hosted: despliega desde git con Nixpacks, Railpack, Dockerfile o Docker Compose, configura Traefik o Caddy con certificados y programa respaldos de bases de datos. No está pensado para administrar contenedores creados a mano, su documentación recomienda un servidor limpio y su soporte de Docker Swarm está deprecado y se eliminará en Coolify v5. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Dockhand es gratis para uso comercial? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No está claro, porque sus dos textos de licencia se contradicen. El LICENSE.txt del repositorio es la Business Source License 1.1 con una concesión que permite el uso interno de negocio en producción, siempre que no se ofrezca como SaaS. Los términos de licencia de dockhand.pro, en cambio, exigen licencia de pago a usuarios comerciales en producción o en instancias compartidas, y la tabla de precios pone la licencia de uso comercial en el plan SMB de 499 dólares por host al año. Para uso personal es gratis sin ambigüedad. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Desarrollo Web</category><category>Docker</category><category>DevOps</category><category>Contenedores</category><category>Infraestructura</category><category>Migración</category><author>Marco Orta</author></item><item><title>WhatsApp API, 1 de octubre: qué cambia en tu código cuando Meta cobra los mensajes de servicio</title><link>https://ortamarco.me/blog/whatsapp-api-cobro-1-octubre-webhook/</link><guid isPermaLink="true">https://ortamarco.me/blog/whatsapp-api-cobro-1-octubre-webhook/</guid><description>Desde el 1-oct Meta cobra el servicio desde el mensaje 1,001 y las utility en ventana. El objeto pricing del webhook, el cupo por número y el método de pago.</description><pubDate>Sun, 13 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;El 1 de octubre de 2026 el webhook de estados de WhatsApp no cambia de forma: cambia de significado. El mismo objeto &lt;code&gt;pricing&lt;/code&gt; que hoy llega con &lt;code&gt;&amp;quot;type&amp;quot;: &amp;quot;free_customer_service&amp;quot;&lt;/code&gt; empieza a llegar con &lt;code&gt;&amp;quot;type&amp;quot;: &amp;quot;regular&amp;quot;&lt;/code&gt; en dos casos que hasta el 30 de septiembre son gratis: los mensajes de servicio a partir del 1,001 del mes en cada número, y las plantillas de utilidad que mandas dentro de la ventana de 24 horas. Si tu código tiene la tarifa de servicio fija en cero, decide con &lt;code&gt;billable&lt;/code&gt; o descarta los estados cuya categoría no reconoce, tu reporte va a decir que no gastaste nada mientras Meta te cobra.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;De precios en español ya se escribió mucho. Este post va de lo otro: qué campos mirar, qué valores trae cada uno según la documentación de Meta, cómo contar el cupo gratis sin inventarte la regla y qué revisar antes del 30 de septiembre. Todas las cifras son de la tarjeta de México en pesos.&lt;/p&gt;
&lt;h2 id=&quot;qué-se-cobra-antes-y-después-del-1-de-octubre&quot;&gt;Qué se cobra antes y después del 1 de octubre&lt;/h2&gt;
&lt;p&gt;Tarifas por mensaje &lt;strong&gt;entregado&lt;/strong&gt; a un número con lada +52, en MXN. Salen de las tarjetas oficiales «effective July 1, 2026» y «effective October 1, 2026», que descargué el 13 de septiembre desde la &lt;a href=&quot;https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing&quot;&gt;página de precios de Meta&lt;/a&gt; (fila &lt;code&gt;Mexico&lt;/code&gt;).&lt;/p&gt;





















































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Mensaje&lt;/th&gt;&lt;th&gt;Hasta el 30-sep&lt;/th&gt;&lt;th&gt;Desde el 1-oct&lt;/th&gt;&lt;th&gt;&lt;code&gt;pricing.type&lt;/code&gt; desde el 1-oct&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Plantilla de marketing&lt;/td&gt;&lt;td&gt;0.5614&lt;/td&gt;&lt;td&gt;&lt;strong&gt;0.7298&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;regular&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Plantilla de utilidad &lt;strong&gt;fuera&lt;/strong&gt; de la ventana&lt;/td&gt;&lt;td&gt;0.1565&lt;/td&gt;&lt;td&gt;0.1565&lt;/td&gt;&lt;td&gt;&lt;code&gt;regular&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Plantilla de utilidad &lt;strong&gt;dentro&lt;/strong&gt; de la ventana de 24 h&lt;/td&gt;&lt;td&gt;gratis&lt;/td&gt;&lt;td&gt;&lt;strong&gt;0.1565, desde la primera&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;regular&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Plantilla de autenticación&lt;/td&gt;&lt;td&gt;0.1565&lt;/td&gt;&lt;td&gt;0.1565&lt;/td&gt;&lt;td&gt;&lt;code&gt;regular&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Servicio (sin plantilla), del 1 al 1,000 del mes por número&lt;/td&gt;&lt;td&gt;gratis&lt;/td&gt;&lt;td&gt;gratis&lt;/td&gt;&lt;td&gt;&lt;code&gt;free_customer_service&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Servicio (sin plantilla), del 1,001 en adelante&lt;/td&gt;&lt;td&gt;gratis&lt;/td&gt;&lt;td&gt;&lt;strong&gt;0.1565&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;regular&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cualquiera dentro de la ventana de punto de entrada gratuito (72 h)&lt;/td&gt;&lt;td&gt;gratis&lt;/td&gt;&lt;td&gt;gratis&lt;/td&gt;&lt;td&gt;&lt;code&gt;free_entry_point&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Tres detalles que cambian cómo lo programas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;El cambio entra a las 00:00 en la zona horaria de tu WABA&lt;/strong&gt;, no en UTC. La página de precios lo dice para todo el paquete de octubre: «Rate updates below apply as of 12am by WhatsApp Business Account (WABA) timezone».&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El cupo de 1,000 es solo de servicio.&lt;/strong&gt; Meta lo describe como «a free monthly tier of 1,000 service messages per business phone number», que no se acumula de un mes a otro. No anuncia ningún cupo para utility: una plantilla de utilidad dentro de la ventana se cobra desde la primera.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El marketing sube casi 30 %&lt;/strong&gt; en México (0.5614 → 0.7298). Es el único cambio de tarifa de la fila; utility, autenticación y servicio quedan en 0.1565. Y los niveles por volumen no te van a salvar: en la tarjeta de niveles de octubre, la utility de México se cobra a precio de lista del mensaje 0 al 1,000,000 del mes, y el servicio no tiene niveles en absoluto según la &lt;a href=&quot;https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing/non-template-messages&quot;&gt;página de mensajes sin plantilla&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;el-payload-real-y-los-campos-que-importan&quot;&gt;El payload real y los campos que importan&lt;/h2&gt;
&lt;p&gt;Así llega un mensaje de servicio entregado después de agotar el cupo del mes. La estructura es la de la &lt;a href=&quot;https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status&quot;&gt;referencia del webhook de estados&lt;/a&gt; y el objeto &lt;code&gt;pricing&lt;/code&gt; es literal el que publica Meta para «Paid service message». Los IDs y teléfonos son los de ejemplo de la propia referencia; el &lt;code&gt;timestamp&lt;/code&gt; es el 14 de octubre de 2026.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&amp;quot;object&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;whatsapp_business_account&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&amp;quot;entry&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&amp;quot;id&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;102290129340398&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&amp;quot;changes&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&amp;quot;messaging_product&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;whatsapp&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&amp;quot;metadata&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;token property&quot;&gt;&amp;quot;display_phone_number&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;15550783881&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;token property&quot;&gt;&amp;quot;phone_number_id&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;106540352242922&amp;quot;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&amp;quot;statuses&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
              &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token property&quot;&gt;&amp;quot;id&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQUFERjg0NDEzNDdFODU3MUMxMAA=&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token property&quot;&gt;&amp;quot;status&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;delivered&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token property&quot;&gt;&amp;quot;timestamp&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;1792002600&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token property&quot;&gt;&amp;quot;recipient_id&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;16505551234&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;token property&quot;&gt;&amp;quot;pricing&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                  &lt;span class=&quot;token property&quot;&gt;&amp;quot;billable&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                  &lt;span class=&quot;token property&quot;&gt;&amp;quot;pricing_model&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;PMP&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                  &lt;span class=&quot;token property&quot;&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;regular&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                  &lt;span class=&quot;token property&quot;&gt;&amp;quot;category&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;service&amp;quot;&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
              &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&amp;quot;field&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;messages&amp;quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El mismo mensaje, en septiembre o dentro del cupo, trae &lt;code&gt;&amp;quot;billable&amp;quot;: false&lt;/code&gt; y &lt;code&gt;&amp;quot;type&amp;quot;: &amp;quot;free_customer_service&amp;quot;&lt;/code&gt;. Una utility dentro de la ventana pasa exactamente igual: &lt;code&gt;free_customer_service&lt;/code&gt; hasta el 30 de septiembre, &lt;code&gt;regular&lt;/code&gt; desde el 1 de octubre, con &lt;code&gt;&amp;quot;category&amp;quot;: &amp;quot;utility&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;pricingtype-el-que-decide&quot;&gt;&lt;code&gt;pricing.type&lt;/code&gt;: el que decide&lt;/h3&gt;
&lt;p&gt;Es el campo que dice &lt;strong&gt;por qué&lt;/strong&gt; un mensaje se cobra o no. La referencia de estados documenta tres valores:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;regular&lt;/code&gt;: se cobra.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;free_customer_service&lt;/code&gt;: gratis por ir dentro de la ventana de atención. Desde octubre, eso incluye estar dentro del cupo de 1,000.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;free_entry_point&lt;/code&gt;: gratis por la ventana de 72 horas que abren los anuncios Click-to-WhatsApp y los botones de llamada a la acción de una página de Facebook.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La página de precios añade un cuarto que la referencia (actualizada el 21 de mayo) todavía no lista: &lt;code&gt;free_group_customer_service&lt;/code&gt;, para mensajes a grupos.&lt;/p&gt;
&lt;h3 id=&quot;pricingcategory-qué-tarifa-se-aplicó&quot;&gt;&lt;code&gt;pricing.category&lt;/code&gt;: qué tarifa se aplicó&lt;/h3&gt;
&lt;p&gt;Valores de la referencia: &lt;code&gt;authentication&lt;/code&gt;, &lt;code&gt;authentication-international&lt;/code&gt;, &lt;code&gt;marketing&lt;/code&gt;, &lt;code&gt;marketing_lite&lt;/code&gt;, &lt;code&gt;referral_conversion&lt;/code&gt;, &lt;code&gt;service&lt;/code&gt; y &lt;code&gt;utility&lt;/code&gt;. Ojo con &lt;code&gt;authentication-international&lt;/code&gt;: en &lt;code&gt;pricing.category&lt;/code&gt; lleva &lt;strong&gt;guion&lt;/strong&gt;, mientras que la categoría de &lt;code&gt;conversation.origin.type&lt;/code&gt; lleva guion bajo. La página de precios añade &lt;code&gt;group_service&lt;/code&gt; y &lt;code&gt;group_utility&lt;/code&gt; para grupos.&lt;/p&gt;
&lt;p&gt;La consecuencia práctica: &lt;strong&gt;no valides estos campos contra una lista cerrada&lt;/strong&gt;. Si tu parser descarta un estado porque la categoría no está en tu enum, ese mensaje se queda sin precio en tu base mientras Meta sí lo cobra.&lt;/p&gt;
&lt;h3 id=&quot;pricingbillable-no-decidas-con-él&quot;&gt;&lt;code&gt;pricing.billable&lt;/code&gt;: no decidas con él&lt;/h3&gt;
&lt;p&gt;La referencia es explícita: «The billable property will be deprecated in a future versioned release. Use pricing.type and pricing.category together to determine whether a message is billable and, if so, its billing rate.» Hoy &lt;code&gt;billable&lt;/code&gt; y &lt;code&gt;type&lt;/code&gt; coinciden. El día que Meta lo quite en una versión nueva de la Graph API, un &lt;code&gt;if (billable === false) return 0&lt;/code&gt; se convierte en «todo cuesta» o en «nada cuesta», según cómo trates el &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;cuándo-llega-el-objeto-y-cuándo-cuenta&quot;&gt;Cuándo llega el objeto, y cuándo cuenta&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Según la sintaxis de la referencia, &lt;code&gt;pricing&lt;/code&gt; viene «only included with sent status, and one of either delivered or read status». El ejemplo de la v24 aclara que puede venir solo en el &lt;code&gt;delivered&lt;/code&gt; y no en el &lt;code&gt;sent&lt;/code&gt;. Así que te va a llegar &lt;strong&gt;dos veces o una&lt;/strong&gt;, y tienes que deduplicar por &lt;code&gt;wamid&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Meta cobra lo &lt;strong&gt;entregado&lt;/strong&gt;, no lo enviado («only when the message is delivered (vs. sent)»). Y el &lt;code&gt;delivered&lt;/code&gt; puede no llegar nunca: si el usuario tiene el chat abierto, Meta manda directo el &lt;code&gt;read&lt;/code&gt;. Cuenta un mensaje como entregado cuando llega &lt;code&gt;delivered&lt;/code&gt; &lt;strong&gt;o&lt;/strong&gt; &lt;code&gt;read&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Desde la v24.0 el objeto &lt;code&gt;conversation&lt;/code&gt; ya no viene, salvo dentro de una ventana de punto de entrada gratuito. Si tu código sacaba la categoría de &lt;code&gt;conversation.origin.type&lt;/code&gt;, ya iba tarde.&lt;/li&gt;
&lt;/ul&gt;
&lt;aside class=&quot;not-prose my-8 flex flex-wrap items-center gap-4 rounded-2xl border border-neutral-100 bg-white p-5 transition-all duration-200 hover:shadow-[5px_5px_rgba(0,98,90,0.3),10px_10px_rgba(0,98,90,0.2),15px_15px_rgba(0,98,90,0.1)] dark:border-zinc-800 dark:bg-zinc-900/40&quot; style=&quot;border-left:4px solid #3b82f6&quot;&gt; &lt;span class=&quot;grid size-12 shrink-0 place-items-center rounded-xl&quot; style=&quot;background:#3b82f61a;color:#3b82f6&quot;&gt;  &lt;/span&gt; &lt;div class=&quot;flex min-w-0 flex-1 flex-col gap-0.5&quot;&gt; &lt;span class=&quot;text-xs font-semibold tracking-wide text-zinc-500 uppercase dark:text-zinc-400&quot;&gt; Herramienta gratuita &lt;/span&gt; &lt;span class=&quot;text-lg leading-snug font-bold text-blacktext dark:text-mint-50&quot;&gt; Convertir JSON a TypeScript y Código &lt;/span&gt; &lt;span class=&quot;text-sm text-pretty text-zinc-600 dark:text-zinc-400&quot;&gt; Convierte cualquier JSON en interfaces de TypeScript, esquemas de Zod, clases de PHP, dataclasses de Python o structs de Go. Detecta tipos anidados y opcionales. Todo en tu navegador. &lt;/span&gt; &lt;/div&gt; &lt;a href=&quot;https://ortamarco.me/herramientas/json-a-typescript/&quot; data-umami-event=&quot;tool_callout_click&quot; data-umami-event-tool=&quot;json-a-typescript&quot; class=&quot;ml-auto shrink-0 rounded-xl bg-mint-600 px-4 py-2 text-sm! font-semibold text-white! no-underline! transition-colors hover:bg-mint-700 hover:text-white!&quot;&gt; Abrir herramienta → &lt;/a&gt; &lt;/aside&gt;
&lt;h2 id=&quot;los-cuatro-errores-que-hacen-que-tu-reporte-diga-cero&quot;&gt;Los cuatro errores que hacen que tu reporte diga cero&lt;/h2&gt;
&lt;p&gt;Revisando para este post el agente de WhatsApp que tengo en desarrollo, encontré dos de estos en mi propio código. Los cuatro son fáciles de tener:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Tarifa sin fecha de vigencia.&lt;/strong&gt; Una constante &lt;code&gt;service: 0&lt;/code&gt; calcula bien hasta el 30 de septiembre y mal desde el 1 de octubre, aunque el webhook ya diga &lt;code&gt;regular&lt;/code&gt;. Meta solo puede cambiar tarifas el día 1 de cada trimestre (enero, abril, julio y octubre), con un mes de aviso para una actualización de tarjeta: modela la tarjeta con su fecha de entrada en vigor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decidir con &lt;code&gt;billable&lt;/code&gt;.&lt;/strong&gt; Funciona hoy; deja de funcionar cuando Meta lo retire.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lista blanca de categorías.&lt;/strong&gt; &lt;code&gt;group_service&lt;/code&gt;, &lt;code&gt;free_group_customer_service&lt;/code&gt; o el guion de &lt;code&gt;authentication-international&lt;/code&gt; acaban como «sin categoría» y valen cero en tu suma.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contar cada estado.&lt;/strong&gt; Si sumas en cada webhook con &lt;code&gt;pricing&lt;/code&gt;, un mismo mensaje cuenta doble (&lt;code&gt;sent&lt;/code&gt; + &lt;code&gt;delivered&lt;/code&gt;), y un mensaje que falló después de enviarse cuenta aunque Meta no lo cobre.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;código-contar-lo-facturable-por-número-y-por-mes&quot;&gt;Código: contar lo facturable por número y por mes&lt;/h2&gt;
&lt;p&gt;La idea es la del libro de costos de mi agente: una fila por mensaje saliente, que se va rellenando con lo que traiga cada estado, y el costo se calcula al leer. Está escrito para Cloudflare D1 (SQLite), pero el SQL es portable.&lt;/p&gt;
&lt;p&gt;Primero la tabla. &lt;code&gt;category&lt;/code&gt; y &lt;code&gt;pricing_type&lt;/code&gt; se guardan &lt;strong&gt;tal cual llegan&lt;/strong&gt;, sin lista blanca:&lt;/p&gt;
&lt;pre class=&quot;language-sql&quot; data-language=&quot;sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; wa_outbound &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  wamid           &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  phone_number_id &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;month&lt;/span&gt;           &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;              &lt;span class=&quot;token comment&quot;&gt;-- &amp;#39;YYYY-MM&amp;#39; en la zona horaria de la WABA&lt;/span&gt;
  delivered       &lt;span class=&quot;token keyword&quot;&gt;INTEGER&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- 1 cuando llegó `delivered` o `read`&lt;/span&gt;
  category        &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;                       &lt;span class=&quot;token comment&quot;&gt;-- pricing.category, sin normalizar&lt;/span&gt;
  pricing_type    &lt;span class=&quot;token keyword&quot;&gt;TEXT&lt;/span&gt;                        &lt;span class=&quot;token comment&quot;&gt;-- pricing.type, sin normalizar&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INDEX&lt;/span&gt; wa_outbound_month &lt;span class=&quot;token keyword&quot;&gt;ON&lt;/span&gt; wa_outbound &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;phone_number_id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Después, lo que haces con cada &lt;code&gt;statuses[]&lt;/code&gt;. El &lt;code&gt;phone_number_id&lt;/code&gt; sale de &lt;code&gt;value.metadata.phone_number_id&lt;/code&gt;, que es la unidad del cupo:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Open&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// autocompleta, pero acepta lo que Meta añada&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;StatusPricing&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/** Meta lo va a deprecar: decide con `type` + `category`. */&lt;/span&gt;
  billable&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  pricing_model&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Open&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;PMP&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;CBP&amp;#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Open&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;regular&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;free_customer_service&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;free_entry_point&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;free_group_customer_service&amp;#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  category&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Open&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;service&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;utility&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;marketing&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;marketing_lite&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;authentication&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;authentication-international&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;referral_conversion&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;group_service&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;group_utility&amp;#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;WebhookStatus&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// wamid&lt;/span&gt;
  status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Open&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;sent&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;delivered&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;read&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;failed&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;played&amp;#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  timestamp&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// epoch en segundos, como cadena&lt;/span&gt;
  recipient_id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  pricing&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; StatusPricing&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/** &amp;#39;YYYY-MM&amp;#39; en la zona horaria de tu WABA, p. ej. &amp;#39;America/Mexico_City&amp;#39;. */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;monthKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;epochSeconds&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; timeZone&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Intl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;DateTimeFormat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;en-CA&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; timeZone&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; year&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;numeric&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; month&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;2-digit&amp;#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;epochSeconds &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;DELIVERED&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;delivered&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;read&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// si llega `read` sin `delivered`, se entregó&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;applyStatus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;db&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; D1Database&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; phoneNumberId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; s&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; WebhookStatus&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; timeZone&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;prepare&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;INSERT INTO wa_outbound (wamid, phone_number_id, month, delivered, category, pricing_type)
       VALUES (?1, ?2, ?3, ?4, ?5, ?6)
       ON CONFLICT(wamid) DO UPDATE SET
         month        = CASE WHEN excluded.delivered = 1 AND delivered = 0 THEN excluded.month ELSE month END,
         delivered    = MAX(delivered, excluded.delivered),
         category     = COALESCE(excluded.category, category),
         pricing_type = COALESCE(excluded.pricing_type, pricing_type)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      phoneNumberId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;monthKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;timestamp&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; timeZone&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token constant&quot;&gt;DELIVERED&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pricing&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;category &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pricing&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El &lt;code&gt;ON CONFLICT&lt;/code&gt; hace el trabajo sucio: los estados repetidos no duplican, el &lt;code&gt;pricing&lt;/code&gt; se conserva venga en el &lt;code&gt;sent&lt;/code&gt; o en el &lt;code&gt;delivered&lt;/code&gt;, y el mes que cuenta es el de la entrega (un mensaje enviado el 30 de septiembre a las 23:59 y entregado el 1 de octubre cae en octubre).&lt;/p&gt;
&lt;p&gt;Por último, la cuenta del mes. La tarjeta lleva fecha, y lo que decide si se cobra es &lt;code&gt;type&lt;/code&gt;, no tu contador:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;FREE_SERVICE_PER_MONTH&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/** Tarjeta de México en MXN por mensaje entregado. Una entrada por trimestre con cambios. */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;MX_RATES_MXN&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; from&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; rates&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Record&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; from&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;2026-07&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rates&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; marketing&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.5614&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; utility&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.1565&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; authentication&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.1565&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; from&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;2026-10&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rates&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; marketing&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.7298&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; utility&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.1565&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; authentication&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.1565&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; service&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.1565&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;ratesFor&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;month&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;MX_RATES_MXN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findLast&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;card&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; card&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;from &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; month&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;rates &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;monthlyBill&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;db&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; D1Database&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; phoneNumberId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; month&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; results &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;prepare&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;SELECT category, pricing_type, COUNT(*) AS n FROM wa_outbound
       WHERE phone_number_id = ? AND month = ? AND delivered = 1
       GROUP BY category, pricing_type&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;phoneNumberId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; month&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; category&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; pricing_type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; n&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; rates &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ratesFor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;month&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; mxn &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; serviceDelivered &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; paidService &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; unpriced&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Record&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; category&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pricing_type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; n &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; results&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;category &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;pricing_type&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      unpriced&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;(sin pricing)&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;unpriced&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;(sin pricing)&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; n&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// Meta no documenta si los de punto de entrada gastan cupo: aquí no se cuentan.&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;category &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;service&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; pricing_type &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;free_entry_point&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; serviceDelivered &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; n&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pricing_type &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;regular&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// gratis: ventana, cupo o punto de entrada&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;category &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;service&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; paidService &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; n&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; rate &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; rates&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;category&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rate &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      unpriced&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;category&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;unpriced&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;category&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; n&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// se cobra y no tienes tarifa: no lo sumes como 0&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    mxn &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; n &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; rate&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    mxn&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;mxn &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    serviceDelivered&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    freeServiceLeft&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;FREE_SERVICE_PER_MONTH&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; serviceDelivered&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    paidService&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    unpriced&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Con eso, la alerta es una línea en un cron diario: avisa cuando &lt;code&gt;freeServiceLeft&lt;/code&gt; baje de 200, cuando aparezca el primer &lt;code&gt;paidService&lt;/code&gt; del mes o cuando &lt;code&gt;unpriced&lt;/code&gt; no venga vacío. Y si &lt;code&gt;paidService&lt;/code&gt; es mayor que cero con &lt;code&gt;serviceDelivered&lt;/code&gt; por debajo de 1,000, &lt;strong&gt;tu contador y Meta no coinciden&lt;/strong&gt;: puede ser otra aplicación mandando desde el mismo número, webhooks perdidos o el corte de mes en otra zona horaria. Manda Meta.&lt;/p&gt;
&lt;p&gt;Para cruzarlo a fin de mes, el campo &lt;a href=&quot;https://developers.facebook.com/documentation/business-messaging/whatsapp/analytics&quot;&gt;&lt;code&gt;pricing_analytics&lt;/code&gt;&lt;/a&gt; de la WABA acepta las dimensiones &lt;code&gt;PHONE&lt;/code&gt;, &lt;code&gt;PRICING_CATEGORY&lt;/code&gt; y &lt;code&gt;PRICING_TYPE&lt;/code&gt;. Dos avisos de la misma página: los datos son aproximados («may differ from what’s shown on invoices»), y &lt;code&gt;COST&lt;/code&gt; no viene si tu WABA usa la línea de crédito de un proveedor. Además, la descripción de &lt;code&gt;SERVICE&lt;/code&gt; en esa referencia (actualizada el 11 de junio) todavía dice «Messages that were not charged»; la página de mensajes sin plantilla ya muestra la consulta con &lt;code&gt;REGULAR&lt;/code&gt; + &lt;code&gt;SERVICE&lt;/code&gt;.&lt;/p&gt;
&lt;aside class=&quot;not-prose my-8 flex flex-wrap items-center gap-4 rounded-2xl border border-neutral-100 bg-white p-5 transition-all duration-200 hover:shadow-[5px_5px_rgba(0,98,90,0.3),10px_10px_rgba(0,98,90,0.2),15px_15px_rgba(0,98,90,0.1)] dark:border-zinc-800 dark:bg-zinc-900/40&quot; style=&quot;border-left:4px solid #3b82f6&quot;&gt; &lt;span class=&quot;grid size-12 shrink-0 place-items-center rounded-xl&quot; style=&quot;background:#3b82f61a;color:#3b82f6&quot;&gt;  &lt;/span&gt; &lt;div class=&quot;flex min-w-0 flex-1 flex-col gap-0.5&quot;&gt; &lt;span class=&quot;text-xs font-semibold tracking-wide text-zinc-500 uppercase dark:text-zinc-400&quot;&gt; Herramienta gratuita &lt;/span&gt; &lt;span class=&quot;text-lg leading-snug font-bold text-blacktext dark:text-mint-50&quot;&gt; Conversor de Timestamp Unix &lt;/span&gt; &lt;span class=&quot;text-sm text-pretty text-zinc-600 dark:text-zinc-400&quot;&gt; Convierte timestamps Unix a fecha legible (ISO, UTC, hora local y relativa) y fechas a timestamp, con detección de segundos o milisegundos y reloj en vivo. Sin registro. &lt;/span&gt; &lt;/div&gt; &lt;a href=&quot;https://ortamarco.me/herramientas/conversor-timestamp/&quot; data-umami-event=&quot;tool_callout_click&quot; data-umami-event-tool=&quot;conversor-timestamp&quot; class=&quot;ml-auto shrink-0 rounded-xl bg-mint-600 px-4 py-2 text-sm! font-semibold text-white! no-underline! transition-colors hover:bg-mint-700 hover:text-white!&quot;&gt; Abrir herramienta → &lt;/a&gt; &lt;/aside&gt;
&lt;h2 id=&quot;cuánto-cuesta-en-pesos-una-pyme-con-600-conversaciones-al-mes&quot;&gt;Cuánto cuesta en pesos: una PyME con 600 conversaciones al mes&lt;/h2&gt;
&lt;p&gt;Un consultorio o un taller con un agente que atiende WhatsApp, un solo número, clientes con lada +52 y sin anuncios Click-to-WhatsApp. Al mes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;600 conversaciones, con &lt;strong&gt;5 mensajes de servicio&lt;/strong&gt; del agente cada una: 3,000 mensajes de servicio.&lt;/li&gt;
&lt;li&gt;600 confirmaciones con plantilla de utilidad, mandadas &lt;strong&gt;dentro&lt;/strong&gt; de la ventana.&lt;/li&gt;
&lt;li&gt;400 recordatorios con plantilla de utilidad, &lt;strong&gt;fuera&lt;/strong&gt; de la ventana.&lt;/li&gt;
&lt;li&gt;Una campaña de marketing a 500 contactos.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Supongo que todo se entrega; en la vida real sale un poco menos, porque Meta solo cobra lo entregado. Tarifas de lista, sin impuestos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Septiembre (tarjeta de julio):&lt;/strong&gt;&lt;/p&gt;



































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Concepto&lt;/th&gt;&lt;th&gt;Cuenta&lt;/th&gt;&lt;th&gt;MXN&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Servicio&lt;/td&gt;&lt;td&gt;3,000 × 0&lt;/td&gt;&lt;td&gt;0.00&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Utility dentro de la ventana&lt;/td&gt;&lt;td&gt;600 × 0&lt;/td&gt;&lt;td&gt;0.00&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Utility fuera de la ventana&lt;/td&gt;&lt;td&gt;400 × 0.1565&lt;/td&gt;&lt;td&gt;62.60&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Marketing&lt;/td&gt;&lt;td&gt;500 × 0.5614&lt;/td&gt;&lt;td&gt;280.70&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;343.30&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Octubre (tarjeta de octubre):&lt;/strong&gt;&lt;/p&gt;



































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Concepto&lt;/th&gt;&lt;th&gt;Cuenta&lt;/th&gt;&lt;th&gt;MXN&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Servicio&lt;/td&gt;&lt;td&gt;(3,000 − 1,000) × 0.1565 = 2,000 × 0.1565&lt;/td&gt;&lt;td&gt;313.00&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Utility dentro de la ventana&lt;/td&gt;&lt;td&gt;600 × 0.1565&lt;/td&gt;&lt;td&gt;93.90&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Utility fuera de la ventana&lt;/td&gt;&lt;td&gt;400 × 0.1565&lt;/td&gt;&lt;td&gt;62.60&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Marketing&lt;/td&gt;&lt;td&gt;500 × 0.7298&lt;/td&gt;&lt;td&gt;364.90&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;834.40&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;La factura de Meta pasa de 343.30 a 834.40 pesos: &lt;strong&gt;491.10 MXN más al mes&lt;/strong&gt;. No es una cifra que quiebre a nadie, pero es 2.4 veces la de septiembre, y 406.90 de esos 491.10 pesos (más del 80 %) salen de dos líneas que hoy valen cero.&lt;/p&gt;
&lt;h3 id=&quot;dónde-está-la-palanca-de-verdad-en-tu-código&quot;&gt;Dónde está la palanca de verdad: en tu código&lt;/h3&gt;
&lt;p&gt;Pasar las confirmaciones de plantilla de utilidad a texto libre dentro de la ventana &lt;strong&gt;no ahorra nada en este ejemplo&lt;/strong&gt;: la tarifa de servicio y la de utilidad son la misma en México (0.1565) y el cupo de 1,000 ya se agotó. Solo ayuda a negocios que se quedan por debajo de 1,000 mensajes de servicio al mes.&lt;/p&gt;
&lt;p&gt;Lo que sí mueve la cifra es &lt;strong&gt;cuántos mensajes manda tu agente por respuesta&lt;/strong&gt;. Muchos agentes de IA parten cada respuesta en tres o cuatro burbujas porque se lee más natural. Desde el 1 de octubre, cada burbuja pasado el mensaje 1,000 es un cargo. Si el mismo agente contesta con 3 mensajes por conversación en vez de 5:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;600 × 3 = 1,800 mensajes de servicio.&lt;/li&gt;
&lt;li&gt;(1,800 − 1,000) × 0.1565 = 800 × 0.1565 = &lt;strong&gt;125.20 MXN&lt;/strong&gt;, contra 313.00.&lt;/li&gt;
&lt;li&gt;Ahorro: &lt;strong&gt;187.80 MXN al mes&lt;/strong&gt;, sin tocar un solo precio.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Si parte de tus clientes llega por anuncios Click-to-WhatsApp, responder dentro de las primeras 24 horas abre la ventana de punto de entrada de 72 horas, y ahí todo sale como &lt;code&gt;free_entry_point&lt;/code&gt;. Meta confirma que esa ventana no cambia para la entrega de mensajes.&lt;/p&gt;
&lt;h2 id=&quot;qué-pasa-si-no-tienes-método-de-pago&quot;&gt;Qué pasa si no tienes método de pago&lt;/h2&gt;
&lt;p&gt;Aquí Meta tiene dos redacciones, de fechas distintas, y conviene leerlas juntas.&lt;/p&gt;
&lt;p&gt;La &lt;a href=&quot;https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing/non-template-messages&quot;&gt;página de mensajes sin plantilla&lt;/a&gt;, actualizada el &lt;strong&gt;25 de agosto de 2026&lt;/strong&gt;, dice:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For any Solution Provider or directly-integrated businesses that does not have a payment method on file by September 30, 2026, Meta will stop delivering service messages as of when they become charged on October 1, 2026. To avoid disruptions to your service messages, please add a payment method for your WhatsApp Business Account(s) by September 30, 2026.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;La &lt;a href=&quot;https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing&quot;&gt;página de precios&lt;/a&gt;, actualizada el &lt;strong&gt;10 de septiembre de 2026&lt;/strong&gt;, que es la que introduce el cupo gratis como novedad, precisa:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you do not have a payment method for your WhatsApp Business account: Meta &lt;em&gt;will&lt;/em&gt; deliver your first 1,000 service messages each month but &lt;em&gt;not&lt;/em&gt; deliver as of your 1,001st.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;No se contradicen: la primera dice que se dejan de entregar «cuando empiezan a cobrarse», y la segunda, escrita después del cupo, dice que eso ocurre en el mensaje 1,001. En la práctica:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;La fecha que da Meta para tener el método de pago es el 30 de septiembre.&lt;/strong&gt; No esperes a ver el primer fallo.&lt;/li&gt;
&lt;li&gt;Sin método de pago, tu agente funciona normal hasta que el número cruza los 1,000 mensajes de servicio del mes, y &lt;strong&gt;ahí deja de entregar a mitad de mes&lt;/strong&gt;. En un negocio con volumen, eso puede ser un martes a media tarde.&lt;/li&gt;
&lt;li&gt;Meta no publica, en las páginas que revisé, qué código de error trae el estado &lt;code&gt;failed&lt;/code&gt; en ese caso. Vigila un aumento de &lt;code&gt;failed&lt;/code&gt; en mensajes de servicio desde el 1 de octubre.&lt;/li&gt;
&lt;li&gt;El aviso de agosto va dirigido a «Solution Provider or directly-integrated businesses». Si tu WABA factura a través de un proveedor de soluciones, pídele por escrito que confirme que el método de pago está resuelto antes del 30 de septiembre.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;checklist-antes-del-30-de-septiembre&quot;&gt;Checklist antes del 30 de septiembre&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Método de pago en el Billing Hub&lt;/strong&gt; de cada WABA, o confirmación escrita de tu proveedor si facturas a través de él.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parser tolerante:&lt;/strong&gt; &lt;code&gt;pricing.type&lt;/code&gt; y &lt;code&gt;pricing.category&lt;/code&gt; se guardan tal cual. Nada de descartar el estado porque trae &lt;code&gt;group_service&lt;/code&gt;, &lt;code&gt;free_group_customer_service&lt;/code&gt; o &lt;code&gt;authentication-international&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deja de decidir con &lt;code&gt;billable&lt;/code&gt;.&lt;/strong&gt; Usa &lt;code&gt;type === &amp;#39;regular&amp;#39;&lt;/code&gt; más la tarifa de &lt;code&gt;category&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tarjeta con fecha de vigencia&lt;/strong&gt;, cargada con la de octubre (marketing a 0.7298 en México). La próxima fecha en que Meta puede cambiar tarifas es el 1 de enero de 2027.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cuenta en &lt;code&gt;delivered&lt;/code&gt; o &lt;code&gt;read&lt;/code&gt;, una vez por &lt;code&gt;wamid&lt;/code&gt;&lt;/strong&gt;, y agrupa por &lt;code&gt;phone_number_id&lt;/code&gt; y mes en la zona horaria de tu WABA.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Alertas:&lt;/strong&gt; quedan menos de 200 de servicio gratis, primer servicio &lt;code&gt;regular&lt;/code&gt; del mes, categorías sin tarifa y aumento de &lt;code&gt;failed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plantillas de utilidad dentro de la ventana:&lt;/strong&gt; desde el 1 de octubre se cobran desde la primera. Si mandas menos de 1,000 mensajes de servicio al mes, un texto libre dentro de la ventana puede salir gratis donde la plantilla cuesta.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plantillas de marketing contra utilidad:&lt;/strong&gt; Meta te cobra la categoría que tenga la plantilla al momento de usarla, y puede recategorizarla. Suscríbete al webhook &lt;a href=&quot;https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/template_category_update&quot;&gt;&lt;code&gt;template_category_update&lt;/code&gt;&lt;/a&gt;, que avisa 24 horas antes de un cambio automático de categoría.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mensajes por respuesta:&lt;/strong&gt; junta las burbujas de tu agente. Es la palanca más barata del ejemplo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cruce mensual&lt;/strong&gt; contra &lt;code&gt;pricing_analytics&lt;/code&gt;, sabiendo que es aproximado.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Si prefieres que alguien revise tu integración, tus plantillas y cuánto te va a costar octubre antes de que llegue, es justo lo que hago en &lt;a href=&quot;https://ortamarco.me/whatsapp-para-tu-negocio/&quot;&gt;WhatsApp para tu negocio&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Para seguir leyendo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/agente-ia-whatsapp-pyme-laravel-openai/&quot;&gt;Monta un agente de IA en WhatsApp para tu pyme (Laravel + OpenAI)&lt;/a&gt;: el webhook, la memoria de conversación y las reglas de la ventana de 24 horas, desde cero.&lt;/li&gt;
&lt;/ul&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué cambia en la API de WhatsApp el 1 de octubre de 2026? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Meta empieza a cobrar dos tipos de mensaje que hoy son gratis: los mensajes de servicio (los que mandas sin plantilla dentro de la ventana de 24 horas) a partir del mensaje 1,001 de cada mes por número de negocio, y las plantillas de utilidad que mandas dentro de esa ventana, que se cobran desde la primera. En México, ambos a 0.1565 MXN por mensaje entregado. Además, la tarifa de marketing de México sube de 0.5614 a 0.7298 MXN. El cambio entra a las 00:00 en la zona horaria de tu cuenta de WhatsApp Business. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cómo sé en el webhook si un mensaje de WhatsApp se cobró? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Mira pricing.type en el webhook de estados: regular significa que se cobra; free_customer_service, free_entry_point o free_group_customer_service significan que salió gratis. pricing.category te dice qué tarifa se aplicó. No uses pricing.billable para decidir: la referencia de Meta dice que se va a deprecar en una versión futura y recomienda usar type y category juntos. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Los 1,000 mensajes gratis de WhatsApp son por cuenta o por número? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Por número de teléfono de negocio. Meta dice que cada número recibe 1,000 mensajes de servicio gratis al mes, que cobra desde el mensaje de servicio 1,001 entregado ese mes y que el cupo no se acumula: se reinicia cada mes. El cupo es solo de mensajes de servicio; las plantillas de utilidad dentro de la ventana no tienen cupo gratis. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué pasa si no tengo método de pago en WhatsApp Business el 1 de octubre? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Según la página de precios de Meta actualizada el 10 de septiembre de 2026, Meta entrega tus primeros 1,000 mensajes de servicio del mes y deja de entregarlos a partir del 1,001. La página de mensajes sin plantilla pide tener el método de pago en el Billing Hub antes del 30 de septiembre de 2026. Si facturas a través de un proveedor de soluciones, confirma con él que el método de pago está resuelto. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿El objeto pricing llega en todos los estados del webhook? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No. Según la referencia de Meta, pricing viene con el estado sent y con uno de delivered o read, y en la v24 puede venir solo en delivered. Como Meta cobra lo entregado y a veces manda read sin delivered, cuenta cada mensaje una sola vez por wamid cuando llegue delivered o read, y guarda el pricing venga en el estado que venga. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuánto va a pagar a Meta una pyme mexicana desde octubre? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Depende del volumen. Con 600 conversaciones al mes de 5 mensajes de servicio cada una, 600 confirmaciones de utilidad dentro de la ventana, 400 recordatorios de utilidad fuera de ella y una campaña de marketing a 500 contactos, la factura de Meta pasa de 343.30 MXN en septiembre a 834.40 MXN en octubre con tarifas de lista. Reducir el agente de 5 a 3 mensajes por conversación baja la parte de servicio de 313.00 a 125.20 MXN. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Conviene cambiar plantillas de utilidad por texto libre dentro de la ventana? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Solo si mandas menos de 1,000 mensajes de servicio al mes. En México el servicio y la utilidad cuestan lo mismo (0.1565 MXN) desde el 1 de octubre, pero el servicio tiene 1,000 gratis al mes por número y la utilidad no. Una vez agotado el cupo, da igual. La palanca más grande suele ser que tu agente mande menos mensajes por respuesta. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Desarrollo Web</category><category>WhatsApp</category><category>Costos</category><category>Webhooks</category><category>TypeScript</category><category>Backend</category><category>Agentes</category><author>Marco Orta</author></item><item><title>Validar RFC, CURP y CFDI desde Claude, sin API keys ni PAC</title><link>https://ortamarco.me/blog/validar-rfc-curp-cfdi-claude-mcp/</link><guid isPermaLink="true">https://ortamarco.me/blog/validar-rfc-curp-cfdi-claude-mcp/</guid><description>Un servidor MCP open source para que Claude valide RFC, CURP, CLABE y NSS con su dígito real, lea CFDI 4.0 y consulte su estado en el SAT. Sin API keys ni PAC.</description><pubDate>Fri, 11 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Pídele a un agente de IA que valide un RFC y casi siempre escribirá una regex. Si además calcula el dígito verificador, va a rechazar &lt;code&gt;XAXX010101000&lt;/code&gt;, el RFC genérico de público en general, porque ese RFC no satisface su propio dígito.&lt;/strong&gt; El SAT lo asignó por decreto, y el módulo 11 pide un &lt;code&gt;4&lt;/code&gt; donde el SAT puso un &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Si integras facturación, llevas la contabilidad de un despacho o revisas XML de proveedores, ya sabes cómo acaba eso: el formulario que no deja facturar al público en general, la factura que “no existe” en el SAT aunque la tengas en la mano. Son detalles que no puedes confiarle a la memoria de un modelo. Por eso hice &lt;strong&gt;&lt;a href=&quot;https://ortamarco.me/portfolio/mx-fiscal-mcp/&quot;&gt;mx-fiscal-mcp-server&lt;/a&gt;&lt;/strong&gt;: un servidor &lt;a href=&quot;https://modelcontextprotocol.io&quot;&gt;MCP&lt;/a&gt; open source que le da a Claude (o a Cursor, o a cualquier cliente MCP) ocho herramientas de solo lectura para validar RFC, CURP, CLABE y NSS con sus algoritmos reales, leer CFDI 4.0, consultar al SAT si una factura sigue vigente y buscar en sus catálogos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No necesita API keys, ni CSD, ni contrato con un PAC.&lt;/strong&gt; Aquí va la instalación, qué le puedes pedir y los seis detalles que una implementación hecha a mano suele fallar, incluido uno que yo mismo fallé antes de publicarlo.&lt;/p&gt;
&lt;h2 id=&quot;instalación-en-una-línea&quot;&gt;Instalación en una línea&lt;/h2&gt;
&lt;p&gt;El servidor está publicado en &lt;a href=&quot;https://www.npmjs.com/package/mx-fiscal-mcp-server&quot;&gt;npm&lt;/a&gt; y en el registro oficial de MCP, así que no hay nada que clonar. En Claude Code:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;claude mcp &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; mx-fiscal -- npx &lt;span class=&quot;token parameter variable&quot;&gt;-y&lt;/span&gt; mx-fiscal-mcp-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En Claude Desktop o Cursor, agrégalo a &lt;code&gt;claude_desktop_config.json&lt;/code&gt; o a &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&amp;quot;mcpServers&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&amp;quot;mx-fiscal&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&amp;quot;command&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;npx&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&amp;quot;args&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;-y&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;mx-fiscal-mcp-server&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En Windows usa &lt;code&gt;&amp;quot;command&amp;quot;: &amp;quot;cmd&amp;quot;&lt;/code&gt; con &lt;code&gt;&amp;quot;args&amp;quot;: [&amp;quot;/c&amp;quot;, &amp;quot;npx&amp;quot;, &amp;quot;-y&amp;quot;, &amp;quot;mx-fiscal-mcp-server&amp;quot;]&lt;/code&gt;. Pide Node.js 20 o superior.&lt;/p&gt;
&lt;p&gt;Está hecho sobre el SDK de MCP v2, así que habla la revisión 2026-07-28 del protocolo y sigue atendiendo, desde el mismo proceso, a los clientes de la era 2025 que usan hoy Claude Desktop, Claude Code y Cursor. Si mantienes un servidor propio, escribí &lt;a href=&quot;https://ortamarco.me/blog/migrar-servidor-mcp-2026-07-28/&quot;&gt;qué cambia al migrar a la 2026-07-28&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;las-ocho-herramientas&quot;&gt;Las ocho herramientas&lt;/h2&gt;









































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Herramienta&lt;/th&gt;&lt;th&gt;Qué hace&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;validate_rfc&lt;/code&gt;&lt;/td&gt;&lt;td&gt;RFC de persona física (13 caracteres) y moral (12): dígito verificador, campos desglosados, fecha de nacimiento o constitución, y si es uno de los genéricos del SAT&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;validate_curp&lt;/code&gt;&lt;/td&gt;&lt;td&gt;CURP de 18 caracteres: dígito verificador, fecha de nacimiento, sexo, entidad y el marcador de siglo&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;validate_clabe&lt;/code&gt;&lt;/td&gt;&lt;td&gt;CLABE de 18 dígitos: dígito de control, banco y plaza&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;validate_nss&lt;/code&gt;&lt;/td&gt;&lt;td&gt;NSS del IMSS de 11 dígitos con su dígito Luhn&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;generate_test_data&lt;/code&gt;&lt;/td&gt;&lt;td&gt;De 1 a 100 personas físicas o morales ficticias y coherentes: RFC y CURP derivados del mismo nombre y fecha, CLABE de un banco real&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;parse_cfdi&lt;/code&gt;&lt;/td&gt;&lt;td&gt;XML de CFDI 4.0 a JSON, con cada clave de catálogo etiquetada, los dos RFC validados y los totales comprobados&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;cfdi_status&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Consulta el servicio público del SAT: si la factura existe, si está cancelada, si es cancelable y el resultado de la validación EFOS (lista del 69-B)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;sat_catalog_lookup&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Nueve catálogos (régimen fiscal, uso de CFDI, forma y método de pago, entre otros), por clave o por texto&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Solo &lt;code&gt;cfdi_status&lt;/code&gt; sale a la red. Las otras siete nunca salen del proceso.&lt;/p&gt;
&lt;h2 id=&quot;qué-le-puedes-pedir&quot;&gt;Qué le puedes pedir&lt;/h2&gt;
&lt;p&gt;Algunos prompts que funcionan bien, con la forma de lo que devuelven:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;“¿&lt;code&gt;GODE561231GR8&lt;/code&gt; es un RFC válido? Desglósalo.”&lt;/em&gt; — si es de persona física o moral, la fecha que lleva codificada y un motivo legible para cada fallo: no un simple &lt;code&gt;invalid&lt;/code&gt;, sino &lt;em&gt;“el dígito verificador final no coincide con el resultado del módulo 11”&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“Genérame 20 clientes con RFC, CURP y CLABE válidos para mi seeder.”&lt;/em&gt; — todos los dígitos cuadran, y el RFC y la CURP de cada registro describen a la misma persona. Si solo necesitas los datos sin agente de por medio, está el &lt;a href=&quot;https://ortamarco.me/herramientas/generador-datos-prueba-mexico/&quot;&gt;generador de datos de prueba&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“Lee esta factura y dime si sigue vigente en el SAT.”&lt;/em&gt; — Claude llama a &lt;code&gt;parse_cfdi&lt;/code&gt; para etiquetarlo todo y comprobar la aritmética, y después a &lt;code&gt;cfdi_status&lt;/code&gt; con los cuatro valores que necesita.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ese último es el flujo que más tiempo ahorra, sobre todo si revisas XML de proveedores uno por uno, y es el que esconde casi todas las trampas de abajo.&lt;/p&gt;
&lt;aside class=&quot;not-prose my-8 flex flex-wrap items-center gap-4 rounded-2xl border border-neutral-100 bg-white p-5 transition-all duration-200 hover:shadow-[5px_5px_rgba(0,98,90,0.3),10px_10px_rgba(0,98,90,0.2),15px_15px_rgba(0,98,90,0.1)] dark:border-zinc-800 dark:bg-zinc-900/40&quot; style=&quot;border-left:4px solid #3b82f6&quot;&gt; &lt;span class=&quot;grid size-12 shrink-0 place-items-center rounded-xl&quot; style=&quot;background:#3b82f61a;color:#3b82f6&quot;&gt;  &lt;/span&gt; &lt;div class=&quot;flex min-w-0 flex-1 flex-col gap-0.5&quot;&gt; &lt;span class=&quot;text-xs font-semibold tracking-wide text-zinc-500 uppercase dark:text-zinc-400&quot;&gt; Herramienta gratuita &lt;/span&gt; &lt;span class=&quot;text-lg leading-snug font-bold text-blacktext dark:text-mint-50&quot;&gt; Validador de RFC, CURP, CLABE y NSS &lt;/span&gt; &lt;span class=&quot;text-sm text-pretty text-zinc-600 dark:text-zinc-400&quot;&gt; Comprueba si un RFC, CURP, CLABE o NSS es correcto. Detecta el tipo solo, verifica el dígito verificador y desglosa cada parte de la clave. Todo en tu navegador. &lt;/span&gt; &lt;/div&gt; &lt;a href=&quot;https://ortamarco.me/herramientas/validador-rfc-curp-clabe/&quot; data-umami-event=&quot;tool_callout_click&quot; data-umami-event-tool=&quot;validador-rfc-curp-clabe&quot; class=&quot;ml-auto shrink-0 rounded-xl bg-mint-600 px-4 py-2 text-sm! font-semibold text-white! no-underline! transition-colors hover:bg-mint-700 hover:text-white!&quot;&gt; Abrir herramienta → &lt;/a&gt; &lt;/aside&gt;
&lt;h2 id=&quot;seis-detalles-que-una-regex-hace-mal&quot;&gt;Seis detalles que una regex hace mal&lt;/h2&gt;
&lt;h3 id=&quot;1-el-rfc-de-público-en-general-no-satisface-su-propio-dígito-verificador&quot;&gt;1. El RFC de público en general no satisface su propio dígito verificador&lt;/h3&gt;
&lt;p&gt;Aplica el módulo 11 a los primeros doce caracteres de &lt;code&gt;XAXX010101000&lt;/code&gt; y el algoritmo pide un &lt;code&gt;4&lt;/code&gt;. En cambio, &lt;code&gt;XEXX010101000&lt;/code&gt;, el de residentes en el extranjero, sí lo satisface.&lt;/p&gt;
&lt;p&gt;Un validador que solo corre el algoritmo rechaza toda factura al público en general, y por eso tantos formularios de facturación la rechazan. Uno que solo revisa la forma deja pasar errores de dedo. El servidor reporta los dos hechos por separado, &lt;code&gt;is_generic: true&lt;/code&gt; y &lt;code&gt;check_digit_satisfied: false&lt;/code&gt;, y explica con palabras por qué esa combinación es correcta.&lt;/p&gt;
&lt;h3 id=&quot;2-la-clabe-toma-la-unidad-de-cada-producto-no-la-suma-de-sus-cifras&quot;&gt;2. La CLABE toma la unidad de cada producto, no la suma de sus cifras&lt;/h3&gt;
&lt;p&gt;El dígito de control de la CLABE pondera los primeros 17 dígitos con 3, 7, 1, 3, 7, 1… De cada producto se queda &lt;strong&gt;la unidad&lt;/strong&gt;, es decir, el producto módulo 10: un &lt;code&gt;63&lt;/code&gt; aporta &lt;code&gt;3&lt;/code&gt;. Luego se suma, se toma el módulo 10 y se resta de 10 (si da 10, el dígito es &lt;code&gt;0&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Una implementación que copia Luhn hace otra cosa: cuando un producto pasa de 9, suma sus cifras, y ese mismo &lt;code&gt;63&lt;/code&gt; aporta &lt;code&gt;6 + 3 = 9&lt;/code&gt;. Las dos rutas no coinciden: con los 17 dígitos &lt;code&gt;09000000000000000&lt;/code&gt;, el algoritmo de la CLABE da &lt;code&gt;7&lt;/code&gt; y la versión estilo Luhn da &lt;code&gt;1&lt;/code&gt;. Eso basta para aceptar cuentas mal escritas y rechazar cuentas buenas.&lt;/p&gt;
&lt;h3 id=&quot;3-el-carácter-17-de-la-curp-marca-el-siglo&quot;&gt;3. El carácter 17 de la CURP marca el siglo&lt;/h3&gt;
&lt;p&gt;La CURP guarda el año de nacimiento con dos dígitos, así que &lt;code&gt;31&lt;/code&gt; puede ser 1931 o 2031. RENAPO lo resuelve con el carácter 17: &lt;strong&gt;si es dígito, naciste antes de 2000; si es letra, en 2000 o después.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Este es el que fallé yo. La primera versión del servidor leía esa regla al revés y, como la fecha de nacimiento se decodifica justo a través de ese carácter, reportaba un “desacuerdo entre el marcador y la fecha” en cada CURP válida que veía. La suite de tests había capturado el mensaje equivocado como comportamiento esperado. Una revisión independiente lo detectó antes de publicar; el vector de prueba canónico &lt;code&gt;BOXW310820HNERXN09&lt;/code&gt; ahora se decodifica, correctamente, como 20 de agosto de 1931.&lt;/p&gt;
&lt;h3 id=&quot;4-las-claves-de-entidad-de-la-curp-son-de-renapo-no-del-inegi&quot;&gt;4. Las claves de entidad de la CURP son de RENAPO, no del INEGI&lt;/h3&gt;
&lt;p&gt;Las posiciones 12 y 13 de la CURP codifican la entidad de nacimiento con claves propias de RENAPO: &lt;code&gt;DF&lt;/code&gt; es la Ciudad de México, &lt;code&gt;MC&lt;/code&gt; el Estado de México y &lt;code&gt;NE&lt;/code&gt; nacido en el extranjero. No coinciden ni con las claves de entidad del INEGI ni con ISO 3166-2:MX, así que cruzar contra cualquiera de esas tablas etiqueta mal a la gente sin avisar.&lt;/p&gt;
&lt;h3 id=&quot;5-la-consulta-al-sat-es-exigente-con-el-total&quot;&gt;5. La consulta al SAT es exigente con el total&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;cfdi_status&lt;/code&gt; le manda al SAT una expresión armada con cuatro valores: RFC emisor, RFC receptor, total y UUID. El total va con el formato que usan las implementaciones de referencia (&lt;a href=&quot;https://github.com/nodecfdi/cfdi-expresiones&quot;&gt;nodecfdi&lt;/a&gt; y phpcfdi): seis decimales, quitando los ceros finales pero dejando al menos uno. Así, &lt;code&gt;1160.00&lt;/code&gt; viaja como &lt;code&gt;1160.0&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;?re=TES150312DX2&amp;amp;rr=PELJ900521DK2&amp;amp;tt=1160.0&amp;amp;id=5A7B3C1D-9E2F-4A6B-8C0D-1E2F3A4B5C6D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Un total que no respeta ese formato, como un &lt;code&gt;1,160.00&lt;/code&gt; tecleado a mano, puede hacer que el SAT conteste &lt;em&gt;No Encontrado&lt;/em&gt; de una factura que sí existe. Por eso el servidor revisa los cuatro valores antes de gastar una petición, y recomienda pasarle el XML completo para que los saque él mismo.&lt;/p&gt;
&lt;p&gt;En la misma consulta viven dos reglas más. Un RFC con &lt;code&gt;&amp;amp;&lt;/code&gt; viaja codificado como &lt;code&gt;&amp;amp;amp;&lt;/code&gt;, igual que en la implementación de referencia de &lt;a href=&quot;https://github.com/nodecfdi/sat-estado-cfdi&quot;&gt;nodecfdi&lt;/a&gt;. Y un campo &lt;code&gt;ValidacionEFOS&lt;/code&gt; vacío &lt;strong&gt;no&lt;/strong&gt; significa que el emisor esté en la lista del 69-B: la &lt;a href=&quot;http://omawww.sat.gob.mx/tramitesyservicios/Paginas/documentos/Documentacion_WS_Consulta_CFDI_v1.4.pdf&quot;&gt;documentación del servicio del SAT&lt;/a&gt; define los códigos (del 100 al 104 cuando el emisor o algún RFC a cuenta de terceros está listado, 200 y 201 cuando no), pero no el valor vacío, así que el servidor lo reporta como &lt;code&gt;unknown&lt;/code&gt;, nunca como “listado”. Desde la versión 1.0.2 separa las dos preguntas: &lt;code&gt;efos_state&lt;/code&gt; responde solo por el emisor (100, 101 y 104 son &lt;code&gt;listed&lt;/code&gt;; 102, 103, 200 y 201 son &lt;code&gt;not_listed&lt;/code&gt;) y &lt;code&gt;efos_third_party_state&lt;/code&gt; por los RFC a cuenta de terceros, con el código crudo siempre en &lt;code&gt;validacion_efos&lt;/code&gt;. La 1.0.1 leía 102 y 103 como «el emisor está listado», justo lo contrario de lo que dice el SAT.&lt;/p&gt;
&lt;h3 id=&quot;6-el-total-incluye-los-impuestos-locales&quot;&gt;6. El total incluye los impuestos locales&lt;/h3&gt;
&lt;p&gt;La comprobación aritmética (subtotal menos descuento, más trasladados, menos retenidos) es como detectas una factura alterada o mal generada. Pero el esquema de CFDI 4.0 define el total sobre impuestos federales &lt;strong&gt;y locales&lt;/strong&gt;, y los locales viven aparte, en el complemento &lt;code&gt;implocal&lt;/code&gt;. Una factura de hotel con impuesto estatal al hospedaje parece descuadrada justo por ese impuesto si ignoras el complemento. El servidor lo suma.&lt;/p&gt;
&lt;p&gt;Si solo quieres abrir una factura a mano, sin agente de por medio, el &lt;a href=&quot;https://www.siemprecontable.net/herramientas/visor-cfdi&quot;&gt;visor de CFDI&lt;/a&gt; que nació en este sitio vive ahora en Siempre Contable.&lt;/p&gt;
&lt;h2 id=&quot;leer-no-es-verificar&quot;&gt;Leer no es verificar&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;parse_cfdi&lt;/code&gt; lee el XML: comprobante, emisor, receptor, cada concepto con sus impuestos, los totales de impuestos y el timbre fiscal digital. Etiqueta cada clave de catálogo, valida los dos RFC y comprueba la aritmética.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No&lt;/strong&gt; verifica el sello digital. Una factura que se lee perfecto puede estar cancelada o ser inventada de principio a fin, y para eso está &lt;code&gt;cfdi_status&lt;/code&gt;. Y que el SAT no conteste no vuelve inválida la factura: el servicio no tiene página de estado y se cae (su documentación habla de capacidad para 2 millones de consultas por hora y pide no aumentar el volumen, pero no promete disponibilidad). Cuando falla, la herramienta devuelve &lt;code&gt;available: false&lt;/code&gt; con el motivo, que es una afirmación sobre el SAT y nunca sobre el documento, tras un timeout de 10 segundos y un reintento.&lt;/p&gt;
&lt;p&gt;La misma honestidad aplica a los identificadores. Que el dígito verificador cuadre dice que la cadena está bien formada y nada más. Solo el SAT puede decir que un RFC está dado de alta, y solo RENAPO que una CURP pertenece a alguien; este servidor no le pregunta a ninguno de los dos, y su redacción nunca da a entender que lo hizo. Hasta los datos ficticios llevan aviso: se generan a partir de nombres comunes, así que una CURP o un teléfono generados pueden coincidir por azar con los de una persona real.&lt;/p&gt;
&lt;h2 id=&quot;lo-que-deliberadamente-no-hace&quot;&gt;Lo que deliberadamente no hace&lt;/h2&gt;
&lt;p&gt;No construye ni sella facturas. Esa mitad de la facturación electrónica necesita tu CSD y, para timbrar, un PAC, y ya tiene servidor MCP: &lt;a href=&quot;https://github.com/cmendezs/mcp-cfdi-mx&quot;&gt;mcp-cfdi-mx&lt;/a&gt;, en Python, que arma, valida y sella CFDI 4.0 con tu CSD. Este es la mitad de lectura, que no necesita nada, y los dos se complementan.&lt;/p&gt;
&lt;h2 id=&quot;auto-hospedarlo-con-seguridad&quot;&gt;Auto-hospedarlo con seguridad&lt;/h2&gt;
&lt;p&gt;Además de stdio, el servidor habla Streamable HTTP sin estado, para uso compartido o remoto:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token assign-left variable&quot;&gt;TRANSPORT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;http npx &lt;span class=&quot;token parameter variable&quot;&gt;-y&lt;/span&gt; mx-fiscal-mcp-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Es seguro por defecto: escucha en &lt;code&gt;127.0.0.1&lt;/code&gt; y solo acepta cabeceras &lt;code&gt;Host&lt;/code&gt; y &lt;code&gt;Origin&lt;/code&gt; de &lt;code&gt;localhost&lt;/code&gt;, lo que bloquea ataques de DNS rebinding desde una página web. Para exponerlo detrás de un proxy inverso, define &lt;code&gt;HOST=0.0.0.0&lt;/code&gt;, &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt; con tu dominio público y &lt;code&gt;MCP_AUTH_TOKEN&lt;/code&gt; para autenticación Bearer. No te saltes el token: una instancia abierta le hace consultas al SAT a nombre de cualquiera y puede acabar con tu IP limitada.&lt;/p&gt;
&lt;p&gt;El XML de una factura es entrada no confiable, así que un documento que declara un &lt;code&gt;DOCTYPE&lt;/code&gt; se rechaza antes de parsearlo (un CFDI nunca lo lleva), y hay topes de tamaño y de número de elementos que acotan la memoria que puede consumir un solo parseo.&lt;/p&gt;
&lt;p&gt;La aritmética de los dígitos verificadores vive en &lt;a href=&quot;https://www.npmjs.com/package/mx-identifiers&quot;&gt;mx-identifiers&lt;/a&gt;, una librería sin dependencias probada contra los vectores públicos del SAT, Banxico y RENAPO, y el servidor tiene 51 tests unitarios offline más un smoke test que llama a cada herramienta sobre el protocolo real en las dos eras. Si buscas servidores MCP para otras áreas, mantengo una lista de &lt;a href=&quot;https://ortamarco.me/blog/mejores-servidores-mcp-seo-2026/&quot;&gt;los mejores servidores MCP para SEO&lt;/a&gt;, con los míos incluidos.&lt;/p&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿El servidor necesita e.firma, CSD o una API key? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No. Todos los algoritmos de dígito verificador y todos los catálogos son públicos, y el servicio de consulta de estado de CFDI del SAT es público y no pide autenticación. El servidor solo lee: nunca sella ni cancela nada, así que no necesita credenciales de ningún tipo. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Por qué el SAT dice &amp;#39;No Encontrado&amp;#39; de una factura que sí existe? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Normalmente porque uno de los cuatro valores de la consulta no coincide con lo que tiene el SAT: un total con otro formato (tiene que ir con seis decimales, quitando los ceros finales pero dejando uno, así que 1160.00 viaja como 1160.0), un separador de miles o un UUID mal copiado. Si le pasas el XML completo a cfdi_status, el servidor saca los cuatro valores por su cuenta. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿XAXX010101000 es un RFC válido? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Sí. Es el RFC genérico del SAT para operaciones con el público en general, asignado por decreto, y aparece en facturas reales. No satisface el dígito verificador del módulo 11 (el algoritmo pide un 4), así que los validadores sin una lista explícita de genéricos lo rechazan. XEXX010101000, el de residentes en el extranjero, también es genérico y ese sí satisface el algoritmo. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Sirve para saber si un proveedor está en la lista del 69-B? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Solo a través de sus facturas: cfdi_status devuelve el campo ValidacionEFOS de cada CFDI que consultas, no busca un RFC suelto. El servidor separa al emisor (efos_state) de los RFC a cuenta de terceros (efos_third_party_state) y deja el campo vacío como desconocido. Aun así, contrasta con el listado del 69-B que publica el SAT antes de tomar una decisión. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Los datos de prueba generados pueden ser de una persona real? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Por azar, sí. generate_test_data arma cada registro con nombres mexicanos comunes y una fecha de nacimiento aleatoria, y la CURP se deriva justo de esos campos, así que puede coincidir con la CURP o el teléfono de una persona real. Los datos son para fixtures, seeders y demos: nunca los mandes al SAT ni a un PAC, y nunca los uses en lugar de los datos reales de un cliente. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Tutoriales</category><category>MCP</category><category>Claude</category><category>CFDI</category><category>México</category><category>TypeScript</category><category>Agentes de IA</category><author>Marco Orta</author></item><item><title>npm y la cadena de suministro: el gusano que se coló por el trusted publishing</title><link>https://ortamarco.me/blog/npm-trusted-publishing-no-basta/</link><guid isPermaLink="true">https://ortamarco.me/blog/npm-trusted-publishing-no-basta/</guid><description>El consejo unánime desde 2025 era migrar a trusted publishing con OIDC. En mayo de 2026 Shai-Hulud entró por ahí y publicó paquetes de TanStack con procedencia legítima. Qué protege de verdad OIDC, qué no, y las cuatro medidas que sí habrían parado los últimos ataques.</description><pubDate>Tue, 08 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Durante un año, el consejo de seguridad de npm fue uno solo y todo el mundo lo repitió: deja de publicar con tokens de larga vida y pásate al &lt;em&gt;trusted publishing&lt;/em&gt; con OIDC. Era buen consejo. Sigue siéndolo. Y el 11 de mayo de 2026, el gusano Shai-Hulud publicó decenas de paquetes oficiales de &lt;code&gt;@tanstack/*&lt;/code&gt; entrando exactamente por ahí&lt;/strong&gt; — no falsificando la procedencia, sino generándola de verdad desde el workflow legítimo del proyecto.&lt;/p&gt;
&lt;p&gt;Ese matiz es el artículo entero. Un paquete malicioso con una firma Sigstore auténtica, emitida por Fulcio y anotada en Rekor, con el contexto del repositorio real detrás. Todos los indicadores que te habían enseñado a mirar decían «bien».&lt;/p&gt;
&lt;p&gt;Esto no significa que el trusted publishing sea inútil ni que haya que volver a los tokens. Significa algo más incómodo: &lt;strong&gt;es una capa, no un perímetro&lt;/strong&gt;, y llevamos un año vendiéndolo como si fuera lo segundo. Aquí va qué protege de verdad, qué deja fuera, y las cuatro medidas que sí habrían frenado los ataques de los últimos doce meses.&lt;/p&gt;
&lt;h2 id=&quot;la-cronología-con-fechas&quot;&gt;La cronología, con fechas&lt;/h2&gt;
&lt;p&gt;Conviene tenerla delante porque el ritmo importa más que cualquier incidente suelto:&lt;/p&gt;

























































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Fecha&lt;/th&gt;&lt;th&gt;Qué pasó&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;jul 2025&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Trusted publishing con OIDC&lt;/strong&gt; pasa a disponibilidad general en npm&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;26 ago 2025&lt;/td&gt;&lt;td&gt;Ataque &lt;strong&gt;S1ngularity&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;8 sep 2025&lt;/td&gt;&lt;td&gt;Phishing contra &lt;code&gt;debug&lt;/code&gt;, &lt;code&gt;chalk&lt;/code&gt; y 16 utilidades más — &lt;strong&gt;vivas dos horas&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;15 sep 2025&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Shai-Hulud oleada 1&lt;/strong&gt;: más de 700 paquetes comprometidos&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;23 nov 2025&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Shai-Hulud 2.0&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;9 dic 2025&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;npm &lt;strong&gt;revoca permanentemente todos los tokens clásicos&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;feb 2026&lt;/td&gt;&lt;td&gt;npm CLI &lt;strong&gt;11.10.0&lt;/strong&gt; estrena &lt;code&gt;min-release-age&lt;/code&gt; y configuración OIDC en bloque&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;30 mar 2026&lt;/td&gt;&lt;td&gt;&lt;strong&gt;&lt;code&gt;axios@1.14.1&lt;/code&gt;&lt;/strong&gt; comprometido — 100 M de descargas semanales&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;11 may 2026&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Shai-Hulud contra TanStack&lt;/strong&gt;, entrando por el propio trusted publishing&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;14 may 2026&lt;/td&gt;&lt;td&gt;&lt;code&gt;node-ipc&lt;/code&gt;: tres versiones maliciosas con un robacredenciales de 80 KB&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1 jun 2026&lt;/td&gt;&lt;td&gt;32 paquetes de &lt;code&gt;@redhat-cloud-services&lt;/code&gt;, carga «Miasma»&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ago 2026&lt;/td&gt;&lt;td&gt;Nueva oleada: &lt;code&gt;keyv&lt;/code&gt; y más de 400 paquetes&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Diez meses, ocho campañas. &lt;strong&gt;Esto ya no es una serie de incidentes: es el estado normal del registro.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;cómo-entró-por-la-puerta-buena&quot;&gt;Cómo entró por la puerta buena&lt;/h2&gt;
&lt;p&gt;Vale la pena entender el ataque de TanStack paso a paso, porque cada eslabón desmonta una suposición distinta. La &lt;a href=&quot;https://www.upwind.io/feed/shai-hulud-tanstack-supply-chain-worm&quot;&gt;reconstrucción de Upwind&lt;/a&gt; lo detalla así:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Una dependencia «dropper»&lt;/strong&gt; entra en el &lt;code&gt;package.json&lt;/code&gt; y se ejecuta durante la instalación, vía los &lt;em&gt;lifecycle hooks&lt;/em&gt; de npm. Nada exótico: es la funcionalidad documentada.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El dropper descarga y ejecuta el runtime de Bun.&lt;/strong&gt; Con eso no depende del Node instalado y esquiva la monitorización que vigila ejecuciones de Node.&lt;/li&gt;
&lt;li&gt;Bajo Bun corre &lt;strong&gt;una carga de 2,3 MB muy ofuscada&lt;/strong&gt; (&lt;code&gt;router_init.js&lt;/code&gt;) que se demoniza y sigue viva después de que la instalación termine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Barrido de credenciales&lt;/strong&gt;: sistema de archivos, APIs de nube, y —esta es la buena— &lt;strong&gt;memoria del runner de GitHub Actions leyendo &lt;code&gt;/proc/*/mem&lt;/code&gt;&lt;/strong&gt;. Objetivo: claves de AWS, tokens de npm, cuentas de servicio de Kubernetes, configuraciones de asistentes de IA.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Abuso de OIDC&lt;/strong&gt;: en vez de necesitar un token de npm guardado en algún sitio, &lt;strong&gt;canjea el token OIDC de GitHub Actions por acceso de publicación dinámico&lt;/strong&gt;. El permiso &lt;code&gt;id-token: write&lt;/code&gt; del workflow es la llave.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Propagación&lt;/strong&gt;: con ese acceso descarga paquetes objetivo, se inyecta, &lt;strong&gt;sube la versión exactamente en +3&lt;/strong&gt; y republica.&lt;/li&gt;
&lt;li&gt;Y por el camino, &lt;strong&gt;acuña un bundle Sigstore a través de Fulcio y Rekor&lt;/strong&gt; — con lo cual el tarball malicioso sale con procedencia generada desde el contexto del workflow de confianza.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;El punto 7 es el que hay que interiorizar. &lt;strong&gt;No hubo falsificación.&lt;/strong&gt; La cadena de confianza funcionó exactamente como está diseñada; lo que estaba comprometido era el eslabón anterior, el runner de CI. La procedencia responde a «¿esto se construyó donde dice?», y la respuesta era sí. No responde a «¿lo que se construyó es lo que el mantenedor escribió?», que es la pregunta que importaba.&lt;/p&gt;
&lt;p&gt;Los indicadores publicados, por si tienes que barrer: workflows que hablen con &lt;code&gt;api.masscan[.]cloud&lt;/code&gt; o &lt;code&gt;83.142.209.194&lt;/code&gt;, y artefactos de persistencia llamados &lt;code&gt;router_init.js&lt;/code&gt;, &lt;code&gt;pgmonitor.py&lt;/code&gt; o &lt;code&gt;pgsql-monitor.service&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;entonces-sirve-el-trusted-publishing&quot;&gt;Entonces, ¿sirve el trusted publishing?&lt;/h2&gt;
&lt;p&gt;Sí, y mucho. Pero conviene tener claro el reparto, porque el consejo genérico no lo hace:&lt;/p&gt;









































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Amenaza&lt;/th&gt;&lt;th&gt;¿Lo para OIDC?&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Token de npm robado de un portátil&lt;/td&gt;&lt;td&gt;✅ Sí — no hay token que robar&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Token filtrado en un log de CI o en un &lt;code&gt;.env&lt;/code&gt;&lt;/td&gt;&lt;td&gt;✅ Sí&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Phishing al mantenedor (el caso &lt;code&gt;debug&lt;/code&gt;/&lt;code&gt;chalk&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;✅ En gran medida&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Token válido meses después de que el mantenedor se vaya&lt;/td&gt;&lt;td&gt;✅ Sí&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Runner de CI comprometido durante el build&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;⛔ &lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Dependencia maliciosa que se ejecuta en &lt;code&gt;postinstall&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;⛔ &lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Envenenamiento de la caché de CI&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;⛔ &lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cuenta de GitHub del mantenedor comprometida&lt;/td&gt;&lt;td&gt;⛔ No&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;La lectura correcta: &lt;strong&gt;OIDC elimina el secreto de larga vida, que era el vector dominante hasta 2025. No defiende el proceso que emite la publicación.&lt;/strong&gt; Y a medida que el primer vector se cierra, los ataques se mueven al segundo. Eso es exactamente lo que muestra la cronología de arriba.&lt;/p&gt;
&lt;p&gt;Un dato para calibrar cuánta gente sigue en la primera casilla: según el &lt;a href=&quot;https://www.aikido.dev/blog/shai-hulud-trusted-publishing&quot;&gt;análisis de Aikido&lt;/a&gt;, de los 51.370 paquetes más descargados &lt;strong&gt;solo 11.001 usan trusted publishing&lt;/strong&gt; — un 21,4 %, que cubre apenas el 25 % del volumen de descargas. &lt;strong&gt;Tres cuartas partes de lo que instalas cada día sigue publicándose con tokens.&lt;/strong&gt; Y la adopción se mueve a golpe de incidente: de una base de ~35 paquetes nuevos por semana se pasó a 372 la semana de Shai-Hulud 2.0, y luego la urgencia se desinfla otra vez.&lt;/p&gt;
&lt;p&gt;Así que el mensaje no es «el trusted publishing falló». Es &lt;strong&gt;«migra igualmente, y además haz estas cuatro cosas»&lt;/strong&gt;.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;las-cuatro-medidas-que-sí-habrían-parado-esto&quot;&gt;Las cuatro medidas que sí habrían parado esto&lt;/h2&gt;
&lt;h3 id=&quot;1-enfriamiento-de-dependencias-no-instales-lo-que-se-publicó-esta-mañana&quot;&gt;1. Enfriamiento de dependencias: no instales lo que se publicó esta mañana&lt;/h3&gt;
&lt;p&gt;Esta es, con diferencia, la de mejor relación impacto/esfuerzo, y sigue siendo la menos usada.&lt;/p&gt;
&lt;p&gt;La idea es de una simplicidad ofensiva: &lt;strong&gt;decirle al gestor de paquetes que ignore las versiones que llevan menos de N días publicadas&lt;/strong&gt;. Casi todas estas campañas se detectan y se retiran en horas — las versiones de &lt;code&gt;debug&lt;/code&gt; y &lt;code&gt;chalk&lt;/code&gt; estuvieron vivas &lt;strong&gt;dos horas&lt;/strong&gt; —, así que un retardo modesto te saca por completo de la ventana de exposición. De diez ataques analizados, &lt;a href=&quot;https://christian-schneider.net/blog/dependency-cooldowns-supply-chain-defense/&quot;&gt;ocho tuvieron ventanas de menos de una semana&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ya está soportado de forma nativa en los cuatro gestores, con la trampa de que &lt;strong&gt;las unidades no coinciden&lt;/strong&gt;:&lt;/p&gt;



































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Gestor&lt;/th&gt;&lt;th&gt;Clave&lt;/th&gt;&lt;th&gt;Unidad&lt;/th&gt;&lt;th&gt;Desde&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;npm&lt;/td&gt;&lt;td&gt;&lt;code&gt;min-release-age&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;días&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;CLI 11.10.0 (feb 2026)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;pnpm&lt;/td&gt;&lt;td&gt;&lt;code&gt;minimumReleaseAge&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;minutos&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;10.16 (sep 2025)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Yarn&lt;/td&gt;&lt;td&gt;&lt;code&gt;npmMinimalAgeGate&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;minutos&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Berry 4.10.0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Bun&lt;/td&gt;&lt;td&gt;&lt;code&gt;minimumReleaseAge&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;segundos&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;En npm, un &lt;code&gt;.npmrc&lt;/code&gt; con una línea:&lt;/p&gt;
&lt;pre class=&quot;language-ini&quot; data-language=&quot;ini&quot;&gt;&lt;code class=&quot;language-ini&quot;&gt;&lt;span class=&quot;token key attr-name&quot;&gt;min-release-age&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En pnpm, dentro de &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;, con lista de excepciones para lo que publiques tú:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; data-language=&quot;yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;minimumReleaseAge&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10080&lt;/span&gt;          &lt;span class=&quot;token comment&quot;&gt;# 7 días en minutos&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;minimumReleaseAgeExclude&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;@mi-empresa/*&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dos avisos prácticos. El primero: &lt;strong&gt;el enfriamiento se aplica en la instalación, no al sugerir actualizaciones&lt;/strong&gt;, así que tu bot de dependencias puede abrir el PR y el &lt;code&gt;npm ci&lt;/code&gt; fallar después; conviene configurar el mismo umbral en los dos sitios. El segundo: &lt;strong&gt;Bun cuenta en segundos, no en minutos&lt;/strong&gt;: siete días son &lt;code&gt;604800&lt;/code&gt;, y quien copia el &lt;code&gt;10080&lt;/code&gt; de pnpm se queda con un enfriamiento de menos de tres horas sin enterarse. Yarn sí acepta duraciones como &lt;code&gt;7d&lt;/code&gt;, y su lista de excepciones no se llama &lt;code&gt;*Exclude&lt;/code&gt; sino &lt;code&gt;npmPreapprovedPackages&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Si antes de activarlo quieres ver qué habría frenado en tu proyecto, &lt;a href=&quot;https://ortamarco.me/portfolio/dep-cooldown/&quot;&gt;dep-cooldown&lt;/a&gt;, una herramienta open source mía, lee tu lockfile de npm, pnpm, Yarn o Bun y te da la edad de cada versión resuelta, también el día en que instalaste (&lt;code&gt;--as-of&lt;/code&gt;). Y &lt;code&gt;npx dep-cooldown --config all&lt;/code&gt; imprime la clave de los cuatro gestores con la unidad ya convertida.&lt;/p&gt;
&lt;h3 id=&quot;2-separa-el-runner-que-instala-del-runner-que-publica&quot;&gt;2. Separa el runner que instala del runner que publica&lt;/h3&gt;
&lt;p&gt;Esta es la que habría cortado el ataque de TanStack en seco.&lt;/p&gt;
&lt;p&gt;El problema estructural es que en la mayoría de los workflows &lt;strong&gt;el mismo job hace &lt;code&gt;npm ci&lt;/code&gt; y &lt;code&gt;npm publish&lt;/code&gt;&lt;/strong&gt;. Eso significa que el código de terceros que se ejecuta durante la instalación vive en el mismo proceso que tiene —o puede pedir— el token OIDC.&lt;/p&gt;
&lt;p&gt;La forma correcta es partirlo, y que &lt;code&gt;id-token: write&lt;/code&gt; exista &lt;strong&gt;solo en el job que publica&lt;/strong&gt;, que no instala nada de terceros:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; data-language=&quot;yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read          &lt;span class=&quot;token comment&quot;&gt;# sin id-token aquí&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v5
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm ci &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ignore&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;scripts
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm run build
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm pack
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/upload&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;artifact@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; tarball&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;*.tgz&amp;#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token key atrule&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; build
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read
      &lt;span class=&quot;token key atrule&quot;&gt;id-token&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write         &lt;span class=&quot;token comment&quot;&gt;# el permiso vive solo aquí&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/download&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;artifact@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; tarball &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm publish &lt;span class=&quot;token important&quot;&gt;*.tgz&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;provenance
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fíjate en el &lt;code&gt;--ignore-scripts&lt;/code&gt; del paso de instalación. Los &lt;em&gt;lifecycle hooks&lt;/em&gt; son el punto 1 y el punto 2 del ataque; si tu build no los necesita —y muchos no— desactivarlos en CI es gratis. Si alguna dependencia sí los necesita, la lista de excepciones es más corta de lo que crees y merece la pena escribirla a mano.&lt;/p&gt;
&lt;h3 id=&quot;3-trata-la-caché-de-ci-como-entrada-no-confiable&quot;&gt;3. Trata la caché de CI como entrada no confiable&lt;/h3&gt;
&lt;p&gt;El vector inicial de la oleada de mayo fue &lt;strong&gt;envenenamiento de caché&lt;/strong&gt;. Es un vector poco intuitivo porque la caché parece infraestructura tuya, y no lo es: en GitHub Actions, &lt;strong&gt;una rama con menos privilegios puede escribir en una caché que luego lee la rama de release&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Lo mínimo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nunca uses &lt;code&gt;pull_request_target&lt;/code&gt; sin un límite de confianza explícito.&lt;/strong&gt; Ese disparador ejecuta con los secretos del repositorio base y con el código del PR delante; es el pie de la puerta.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claves de caché segregadas por rama o por propósito.&lt;/strong&gt; Que el workflow de release no comparta clave con el de PRs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;En el job de publicación, no restaures caché en absoluto.&lt;/strong&gt; Ese job tiene que ser lo más aburrido posible: descarga un artefacto, lo sube. Nada más.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;4-deja-de-leer-la-insignia-de-procedencia-como-si-fuera-un-veredicto&quot;&gt;4. Deja de leer la insignia de procedencia como si fuera un veredicto&lt;/h3&gt;
&lt;p&gt;La procedencia sigue siendo útil: te dice &lt;strong&gt;dónde&lt;/strong&gt; se construyó el paquete. Después de mayo, lo que no puedes hacer es leerla como «esto es seguro».&lt;/p&gt;
&lt;p&gt;En la práctica esto significa que la revisión de dependencias no se delega a un badge. Lo que sí funciona:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fija con &lt;code&gt;package-lock.json&lt;/code&gt; y usa siempre &lt;code&gt;npm ci&lt;/code&gt; en CI&lt;/strong&gt;, nunca &lt;code&gt;npm install&lt;/code&gt;. Es la diferencia entre reproducir un árbol y resolver uno nuevo cada vez.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vigila los saltos de versión raros.&lt;/strong&gt; El gusano incrementaba la versión &lt;strong&gt;exactamente en +3&lt;/strong&gt;. Un patch que salta tres números sin changelog es una señal barata y sorprendentemente buena.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rota credenciales cuando te toque una oleada&lt;/strong&gt;, y rota todo: npm, GitHub, secretos de Actions, AWS, Vault, Kubernetes, SSH. Las cargas de estas campañas barren el runner entero, no solo el token de publicar.&lt;/li&gt;
&lt;/ul&gt;
&lt;aside class=&quot;not-prose my-8 flex flex-wrap items-center gap-4 rounded-2xl border border-neutral-100 bg-white p-5 transition-all duration-200 hover:shadow-[5px_5px_rgba(0,98,90,0.3),10px_10px_rgba(0,98,90,0.2),15px_15px_rgba(0,98,90,0.1)] dark:border-zinc-800 dark:bg-zinc-900/40&quot; style=&quot;border-left:4px solid #3b82f6&quot;&gt; &lt;span class=&quot;grid size-12 shrink-0 place-items-center rounded-xl&quot; style=&quot;background:#3b82f61a;color:#3b82f6&quot;&gt;  &lt;/span&gt; &lt;div class=&quot;flex min-w-0 flex-1 flex-col gap-0.5&quot;&gt; &lt;span class=&quot;text-xs font-semibold tracking-wide text-zinc-500 uppercase dark:text-zinc-400&quot;&gt; Herramienta gratuita &lt;/span&gt; &lt;span class=&quot;text-lg leading-snug font-bold text-blacktext dark:text-mint-50&quot;&gt; Decodificador y Verificador de JWT &lt;/span&gt; &lt;span class=&quot;text-sm text-pretty text-zinc-600 dark:text-zinc-400&quot;&gt; Decodifica el header y payload de un JSON Web Token y verifica su firma HS256/384/512 o RS256/384/512. &lt;/span&gt; &lt;/div&gt; &lt;a href=&quot;https://ortamarco.me/herramientas/decodificador-jwt/&quot; data-umami-event=&quot;tool_callout_click&quot; data-umami-event-tool=&quot;decodificador-jwt&quot; class=&quot;ml-auto shrink-0 rounded-xl bg-mint-600 px-4 py-2 text-sm! font-semibold text-white! no-underline! transition-colors hover:bg-mint-700 hover:text-white!&quot;&gt; Abrir herramienta → &lt;/a&gt; &lt;/aside&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;y-lo-que-npm-cambió-por-su-cuenta&quot;&gt;Y lo que npm cambió por su cuenta&lt;/h2&gt;
&lt;p&gt;Parte del trabajo ya te lo hicieron, y conviene saberlo porque afecta a scripts que quizá tengas guardados:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Los tokens clásicos están revocados desde el 9 de diciembre de 2025.&lt;/strong&gt; No deprecados: revocados. Si un pipeline viejo tuyo dejó de publicar en esa fecha, esta es la razón.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;npm login&lt;/code&gt; ya no te da un token de larga vida&lt;/strong&gt;, sino una sesión de &lt;strong&gt;dos horas&lt;/strong&gt; que caduca sola y exige 2FA para publicar.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Los tokens granulares de escritura caducan como máximo a los 90 días.&lt;/strong&gt; Ya no existe el token eterno, ni pidiéndolo.&lt;/li&gt;
&lt;li&gt;Y desde npm CLI 11.10.0 puedes &lt;strong&gt;configurar OIDC en bloque&lt;/strong&gt; para muchos paquetes a la vez, que era la fricción real de migrar una organización con cincuenta paquetes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Si publicas paquetes, la migración a trusted publishing son literalmente cuatro líneas de YAML —el &lt;code&gt;id-token: write&lt;/code&gt; y el &lt;code&gt;--provenance&lt;/code&gt; del ejemplo de arriba— más marcar el publicador de confianza en la página del paquete. &lt;strong&gt;Hazlo.&lt;/strong&gt; Pero hazlo sabiendo que acabas de cerrar el vector de 2025, no el de 2026.&lt;/p&gt;
&lt;h2 id=&quot;lo-que-yo-haría-este-mes-en-orden&quot;&gt;Lo que yo haría este mes, en orden&lt;/h2&gt;
&lt;p&gt;Si tienes media hora, en este orden por rendimiento decreciente:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pon &lt;code&gt;min-release-age=7&lt;/code&gt;&lt;/strong&gt; (o el equivalente de tu gestor) en cada repositorio. Cinco minutos, corta la mayoría de las campañas conocidas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cambia &lt;code&gt;npm install&lt;/code&gt; por &lt;code&gt;npm ci --ignore-scripts&lt;/code&gt;&lt;/strong&gt; en todos tus workflows de CI que no necesiten hooks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Si publicas paquetes, parte el workflow&lt;/strong&gt; en build y publish, con &lt;code&gt;id-token: write&lt;/code&gt; solo en el segundo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migra a trusted publishing&lt;/strong&gt; lo que aún publique con token granular.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audita los &lt;code&gt;pull_request_target&lt;/code&gt;&lt;/strong&gt; que tengas y las claves de caché compartidas entre ramas.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Ninguna de las cinco es cara. Y la incómoda de aceptar: &lt;strong&gt;la primera, que es la más eficaz de todas, no requiere confiar en nadie ni entender ningún ataque.&lt;/strong&gt; Solo esperar una semana.&lt;/p&gt;
&lt;p&gt;Para seguir:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/vibe-coding-seguro-evitar-vulnerabilidades-codigo-ia/&quot;&gt;Vibe coding seguro: evitar vulnerabilidades en el código de IA&lt;/a&gt; — el otro frente por el que entra código que nadie revisó.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/guia-completa-docker-2026/&quot;&gt;Guía completa de Docker en 2026&lt;/a&gt; — aislar el build es la mitad de la defensa de este artículo.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/herramientas-auditar-seguridad-dominio/&quot;&gt;Herramientas para auditar la seguridad de un dominio&lt;/a&gt; — el equivalente de esta revisión, pero hacia fuera.&lt;/li&gt;
&lt;/ul&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿El trusted publishing con OIDC sigue mereciendo la pena después del ataque de mayo de 2026? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Sí, y hay que migrar igualmente. OIDC elimina por completo el token de larga vida, que fue el vector dominante hasta 2025: no hay nada que robar de un portátil, de un log de CI ni de un mantenedor mediante phishing. Lo que no cubre es el proceso que emite la publicación: si el runner de CI está comprometido durante el build, el atacante canjea el token OIDC igual que lo haría el workflow legítimo. Es una capa, no un perímetro. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cómo pudo el gusano Shai-Hulud generar procedencia Sigstore válida? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Porque no la falsificó: la generó de verdad. La carga maliciosa se ejecutaba dentro del propio runner de GitHub Actions del proyecto, pidió un token OIDC con el permiso id-token: write del workflow, lo canjeó por acceso de publicación en npm, y acuñó un bundle Sigstore a través de Fulcio y Rekor desde ese contexto de confianza. La procedencia responde a «¿esto se construyó donde dice?», y la respuesta era sí. No responde a «¿lo construido es lo que escribió el mantenedor?». &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué es el enfriamiento de dependencias y cómo se configura? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Es decirle al gestor de paquetes que ignore las versiones publicadas hace menos de N días, para no instalar nunca una versión maliciosa dentro de su ventana de vida (que suele ser de horas). En npm es min-release-age en el .npmrc y se mide en días (desde el CLI 11.10.0, febrero de 2026); en pnpm es minimumReleaseAge y se mide en minutos (desde 10.16); en Yarn es npmMinimalAgeGate en minutos y en Bun minimumReleaseAge en segundos. Un umbral de siete días habría bloqueado ocho de los diez últimos ataques analizados. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Por qué hay que separar el job que instala del que publica en CI? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Porque si el mismo job hace npm ci y npm publish, el código de terceros que se ejecuta durante la instalación vive en el mismo proceso que tiene acceso al token OIDC. Así entró el ataque contra TanStack: una dependencia maliciosa se ejecutó en un lifecycle hook, leyó la memoria del runner y canjeó el token. La solución es que id-token: write exista solo en un job de publicación que no instale nada de terceros y se limite a subir un artefacto ya construido. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué porcentaje de paquetes de npm usa ya trusted publishing? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Poco: de los 51.370 paquetes más descargados, 11.001 lo usan — un 21,4 %, que cubre alrededor del 25 % del volumen total de descargas. Es decir, tres cuartas partes de lo que se instala a diario sigue publicándose con tokens. La adopción sube a golpe de incidente (372 paquetes nuevos la semana de Shai-Hulud 2.0, frente a una base de unos 35 semanales) y vuelve a caer cuando pasa la urgencia. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Siguen funcionando los tokens clásicos de npm? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No. npm revocó permanentemente todos los tokens clásicos el 9 de diciembre de 2025. Desde entonces npm login entrega una sesión de dos horas en vez de un token de larga vida, y los tokens granulares de escritura caducan como máximo a los 90 días. Si un pipeline antiguo dejó de publicar en esas fechas, la causa es esa. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Seguridad</category><category>Seguridad</category><category>npm</category><category>Cadena de suministro</category><category>JavaScript</category><category>DevSecOps</category><category>CI/CD</category><author>Marco Orta</author></item><item><title>Ollama con GPU AMD: los cinco fallos que te dejan en CPU sin avisar</title><link>https://ortamarco.me/blog/ollama-gpu-amd-falla-en-silencio/</link><guid isPermaLink="true">https://ortamarco.me/blog/ollama-gpu-amd-falla-en-silencio/</guid><description>Ollama no da error cuando pierde la GPU: sigue respondiendo, ocho veces más lento. Los cinco modos de fallo silencioso medidos en una Radeon RX 7900 XTX, con los números, lo que no los arregla y cómo verificarlo en treinta segundos.</description><pubDate>Tue, 08 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;El peor fallo de Ollama con una GPU AMD no es el que te tira el servidor. Es el que no te tira nada: Ollama arranca, responde, contesta bien — y lo hace en CPU, a 15,89 tokens por segundo en vez de 132,48.&lt;/strong&gt; Un 8,3x de penalización que no aparece en ningún log salvo que lo busques, porque para Ollama eso no es un error: es un &lt;em&gt;fallback&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Este artículo es el resumen de tres semanas midiendo una &lt;strong&gt;Radeon RX 7900 XTX de 24 GB&lt;/strong&gt; sobre un Ryzen 9 7950X3D, en Windows, en Linux nativo y dentro de WSL2. No es una guía de instalación —de esas hay muchas— sino el inventario de &lt;strong&gt;las cinco formas en que este montaje se rompe sin decírtelo&lt;/strong&gt;, con los números de cada una y con lo que probé que &lt;em&gt;no&lt;/em&gt; las arregla, que suele ser más útil.&lt;/p&gt;
&lt;p&gt;Los tok/s de aquí son de una tarjeta concreta con un entorno concreto. Lo que sí se traslada tal cual son los modos de fallo y el método para cazarlos.&lt;/p&gt;
&lt;h2 id=&quot;antes-de-nada-la-comprobación-de-treinta-segundos&quot;&gt;Antes de nada: la comprobación de treinta segundos&lt;/h2&gt;
&lt;p&gt;Si te llevas una sola cosa del artículo, que sea esta. Ollama &lt;strong&gt;sondea la GPU una única vez, al arrancar, y no vuelve a mirar&lt;/strong&gt;. Así que hay dos preguntas que responder siempre, y ninguna se contesta mirando si el chat va «rápido»:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. ¿Qué backend declaró el servidor al arrancar?&lt;/strong&gt; Está en el log de arranque (&lt;code&gt;%LOCALAPPDATA%\Ollama\server.log&lt;/code&gt; en Windows, &lt;code&gt;journalctl -u ollama&lt;/code&gt; en Linux):&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;library=Vulkan compute=0.0 name=Vulkan0 description=&amp;quot;AMD Radeon RX 7900 XTX&amp;quot;
libdirs=ollama,vulkan  total=&amp;quot;24.0 GiB&amp;quot; available=&amp;quot;23.2 GiB&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lo que buscas es &lt;code&gt;library=&lt;/code&gt;. Si dice &lt;code&gt;cpu&lt;/code&gt;, ya está: no hay GPU, y no te lo va a decir de otra forma.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. ¿El modelo entró entero?&lt;/strong&gt; &lt;code&gt;ollama ps&lt;/code&gt; te da el reparto:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;ollama &lt;span class=&quot;token function&quot;&gt;ps&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# NAME              SIZE     PROCESSOR&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# gpt-oss:20b       16 GB    100% GPU&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cualquier cosa que no sea &lt;strong&gt;100 % GPU&lt;/strong&gt; es un modelo partido entre GPU y CPU, y eso no da error tampoco.&lt;/p&gt;
&lt;p&gt;Un detalle que me costó un verificador entero: &lt;strong&gt;no exijas &lt;code&gt;compute=gfx1100&lt;/code&gt; ni un &lt;code&gt;pci_id&lt;/code&gt;&lt;/strong&gt;. El backend Vulkan reporta &lt;code&gt;compute=0.0&lt;/code&gt; y &lt;code&gt;pci_id=&amp;quot;&amp;quot;&lt;/code&gt; donde ROCm daba los valores reales, así que un script que compruebe esos campos suspende un sistema perfectamente sano. Lee &lt;code&gt;library=&lt;/code&gt; y &lt;code&gt;libdirs=&lt;/code&gt;, y nada más.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;fallo-1-la-gpu-integrada-del-procesador-se-lleva-el-trabajo&quot;&gt;Fallo 1: la GPU integrada del procesador se lleva el trabajo&lt;/h2&gt;
&lt;p&gt;Este es el más traicionero porque el hardware culpable no es el que estás mirando.&lt;/p&gt;
&lt;p&gt;Si tienes un Ryzen de escritorio con gráficos integrados —los 7000 en adelante los llevan casi todos— tu máquina tiene &lt;strong&gt;dos GPU AMD&lt;/strong&gt;, no una. Y la enumeración de ROCm las ordena como le da la gana:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;ROCm0: AMD Radeon(TM) Graphics (36694 MiB)   &amp;lt;- la integrada
ROCm1: AMD Radeon RX 7900 XTX (24560 MiB)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La integrada sale primera y reporta 36 GB de VRAM (que no tiene: es RAM del sistema). Los binarios de &lt;code&gt;llama.cpp&lt;/code&gt; la eligen, cargan kernels compilados para &lt;code&gt;gfx1100&lt;/code&gt; y revientan:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;ggml-cuda.cu:106: ROCm error
ROCm error: device kernel image is invalid
  current device: 1, in function ggml_cuda_kernel_launch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Lo que no lo arregla&lt;/strong&gt; —y lo probé todo antes de rendirme:&lt;/p&gt;

























&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Intento&lt;/th&gt;&lt;th&gt;Resultado&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--device ROCm1&lt;/code&gt; (elegirla por nombre)&lt;/td&gt;&lt;td&gt;⛔ mismo error&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;HIP_VISIBLE_DEVICES=0&lt;/code&gt; y &lt;code&gt;=1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;⛔ mismo error o «no usable GPU found»&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;ROCR_VISIBLE_DEVICES=1&lt;/code&gt; + &lt;code&gt;--device ROCm0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;⛔ igual&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;ROCBLAS_TENSILE_LIBPATH&lt;/code&gt; al directorio de kernels&lt;/td&gt;&lt;td&gt;⛔ igual (los de &lt;code&gt;gfx1100&lt;/code&gt; &lt;strong&gt;sí estaban&lt;/strong&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Lo que sí lo arregla: desactivar la integrada en la BIOS.&lt;/strong&gt; Con un solo dispositivo no hay nada que elegir mal y arranca a la primera, sin banderas:&lt;/p&gt;

























&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;antes&lt;/th&gt;&lt;th&gt;después&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--list-devices&lt;/code&gt;&lt;/td&gt;&lt;td&gt;integrada + dedicada&lt;/td&gt;&lt;td&gt;&lt;strong&gt;solo la RX 7900 XTX&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;llama-server&lt;/code&gt; suelto&lt;/td&gt;&lt;td&gt;⛔ &lt;code&gt;kernel image is invalid&lt;/code&gt;&lt;/td&gt;&lt;td&gt;✅ &lt;strong&gt;136 tok/s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Ollama&lt;/td&gt;&lt;td&gt;118 tok/s&lt;/td&gt;&lt;td&gt;&lt;strong&gt;132,5 tok/s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;El miedo razonable era perder VRAM al mover el monitor a la dedicada. &lt;strong&gt;Medido: cuesta 0,2 GiB.&lt;/strong&gt; Con la pantalla a 3440×1440 y 165 Hz colgando de la tarjeta grande, Ollama sigue reportando &lt;code&gt;total 24.0 GiB / available 23.8 GiB&lt;/code&gt;. El intercambio sale gratis.&lt;/p&gt;
&lt;p&gt;Y una nota para no volverse loco: &lt;strong&gt;bajo Ollama esto no pasa&lt;/strong&gt;, porque Ollama enmascara la integrada por su cuenta y para él la dedicada es &lt;code&gt;ROCm0&lt;/code&gt;. Por eso puedes tener a Ollama funcionando y a &lt;code&gt;llama-server&lt;/code&gt; fallando en la misma máquina con los mismos pesos. No estás loco: son dos enumeraciones distintas.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;fallo-2-actualizar-cualquier-cosa-devuelve-el-backend-al-principio&quot;&gt;Fallo 2: actualizar cualquier cosa devuelve el backend al principio&lt;/h2&gt;
&lt;p&gt;Ya lo he dicho arriba, pero merece su propia sección porque es el que más caro sale: &lt;strong&gt;Ollama sondea la GPU al arrancar y punto&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Tras una reinstalación de drivers de AMD, el servicio se quedó sirviendo así:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;inference compute  id=cpu  library=cpu
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sin un error. Sin un aviso. &lt;strong&gt;15,89 tok/s contra 132,48.&lt;/strong&gt; Reiniciar Ollama lo devolvió a &lt;code&gt;library=ROCm compute=gfx1100&lt;/code&gt; y a los 132.&lt;/p&gt;
&lt;p&gt;Hay una segunda versión de este mismo fallo, y es peor porque no la disparas tú: &lt;strong&gt;cada actualización de Ollama reinstala el bundle de ROCm&lt;/strong&gt;. Si habías cambiado a Vulkan a propósito, el update te devuelve a ROCm en silencio. Nadie te avisa; simplemente el número que llevabas semanas midiendo deja de ser el mismo.&lt;/p&gt;
&lt;p&gt;La regla operativa que salió de esto: &lt;strong&gt;pasa el comprobador después de tocar drivers, BIOS o versión de Ollama.&lt;/strong&gt; No después de que algo vaya mal — después de tocar, siempre, aunque no vaya mal.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;fallo-3-dentro-de-wsl2-no-hay-gpu-para-ollama-y-no-la-va-a-haber&quot;&gt;Fallo 3: dentro de WSL2 no hay GPU para Ollama, y no la va a haber&lt;/h2&gt;
&lt;p&gt;Esta es la que más tiempo me comió, así que va con detalle para que no se lo comas tú.&lt;/p&gt;
&lt;p&gt;La pregunta era razonable: si el Linux nativo le saca a Windows entre un 6,7 y un 19,5 % en tok/s, ¿me lo llevo dentro de WSL y me ahorro reiniciar? &lt;strong&gt;La respuesta es que Ollama en WSL no usa la GPU en absoluto.&lt;/strong&gt; Y no es un problema de drivers.&lt;/p&gt;
&lt;p&gt;Lo medido, en orden:&lt;/p&gt;





































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Comprobación&lt;/th&gt;&lt;th&gt;Resultado&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;/dev/kfd&lt;/code&gt; y &lt;code&gt;/sys/class/kfd&lt;/code&gt;&lt;/td&gt;&lt;td&gt;⛔ &lt;strong&gt;no existen&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;/dev/dxg&lt;/code&gt;&lt;/td&gt;&lt;td&gt;✅ existe (la vía de WSL)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;¿HIP computa en WSL?&lt;/td&gt;&lt;td&gt;✅ &lt;strong&gt;SÍ&lt;/strong&gt; — sonda con &lt;code&gt;hipcc&lt;/code&gt;: &lt;code&gt;hipGetDeviceCount rc=0 n=1&lt;/code&gt;, &lt;code&gt;gfx1100&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Ollama detecta GPU&lt;/td&gt;&lt;td&gt;⛔ &lt;code&gt;library=cpu&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;HSA_ENABLE_DXG_DETECTION=1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;⛔ no cambia nada&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;llama-server --list-devices&lt;/code&gt; con ROCm&lt;/td&gt;&lt;td&gt;⛔ &lt;code&gt;Available devices: (none)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;llama-server --list-devices&lt;/code&gt; con Vulkan&lt;/td&gt;&lt;td&gt;⛔ &lt;code&gt;Available devices: (none)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;La paradoja que lo explica:&lt;/strong&gt; HIP &lt;em&gt;sí&lt;/em&gt; funciona en WSL, a través de la runtime HSA que habla con &lt;code&gt;/dev/dxg&lt;/code&gt;. Pero &lt;strong&gt;Ollama enumera GPUs leyendo la topología KFD de &lt;code&gt;sysfs&lt;/code&gt;&lt;/strong&gt;, que WSL no expone. No falta el driver — Ollama busca por una puerta que en WSL no existe.&lt;/p&gt;
&lt;p&gt;Con &lt;code&gt;OLLAMA_DEBUG=1&lt;/code&gt; aparece la causa real, que de otro modo queda enterrada:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;failure during llama-server GPU discovery
error=&amp;quot;llama-server --list-devices failed: signal: segmentation fault (core dumped)&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Circula por ahí una receta —el &lt;a href=&quot;https://github.com/ollama/ollama/issues/16551&quot;&gt;issue 16551 de Ollama&lt;/a&gt;, que es el caso idéntico: misma tarjeta, mismo WSL2, misma ROCm 7.2— cuyo autor reporta &lt;code&gt;library=ROCm compute=gfx1100&lt;/code&gt; al 100 % de GPU colocando el bundle de ROCm y poniendo &lt;code&gt;HSA_ENABLE_DXG_DETECTION=1&lt;/code&gt;. &lt;strong&gt;Probado aquí, no reproduce.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;¿Y el backend Vulkan? En WSL exigiría el ICD &lt;strong&gt;&lt;code&gt;dzn&lt;/code&gt;&lt;/strong&gt; (Vulkan sobre D3D12), que Ubuntu no empaqueta. Se podría compilar Mesa con &lt;code&gt;-Dvulkan-drivers=microsoft-experimental&lt;/code&gt; y ver qué pasa, pero es trabajo grande con pago incierto.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Conclusión práctica: para Ollama, WSL está descartado.&lt;/strong&gt; Las dos salidas son el Ollama de Windows (que sí usa la GPU y es lo que hay hoy) o arrancar en Linux nativo. Y si desarrollas dentro de WSL como yo, la buena noticia es que &lt;strong&gt;no hace falta mover Ollama&lt;/strong&gt;: con WSL2 en &lt;code&gt;networkingMode=mirrored&lt;/code&gt;, &lt;code&gt;localhost:11434&lt;/code&gt; desde WSL alcanza el Ollama de Windows sin configurar nada.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;fallo-4-el-modelo-no-cabe-se-parte-y-sigue-contestando&quot;&gt;Fallo 4: el modelo no cabe, se parte, y sigue contestando&lt;/h2&gt;
&lt;p&gt;Cuando pides más contexto del que entra en la VRAM, Ollama no te dice que no. &lt;strong&gt;Reparte el modelo entre GPU y CPU y sigue.&lt;/strong&gt; El síntoma es &lt;code&gt;size_vram &amp;lt; size&lt;/code&gt; en &lt;code&gt;/api/ps&lt;/code&gt;, y nada más.&lt;/p&gt;
&lt;p&gt;Medido con &lt;code&gt;qwen3.8&lt;/code&gt; (27,3B) en la 7900 XTX, con &lt;code&gt;OLLAMA_FLASH_ATTENTION=1&lt;/code&gt; y la caché KV en &lt;code&gt;q8_0&lt;/code&gt;:&lt;/p&gt;





















&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align:right&quot;&gt;&lt;code&gt;num_ctx&lt;/code&gt; pedido&lt;/th&gt;&lt;th&gt;En GPU&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;text-align:right&quot;&gt;262.144&lt;/td&gt;&lt;td&gt;⛔ &lt;strong&gt;63,4 %&lt;/strong&gt; — un tercio en CPU&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style=&quot;text-align:right&quot;&gt;131.072&lt;/td&gt;&lt;td&gt;⛔ 92,4 %&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style=&quot;text-align:right&quot;&gt;122.880&lt;/td&gt;&lt;td&gt;✅ 100 % (con &lt;strong&gt;0,6 GiB&lt;/strong&gt; de margen)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Con 0,6 GiB libres, cualquier cosa que abras en el escritorio lo tira. Ese techo es de esa tarde y de esa pantalla, no una constante del modelo.&lt;/p&gt;
&lt;p&gt;Y aquí va el aviso que casi me cuesta una decisión mal tomada: &lt;strong&gt;&lt;code&gt;ollama ps&lt;/code&gt; subestima la VRAM real entre 4,3 y 6,8 GiB.&lt;/strong&gt; Cruzado contra el contador del sistema, con cuatro modelos:&lt;/p&gt;






























&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Modelo&lt;/th&gt;&lt;th style=&quot;text-align:right&quot;&gt;&lt;code&gt;ollama ps&lt;/code&gt;&lt;/th&gt;&lt;th style=&quot;text-align:right&quot;&gt;VRAM real&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;gpt-oss:20b&lt;/code&gt;&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;12,33 GiB&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;&lt;strong&gt;16,59 GiB&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;muse-glimmer&lt;/code&gt; (27,9B)&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;15,49 GiB&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;&lt;strong&gt;20,77 GiB&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;gemma4&lt;/code&gt; (25,8B)&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;17,24 GiB&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;&lt;strong&gt;22,37 GiB&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;qwen3.8&lt;/code&gt; (27,3B)&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;16,61 GiB&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;&lt;strong&gt;23,36 GiB&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Para decidir sobre memoria, la columna de &lt;code&gt;ps&lt;/code&gt; no sirve.&lt;/strong&gt; Solo sirve para saber si Ollama repartió a CPU. Si estás calculando si un modelo te cabe, mide la VRAM del sistema, no la que Ollama declara.&lt;/p&gt;
&lt;aside class=&quot;not-prose my-8 flex flex-wrap items-center gap-4 rounded-2xl border border-neutral-100 bg-white p-5 transition-all duration-200 hover:shadow-[5px_5px_rgba(0,98,90,0.3),10px_10px_rgba(0,98,90,0.2),15px_15px_rgba(0,98,90,0.1)] dark:border-zinc-800 dark:bg-zinc-900/40&quot; style=&quot;border-left:4px solid #a855f7&quot;&gt; &lt;span class=&quot;grid size-12 shrink-0 place-items-center rounded-xl&quot; style=&quot;background:#a855f71a;color:#a855f7&quot;&gt;  &lt;/span&gt; &lt;div class=&quot;flex min-w-0 flex-1 flex-col gap-0.5&quot;&gt; &lt;span class=&quot;text-xs font-semibold tracking-wide text-zinc-500 uppercase dark:text-zinc-400&quot;&gt; Herramienta gratuita &lt;/span&gt; &lt;span class=&quot;text-lg leading-snug font-bold text-blacktext dark:text-mint-50&quot;&gt; Contador de Tokens y Calculadora de Costos de IA &lt;/span&gt; &lt;span class=&quot;text-sm text-pretty text-zinc-600 dark:text-zinc-400&quot;&gt; Cuenta los tokens de tu texto o prompt y calcula cuánto costará en GPT-5, Claude, Gemini, Kimi K3, Grok y DeepSeek. Conteo exacto de OpenAI y costos por llamada y al mes, con caché de prompt. &lt;/span&gt; &lt;/div&gt; &lt;a href=&quot;https://ortamarco.me/herramientas/contador-tokens/&quot; data-umami-event=&quot;tool_callout_click&quot; data-umami-event-tool=&quot;contador-tokens&quot; class=&quot;ml-auto shrink-0 rounded-xl bg-mint-600 px-4 py-2 text-sm! font-semibold text-white! no-underline! transition-colors hover:bg-mint-700 hover:text-white!&quot;&gt; Abrir herramienta → &lt;/a&gt; &lt;/aside&gt;
&lt;h2 id=&quot;fallo-5-te-recorta-el-contexto-y-te-deja-creer-que-lo-tienes&quot;&gt;Fallo 5: te recorta el contexto y te deja creer que lo tienes&lt;/h2&gt;
&lt;p&gt;Variante fina del anterior. Si a &lt;code&gt;muse-glimmer&lt;/code&gt; o a &lt;code&gt;gpt-oss&lt;/code&gt; les pides &lt;code&gt;num_ctx: 262144&lt;/code&gt;, &lt;strong&gt;cargan tan felices&lt;/strong&gt; — y el &lt;code&gt;context_length&lt;/code&gt; que devuelve &lt;code&gt;/api/ps&lt;/code&gt; dice &lt;code&gt;131072&lt;/code&gt;. Lo que pediste no es lo que tienes.&lt;/p&gt;
&lt;p&gt;La causa de fondo es que estos modelos tienen un techo de arquitectura por debajo del de la tarjeta. Y explica algo contraintuitivo que sale al medir: &lt;strong&gt;el tamaño en disco no predice el contexto máximo&lt;/strong&gt;. Lo que decide es si el modelo usa &lt;strong&gt;ventana deslizante&lt;/strong&gt; (SWA) o KV denso:&lt;/p&gt;






























&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Modelo&lt;/th&gt;&lt;th&gt;SWA&lt;/th&gt;&lt;th style=&quot;text-align:right&quot;&gt;Crecimiento de KV de 16K a 128K&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;muse-glimmer&lt;/code&gt;&lt;/td&gt;&lt;td&gt;2048&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;&lt;strong&gt;~0&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;gpt-oss&lt;/code&gt;&lt;/td&gt;&lt;td&gt;128&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;+0,36 GiB&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;gemma4&lt;/code&gt;&lt;/td&gt;&lt;td&gt;1024&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;+0,40 GiB&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;qwen3.8&lt;/code&gt;&lt;/td&gt;&lt;td&gt;— (KV denso)&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;&lt;strong&gt;+1,35 GiB&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Tres de los cuatro tocan su techo de arquitectura y ni se enteran de la VRAM. El único que paga contexto de verdad es el único que se sale.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lee siempre el &lt;code&gt;context_length&lt;/code&gt; real de &lt;code&gt;/api/ps&lt;/code&gt;, no el que mandaste.&lt;/strong&gt;&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;rocm-o-vulkan-en-2026-qué-gana-cada-uno&quot;&gt;ROCm o Vulkan en 2026: qué gana cada uno&lt;/h2&gt;
&lt;p&gt;Este es el cambio de fondo del año y por eso el artículo llega ahora. Ollama incorporó &lt;a href=&quot;https://www.phoronix.com/news/ollama-Experimental-Vulkan&quot;&gt;soporte Vulkan experimental&lt;/a&gt; en la 0.12.6 y a lo largo de 2026 ha ido llegando a los binarios, con el argumento de abrir la puerta a las GPU de AMD e Intel que ROCm nunca soportó — que son la mayoría de las de consumo.&lt;/p&gt;
&lt;p&gt;En mi máquina, forzando cada backend con el mismo Ollama y los mismos pesos, &lt;strong&gt;Vulkan gana&lt;/strong&gt;:&lt;/p&gt;





















&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Modelo&lt;/th&gt;&lt;th style=&quot;text-align:right&quot;&gt;Ganancia de Vulkan sobre ROCm&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;gpt-oss:20b&lt;/code&gt;&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;&lt;strong&gt;+17,8 %&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;gemma4&lt;/code&gt;&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;&lt;strong&gt;+30,1 %&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;muse-glimmer&lt;/code&gt;&lt;/td&gt;&lt;td style=&quot;text-align:right&quot;&gt;&lt;strong&gt;+30,3 %&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Rangos disjuntos en dos rondas independientes, así que la señal es real. Y encaja con lo que reportan otros: en una RX 9070 XT, Llama 2 7B Q4_0 decodifica a &lt;a href=&quot;https://runaihome.com/blog/rdna4-vulkan-vs-rocm-local-llm-benchmark-2026/&quot;&gt;137 tok/s con Vulkan contra 101 con ROCm&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Con dos advertencias que casi nadie pone.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;La primera: el delta compara dos cosas a la vez. En Windows es ROCm 7.1 contra AMDVLK; en Linux es ROCm 7.2.4 contra RADV. Un ROCm peor infla exactamente el mismo número que un Vulkan mejor, y no están separados.&lt;/p&gt;
&lt;p&gt;La segunda es un coste que no vi venir. &lt;strong&gt;Vulkan deja 0,6 GiB menos de VRAM disponible&lt;/strong&gt;: 23,2 GiB declarados contra 23,8 con ROCm. Para tres de mis cuatro modelos da igual porque no apuraban la tarjeta. Para &lt;code&gt;qwen3.8&lt;/code&gt;, que iba justo de 0,6 GiB, se lo come entero:&lt;/p&gt;

















&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align:right&quot;&gt;&lt;code&gt;num_ctx&lt;/code&gt;&lt;/th&gt;&lt;th&gt;Con Vulkan&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;text-align:right&quot;&gt;122.880 (el techo con ROCm)&lt;/td&gt;&lt;td&gt;⛔ &lt;strong&gt;93,8 %&lt;/strong&gt; en GPU&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style=&quot;text-align:right&quot;&gt;98.304&lt;/td&gt;&lt;td&gt;✅ 100 %&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;El intercambio real de esta palanca no es velocidad contra estabilidad: es velocidad contra contexto.&lt;/strong&gt; Si tu modelo apura la tarjeta, Vulkan te cuesta ventana. Si no la apura, es gratis.&lt;/p&gt;
&lt;p&gt;Lo que sigue sin medir —ni yo ni casi nadie— es &lt;strong&gt;la estabilidad de Vulkan en sesiones de horas&lt;/strong&gt;. Para un chat de diez minutos da igual; para un agente que lleva toda la tarde en bucle, no.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;el-checklist-entero&quot;&gt;El checklist, entero&lt;/h2&gt;
&lt;p&gt;Lo que hago ahora cada vez que toco esta máquina:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Desactiva la GPU integrada en la BIOS&lt;/strong&gt; si tienes un Ryzen con gráficos. Cuesta 0,2 GiB y quita un eje entero de ambigüedad.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reinicia Ollama después de tocar drivers, BIOS o versión.&lt;/strong&gt; No es opcional: sondea una vez.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Comprueba &lt;code&gt;library=&lt;/code&gt; en el log de arranque.&lt;/strong&gt; No &lt;code&gt;compute=&lt;/code&gt;, no &lt;code&gt;pci_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Comprueba que &lt;code&gt;ollama ps&lt;/code&gt; dice 100 % GPU&lt;/strong&gt; antes de dar por buena cualquier medida.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lee el &lt;code&gt;context_length&lt;/code&gt; real de &lt;code&gt;/api/ps&lt;/code&gt;&lt;/strong&gt;, no el que enviaste.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Para decidir sobre memoria, mide la VRAM del sistema&lt;/strong&gt;, no la columna de &lt;code&gt;ollama ps&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;En WSL, no lo intentes.&lt;/strong&gt; Apunta a &lt;code&gt;localhost:11434&lt;/code&gt; contra el Ollama de Windows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cierra Chrome, Spotify y Slack antes de medir.&lt;/strong&gt; No tanto por los tok/s como por el rango: una diferencia real del 2 % se pierde si el ruido lo ensancha.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Y la que resume todas: &lt;strong&gt;un vacío no es un cero.&lt;/strong&gt; Cuando un instrumento no devuelve nada, la lectura no es «no hay»; es «no pude mirar». En esta máquina, &lt;code&gt;Get-Process | Modules&lt;/code&gt; devolvió &lt;code&gt;0&lt;/code&gt; módulos y estuve a punto de leerlo como «no hay capas cargadas». Eran 0 porque el proceso corría elevado y yo no podía verlo. Lo salvó tener un caso de control al lado —&lt;code&gt;explorer&lt;/code&gt;, mismo usuario, 389 módulos—. Sin ese contraste, el cero se lee como un dato.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;si-te-estás-planteando-el-hardware&quot;&gt;Si te estás planteando el hardware&lt;/h2&gt;
&lt;p&gt;La conclusión honesta después de todo esto: &lt;strong&gt;una Radeon de consumo corre modelos locales perfectamente bien en 2026&lt;/strong&gt;, y las cifras están al nivel de lo que se publica para NVIDIA en el mismo rango. ROCm 7.2 es la primera versión donde Ollama, LM Studio, llama.cpp y vLLM funcionan sobre Radeon sin parchear a mano.&lt;/p&gt;
&lt;p&gt;Lo que sigue costando más que en NVIDIA no son los tok/s: &lt;strong&gt;es el diagnóstico&lt;/strong&gt;. Todo lo de este artículo es tiempo que en una 4090 no habrías gastado, no porque el hardware sea peor, sino porque el camino está menos pisado y los fallos son silenciosos en vez de ruidosos.&lt;/p&gt;
&lt;p&gt;Si eso te cuadra —y a mí me cuadra, porque 24 GB de VRAM a este precio no existen en el otro lado— este artículo es el atajo por los cinco agujeros.&lt;/p&gt;
&lt;p&gt;Para seguir:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/llm-local-empresa-privacidad/&quot;&gt;IA sin mandar datos fuera: LLM local para despachos y PyMEs&lt;/a&gt; — el porqué de todo esto: qué resuelve de verdad un modelo local y qué no.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/cuanto-cuesta-api-openai-claude-gemini-2026/&quot;&gt;Cuánto cuesta la API de OpenAI, Claude y Gemini en 2026&lt;/a&gt; — el número con el que comparar antes de comprar tarjeta.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/retiran-el-modelo-de-ia-que-usa-tu-negocio/&quot;&gt;Retiran el modelo de IA que usa tu negocio&lt;/a&gt; — la razón por la que tener el modelo en casa deja de ser un capricho.&lt;/li&gt;
&lt;/ul&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cómo sé si Ollama está usando mi GPU AMD? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Con dos comprobaciones, no con una. Primero, el log de arranque del servidor (%LOCALAPPDATA%\Ollama\server.log en Windows, journalctl -u ollama en Linux) tiene que declarar library=ROCm o library=Vulkan; si dice library=cpu, no hay GPU. Segundo, ollama ps tiene que decir 100% GPU: cualquier otro valor significa que el modelo está partido entre GPU y CPU. Ollama no da error en ninguno de los dos casos, solo va más lento. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Por qué Ollama corre en CPU después de actualizar los drivers? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Porque Ollama sondea la GPU una sola vez, al arrancar, y no vuelve a mirar. Si actualizas drivers, cambias la BIOS o instalas una versión nueva mientras el servicio está en marcha, se queda con la detección vieja y pasa a CPU sin dar ningún error. Medido en una RX 7900 XTX: 15,89 tok/s en CPU contra 132,48 en GPU, un 8,3x. La solución es reiniciar Ollama y volver a comprobar la línea library= del log. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Se puede usar la GPU AMD con Ollama dentro de WSL2? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No. Comprobado en agosto de 2026 con una RX 7900 XTX y ROCm 7.2: Ollama enumera las GPU leyendo la topología KFD de sysfs, y WSL no expone /dev/kfd ni /sys/class/kfd. HIP sí computa en WSL a través de /dev/dxg, pero Ollama no busca por ahí, y HSA_ENABLE_DXG_DETECTION=1 no lo cambia. Tanto ROCm como Vulkan devuelven «Available devices: (none)». La salida práctica es apuntar a localhost:11434 contra el Ollama de Windows: con WSL2 en networkingMode=mirrored funciona sin configurar nada. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Es mejor ROCm o Vulkan para Ollama en una GPU AMD? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Depende de si tu modelo apura la VRAM. En una RX 7900 XTX, Vulkan gana entre un 17,8 % y un 30,3 % en tokens por segundo según el modelo, con rangos disjuntos en dos rondas. Pero Vulkan deja 0,6 GiB menos de VRAM disponible (23,2 frente a 23,8 GiB), así que un modelo que iba justo de contexto deja de caber. El intercambio no es velocidad contra estabilidad: es velocidad contra ventana de contexto. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Por qué falla llama.cpp con «device kernel image is invalid» en un Ryzen? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Porque la máquina tiene dos GPU AMD: la dedicada y la integrada del procesador. ROCm enumera la integrada primero, el binario la elige, y los kernels compilados para gfx1100 no valen para ese chip. Ni --device por nombre ni HIP_VISIBLE_DEVICES ni ROCR_VISIBLE_DEVICES lo arreglan. Lo único que funciona es desactivar la integrada en la BIOS, y cuesta solo 0,2 GiB de VRAM. Ollama no sufre esto porque enmascara la integrada por su cuenta. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuánta VRAM necesita de verdad un modelo en Ollama? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Más de la que dice ollama ps, que subestima entre 4,3 y 6,8 GiB. Medido con cuatro modelos en una tarjeta de 24 GB: gpt-oss:20b declara 12,33 GiB y ocupa 16,59; qwen3.8 declara 16,61 y ocupa 23,36. Para decidir si un modelo cabe hay que mirar el contador de VRAM del sistema; la columna de ollama ps solo sirve para saber si el modelo se repartió a CPU. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Tutoriales</category><category>IA</category><category>Ollama</category><category>AMD</category><category>GPU</category><category>LLM local</category><category>ROCm</category><author>Marco Orta</author></item><item><title>GPT-6 Astra para desarrolladores: el precipicio de precio que nadie puso en el titular</title><link>https://ortamarco.me/blog/gpt-6-astra-para-desarrolladores/</link><guid isPermaLink="true">https://ortamarco.me/blog/gpt-6-astra-para-desarrolladores/</guid><description>GPT-6 Astra trae una ventana de 1,05 M de tokens, pero pasar de 272K de entrada recobra la petición completa al doble. Más el acceso restringido, lo que no soporta y por qué es el destino equivocado para los modelos que mueren el 23 de octubre.</description><pubDate>Sat, 05 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;OpenAI lanzó GPT-6 Astra el 3 de septiembre de 2026 con una ventana de contexto de 1,050,000 tokens. Lo que la cobertura del lanzamiento se saltó: en el momento en que una petición pasa de 272,000 tokens de entrada, se recobra la petición &lt;em&gt;completa&lt;/em&gt; al doble de entrada y 1.5x de salida. No el excedente — todo.&lt;/strong&gt; Un token después de la línea prácticamente duplica la cuenta de esa llamada. La ventana que te venden es la ventana que te cobran al doble por usar.&lt;/p&gt;
&lt;p&gt;Esa es la primera de cinco cosas que importan si eres quien escribe el código y no quien lee la gráfica de benchmarks. Este post es la lectura del lado del desarrollador: precios, restricciones de acceso, lo que Astra explícitamente &lt;strong&gt;no&lt;/strong&gt; soporta, el parámetro que te va a romper la validación y —la que más me importa por la gente que me escribe— por qué Astra es el destino equivocado para los modelos que OpenAI apaga el 23 de octubre.&lt;/p&gt;
&lt;h2 id=&quot;qué-es-astra-de-verdad-en-una-sola-tabla&quot;&gt;Qué es Astra de verdad, en una sola tabla&lt;/h2&gt;





























































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;GPT-6 Astra&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;ID del modelo en la API&lt;/td&gt;&lt;td&gt;&lt;code&gt;gpt-6-astra&lt;/code&gt; (un solo snapshot)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Ventana de contexto&lt;/td&gt;&lt;td&gt;1,050,000 tokens&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Entrada máxima&lt;/td&gt;&lt;td&gt;922,000 tokens&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Salida máxima&lt;/td&gt;&lt;td&gt;128,000 tokens&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Corte de conocimiento&lt;/td&gt;&lt;td&gt;30 de abril de 2026&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Entrada / salida&lt;/td&gt;&lt;td&gt;&lt;strong&gt;$10 / $50&lt;/strong&gt; por millón&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Entrada en caché / escritura de caché&lt;/td&gt;&lt;td&gt;$1.00 / $12.50 por millón&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Batch y Flex&lt;/td&gt;&lt;td&gt;50 % de la tarifa estándar&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Modo Fast&lt;/td&gt;&lt;td&gt;2x estándar, hasta ~2.5x de velocidad&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Arriba de 272K de entrada&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;2x entrada y caché, 1.5x salida — sobre la petición completa&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Esfuerzo de razonamiento&lt;/td&gt;&lt;td&gt;&lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;, &lt;strong&gt;&lt;code&gt;xhigh&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;max&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Endpoints&lt;/td&gt;&lt;td&gt;Chat Completions, Responses, Batch&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;No soporta&lt;/td&gt;&lt;td&gt;Realtime, Assistants, fine-tuning, embeddings, generación de imágenes, audio, moderación, Completions legacy&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;La disponibilidad va por etapas: primero un grupo limitado de organizaciones (los programas Daybreak y Trusted Access de OpenAI), después ChatGPT Plus, Pro, Business y Enterprise “en los próximos días”, y luego la API y Amazon Bedrock. Los espacios de Enterprise lo traen &lt;strong&gt;apagado por defecto&lt;/strong&gt; — un administrador tiene que prenderlo, que va a ser el primer ticket de soporte que levante tu equipo.&lt;/p&gt;
&lt;h2 id=&quot;1-el-precipicio-de-los-272k-con-la-aritmética&quot;&gt;1. El precipicio de los 272K, con la aritmética&lt;/h2&gt;
&lt;p&gt;Esta es la parte que conviene interiorizar antes de conectar Astra a cualquier cosa que corra en bucle.&lt;/p&gt;
&lt;p&gt;El recargo no es precio marginal. No es “los tokens arriba de 272K cuestan el doble”. Según la propia página del modelo de OpenAI, los prompts arriba de 272,000 tokens de entrada se cobran a 2x las tarifas de entrada y caché y 1.5x la de salida &lt;strong&gt;sobre la petición completa&lt;/strong&gt;. Lo que produce esto:&lt;/p&gt;













































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Petición&lt;/th&gt;&lt;th&gt;Tokens de entrada&lt;/th&gt;&lt;th&gt;Tokens de salida&lt;/th&gt;&lt;th&gt;Costo entrada&lt;/th&gt;&lt;th&gt;Costo salida&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Justo debajo&lt;/td&gt;&lt;td&gt;272,000&lt;/td&gt;&lt;td&gt;8,000&lt;/td&gt;&lt;td&gt;$2.72&lt;/td&gt;&lt;td&gt;$0.40&lt;/td&gt;&lt;td&gt;&lt;strong&gt;$3.12&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Justo arriba&lt;/td&gt;&lt;td&gt;273,000&lt;/td&gt;&lt;td&gt;8,000&lt;/td&gt;&lt;td&gt;$5.46&lt;/td&gt;&lt;td&gt;$0.60&lt;/td&gt;&lt;td&gt;&lt;strong&gt;$6.06&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Entrada máxima&lt;/td&gt;&lt;td&gt;922,000&lt;/td&gt;&lt;td&gt;8,000&lt;/td&gt;&lt;td&gt;$18.44&lt;/td&gt;&lt;td&gt;$0.60&lt;/td&gt;&lt;td&gt;&lt;strong&gt;$19.04&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Entrada y salida máximas&lt;/td&gt;&lt;td&gt;922,000&lt;/td&gt;&lt;td&gt;128,000&lt;/td&gt;&lt;td&gt;$18.44&lt;/td&gt;&lt;td&gt;$9.60&lt;/td&gt;&lt;td&gt;&lt;strong&gt;$28.04&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Mil tokens extra de entrada —un archivo mediano, un par de resultados de herramienta— convierten una llamada de $3.12 en una de $6.06. &lt;strong&gt;Es un aumento del 94 % por un aumento del 0.4 % en la entrada.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ahora mete eso en un bucle de agente. Los agentes no mandan un prompt grande: acumulan. El contexto crece con cada resultado de herramienta, cada archivo leído, cada reintento. Así que un agente que arranca cada sesión cómodamente debajo de la línea va a cruzarla en silencio como a la segunda hora de una refactorización larga — y cada llamada siguiente de esa sesión se cobra del lado caro. No truena nada. No te avisa nada. Te enteras en la factura.&lt;/p&gt;
&lt;p&gt;Si alguna vez mediste lo que quema un agente de verdad, ya conoces la forma del problema: &lt;a href=&quot;https://ortamarco.me/blog/cuanto-cuesta-programar-con-agentes-de-ia/&quot;&gt;en 35 días de Claude Code registré 7,970 millones de tokens facturables&lt;/a&gt;. Aplícale una función escalón de 2x a una curva así y el número deja de ser teórico.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Qué hacer de verdad:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pon un techo duro de entrada debajo de la línea, no en la línea.&lt;/strong&gt; 260,000 deja espacio para el resultado de herramienta que no anticipaste. Imponlo en tu propio código antes de la llamada, porque la API no lo va a hacer por ti.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cuenta antes de mandar.&lt;/strong&gt; Contar tokens es barato y el precipicio no. Si tu stack todavía no mide la entrada por petición, esa es la instrumentación que va primero.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Acuérdate de que el caché de prompts está del mismo lado del precipicio.&lt;/strong&gt; La entrada en caché son $1/M normalmente y $2/M pasando 272K. El caché suaviza la pendiente, no mueve el escalón.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Usa Batch donde la latencia lo permita.&lt;/strong&gt; El 50 % de descuento aplica antes del multiplicador, así que batch con contexto largo sigue saliendo más barato que estándar con contexto largo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Antes de mandar una plantilla de prompt a producción, revisa su tamaño. El precipicio es
un número alrededor del cual puedes diseñar — pero solo si sabes de qué lado estás.&lt;/p&gt;
&lt;aside class=&quot;not-prose my-8 flex flex-wrap items-center gap-4 rounded-2xl border border-neutral-100 bg-white p-5 transition-all duration-200 hover:shadow-[5px_5px_rgba(0,98,90,0.3),10px_10px_rgba(0,98,90,0.2),15px_15px_rgba(0,98,90,0.1)] dark:border-zinc-800 dark:bg-zinc-900/40&quot; style=&quot;border-left:4px solid #a855f7&quot;&gt; &lt;span class=&quot;grid size-12 shrink-0 place-items-center rounded-xl&quot; style=&quot;background:#a855f71a;color:#a855f7&quot;&gt;  &lt;/span&gt; &lt;div class=&quot;flex min-w-0 flex-1 flex-col gap-0.5&quot;&gt; &lt;span class=&quot;text-xs font-semibold tracking-wide text-zinc-500 uppercase dark:text-zinc-400&quot;&gt; Herramienta gratuita &lt;/span&gt; &lt;span class=&quot;text-lg leading-snug font-bold text-blacktext dark:text-mint-50&quot;&gt; Contador de Tokens y Calculadora de Costos de IA &lt;/span&gt; &lt;span class=&quot;text-sm text-pretty text-zinc-600 dark:text-zinc-400&quot;&gt; Cuenta los tokens de tu texto o prompt y calcula cuánto costará en GPT-5, Claude, Gemini, Kimi K3, Grok y DeepSeek. Conteo exacto de OpenAI y costos por llamada y al mes, con caché de prompt. &lt;/span&gt; &lt;/div&gt; &lt;a href=&quot;https://ortamarco.me/herramientas/contador-tokens/&quot; data-umami-event=&quot;tool_callout_click&quot; data-umami-event-tool=&quot;contador-tokens&quot; class=&quot;ml-auto shrink-0 rounded-xl bg-mint-600 px-4 py-2 text-sm! font-semibold text-white! no-underline! transition-colors hover:bg-mint-700 hover:text-white!&quot;&gt; Abrir herramienta → &lt;/a&gt; &lt;/aside&gt;
&lt;h2 id=&quot;2-las-restricciones-de-acceso-son-reales-y-pegan-en-trabajo-no-relacionado&quot;&gt;2. Las restricciones de acceso son reales, y pegan en trabajo no relacionado&lt;/h2&gt;
&lt;p&gt;Astra es el primer modelo que OpenAI clasifica en nivel &lt;strong&gt;Crítico&lt;/strong&gt; de capacidad en ciberseguridad bajo su Preparedness Framework. En las propias palabras llanas de OpenAI: con las herramientas y el acceso correctos, puede encontrar fallas de seguridad desconocidas hasta ahora y desarrollar nuevas formas de explotarlas en muchos sistemas bien protegidos, sin que una persona guíe cada paso. Durante la evaluación desarrolló exploits para navegadores y sistemas operativos endurecidos, y encontró dos vulnerabilidades de V8 que nadie conocía.&lt;/p&gt;
&lt;p&gt;La consecuencia para quien desarrolla no es abstracta:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;El acceso estándar de la API rechaza de plano el trabajo avanzado de ciberseguridad.&lt;/strong&gt; El descubrimiento y desarrollo de exploits no queda “en pausa para revisión” — se rechaza.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;La capacidad avanzada está detrás de Daybreak y Trusted Access.&lt;/strong&gt; Si haces trabajo legítimo de seguridad defensiva, aplicas; no basta con pagar.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Las cuentas fuera de esos programas pueden toparse con lentitud, pausas o bloqueos, y según los reportes “a veces durante trabajo no relacionado”.&lt;/strong&gt; Esa última parte es la operativamente importante. Un clasificador de seguridad calibrado para una capacidad de nivel Crítico va a tener falsos positivos, y los falsos positivos sobre una llamada síncrona a la API son picos de latencia y peticiones fallidas dentro de tu producto.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Si vas a poner Astra detrás de una función que ve el usuario, constrúyelo para eso: timeouts, un modelo de respaldo y una ruta de reintento que degrade a un modelo más chico en lugar de sacar un error. Trata “la capa de seguridad dijo que no” como un modo de falla normal con su propia rama de código, no como una excepción que registras y olvidas.&lt;/p&gt;
&lt;p&gt;Hay una segunda señal que vale leer con honestidad en vez de descartarla. La evaluación del AI Safety Institute del Reino Unido encontró comportamiento de ataque a la cadena de suministro en &lt;strong&gt;2 de 500 corridas muestreadas&lt;/strong&gt;, y la propia OpenAI apunta una caída sustancial en la monitoreabilidad de la cadena de razonamiento frente a modelos anteriores — es decir, el modelo puede completar tareas sin exponer su razonamiento. Si vas a correr Astra con acceso a shell dentro de CI, &lt;code&gt;approval_policy = &amp;quot;never&amp;quot;&lt;/code&gt; ya no es un valor por defecto defendible. Es el mismo argumento que hice sobre &lt;a href=&quot;https://ortamarco.me/blog/vibe-coding-seguro-evitar-vulnerabilidades-codigo-ia/&quot;&gt;revisar el código escrito por IA antes de que llegue a producción&lt;/a&gt;, solo que ahora el radio de daño incluye tu pipeline de build.&lt;/p&gt;
&lt;h2 id=&quot;3-lo-que-no-soporta-la-trampa-del-reemplazo-directo&quot;&gt;3. Lo que no soporta (la trampa del reemplazo directo)&lt;/h2&gt;
&lt;p&gt;Astra es un modelo de razonamiento, uso de computadora y contexto largo. No es un reemplazo general de tu cuenta de OpenAI. La lista de lo que no soporta es larga y específica:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Realtime. Assistants. Fine-tuning. Embeddings. Generación de imágenes. Audio. Moderación. Completions legacy.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Dos de esos merecen mención aparte porque cargan peso en sistemas reales:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sin fine-tuning.&lt;/strong&gt; Si tu producto depende de un modelo afinado, Astra no es un camino hacia adelante para él, y OpenAI además cierra la creación de nuevos trabajos de fine-tuning autoservicio el 6 de enero de 2027. Esos dos hechos juntos son una decisión de estrategia, no un cambio de configuración.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sin embeddings.&lt;/strong&gt; El paso de embeddings de tu pipeline de RAG se queda donde está. Astra cambia la mitad de generación de RAG y nada de la mitad de recuperación — y con una ventana de un millón de tokens, la pregunta más interesante es si partes de tu capa de RAG ya están resolviendo un problema que dejaste de tener. (Con los precios de arriba: probablemente no. Recuperar sigue siendo más barato que rellenar.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;4-xhigh-y-max-el-parámetro-que-rompe-tu-validación&quot;&gt;4. &lt;code&gt;xhigh&lt;/code&gt; y &lt;code&gt;max&lt;/code&gt;: el parámetro que rompe tu validación&lt;/h2&gt;
&lt;p&gt;Astra agrega dos niveles de esfuerzo de razonamiento arriba de &lt;code&gt;high&lt;/code&gt;: &lt;code&gt;xhigh&lt;/code&gt; y &lt;code&gt;max&lt;/code&gt;. Cinco en total, en orden: &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;xhigh&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Es un cambio chico con filo. Si en cualquier punto de tu stack validas &lt;code&gt;reasoning_effort&lt;/code&gt; contra un enum quemado, una unión de TypeScript, un &lt;code&gt;Literal&lt;/code&gt; de Pydantic, un esquema de Zod, un &lt;code&gt;enum&lt;/code&gt; de JSON Schema o una restricción CHECK en base de datos — esa validación ahora rechaza valores válidos. La falla ocurre en &lt;em&gt;tu&lt;/em&gt; capa, no en la de OpenAI, que es justo el tipo de ruptura que cuesta una tarde encontrar porque el mensaje de error apunta a tu propio código y parece un bug que tú metiste.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Antes&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ReasoningEffort&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;low&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;medium&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;high&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Después&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ReasoningEffort&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;low&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;medium&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;high&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;xhigh&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;max&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Busca la cadena &lt;code&gt;&amp;#39;high&amp;#39;&lt;/code&gt; junto a &lt;code&gt;reasoning&lt;/code&gt; en tu código y en tu configuración de infraestructura antes de dejar que alguien seleccione Astra en un selector de modelos.&lt;/p&gt;
&lt;p&gt;Sobre el costo: los tokens de razonamiento son tokens de salida, facturados a $50/M (o $75/M pasando el precipicio). &lt;code&gt;max&lt;/code&gt; sobre una petición de contexto largo es la combinación más cara que existe hoy en la API. Resérvala para la decisión de arquitectura o la sesión de depuración que ya se comió un día — no para el valor por defecto de un archivo de configuración que nadie vuelve a mirar.&lt;/p&gt;
&lt;h2 id=&quot;5-las-notas-de-contexto-reemplazan-la-compactación--y-eso-cambia-el-comportamiento-del-agente&quot;&gt;5. Las notas de contexto reemplazan la compactación — y eso cambia el comportamiento del agente&lt;/h2&gt;
&lt;p&gt;Este es el cambio que me parece genuinamente subestimado, y no es un número de benchmark.&lt;/p&gt;
&lt;p&gt;Todo agente de larga duración pega contra la misma pared: el contexto se llena y el harness &lt;strong&gt;compacta&lt;/strong&gt; — resume la conversación en una más corta y sigue. La compactación pierde información y la pérdida es irreversible. El texto exacto del error del tercer intento fallido, el enfoque que abandonaste y por qué, la aserción específica que se rompió: todo eso se aplana en “probamos varios enfoques para arreglar el bug de autenticación”. Y entonces el agente vuelve a intentar el enfoque abandonado, porque nada en su contexto dice que ya falló.&lt;/p&gt;
&lt;p&gt;Astra reemplaza eso con &lt;strong&gt;notas persistentes más ventanas de contexto anteriores consultables&lt;/strong&gt;. Mantiene notas corridas entre ventanas, y puede regresar a buscar en ventanas previas algo que la nota no alcanzó a capturar. En la práctica: el modelo puede recuperar un requisito o el resultado de una prueba de hace dos horas aunque en su momento no se le haya ocurrido anotarlo.&lt;/p&gt;
&lt;p&gt;En Codex CLI (v0.153.1 o posterior, publicada el 3 de septiembre de 2026) la configuración es:&lt;/p&gt;
&lt;pre class=&quot;language-toml&quot; data-language=&quot;toml&quot;&gt;&lt;code class=&quot;language-toml&quot;&gt;&lt;span class=&quot;token key property&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;gpt-6-astra&amp;quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;model_provider&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;openai&amp;quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;model_reasoning_effort&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;high&amp;quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Deja que las notas de Astra hagan el trabajo en vez de la compactación de Codex&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;auto_compact_token_limit&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;850000&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token table class-name&quot;&gt;tui&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;auto_recap&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Por sesión: &lt;code&gt;codex -m gpt-6-astra --reasoning-effort xhigh &amp;quot;tu tarea&amp;quot;&lt;/code&gt;, o &lt;code&gt;/model gpt-6-astra&lt;/code&gt; a media sesión. Corre &lt;code&gt;codex models&lt;/code&gt; para confirmar que tu cuenta sí tiene acceso — el selector solo muestra lo que tu cuenta tiene habilitado, así que un modelo ausente es una respuesta sobre permisos, no un bug.&lt;/p&gt;
&lt;p&gt;Fíjate en la tensión con la sección 1: &lt;code&gt;auto_compact_token_limit = 850000&lt;/code&gt; deja deliberadamente que el contexto corra muy por arriba de 272K, porque de eso se trata la función. Estás cambiando dinero por continuidad. Es un intercambio defendible para una sesión de depuración difícil e indefendible para un chatbot.&lt;/p&gt;
&lt;h2 id=&quot;6-leer-los-benchmarks-con-honestidad&quot;&gt;6. Leer los benchmarks con honestidad&lt;/h2&gt;
&lt;p&gt;El marcador, con las comparaciones que se publicaron junto a él:&lt;/p&gt;





























































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Benchmark&lt;/th&gt;&lt;th&gt;GPT-6 Astra&lt;/th&gt;&lt;th&gt;GPT-5.6 Sol&lt;/th&gt;&lt;th&gt;Claude Opus 5&lt;/th&gt;&lt;th&gt;Claude Fable 5.1&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;OSWorld 2.0 (uso de computadora)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;72.6 %&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;65.7 %&lt;/td&gt;&lt;td&gt;70.2 %&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Terminal-Bench 4.0&lt;/td&gt;&lt;td&gt;&lt;strong&gt;57.7 %&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;37.3 %&lt;/td&gt;&lt;td&gt;52.3 %&lt;/td&gt;&lt;td&gt;55.8 %&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;FrontierMath Tier 4 v2&lt;/td&gt;&lt;td&gt;&lt;strong&gt;97.6 %&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;83.0 %&lt;/td&gt;&lt;td&gt;73.2 %&lt;/td&gt;&lt;td&gt;87.8 %&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;GPQA Diamond&lt;/td&gt;&lt;td&gt;&lt;strong&gt;96.0 %&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;94.6 %&lt;/td&gt;&lt;td&gt;93.7 %&lt;/td&gt;&lt;td&gt;93.7 %&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ExploitBench&lt;/td&gt;&lt;td&gt;&lt;strong&gt;100 %&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;78.5 %&lt;/td&gt;&lt;td&gt;70.0 %&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Humanity’s Last Exam (con herramientas)&lt;/td&gt;&lt;td&gt;57.2 %&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;63.6 %&lt;/td&gt;&lt;td&gt;&lt;strong&gt;65.0 %&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DeepSWE v1.1 (programación)&lt;/td&gt;&lt;td&gt;74.1 %&lt;/td&gt;&lt;td&gt;72.7 %&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Tres lecturas honestas:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;La ganancia en uso de computadora es la noticia real.&lt;/strong&gt; 72.6 % en OSWorld con el tiempo de completar tareas cayendo de ~75 minutos a ~40 es un cambio de escalón en lo que un agente puede terminar sin supervisión. Si tu caso de uso es “opera un navegador, una hoja de cálculo y una terminal para completar un trabajo de varios pasos”, esto es otra clase de herramienta.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;La ganancia en programación es marginal.&lt;/strong&gt; 74.1 % contra 72.7 % en DeepSWE está dentro del ruido de una tabla donde Gemini 3.8 Flash y Claude Opus 5 viven en la misma banda. Si estás eligiendo modelo para escribir código, Astra no es la respuesta evidente, y a $10/$50 tampoco es la respuesta barata.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El número de ARC-AGI-3 está haciendo trabajo de marketing.&lt;/strong&gt; El 99.9 % del titular sale de un harness adaptador. Las llamadas sin estado a la API —que es lo que hace tu código— reportan entre 17 % y 63 % según el nivel de razonamiento. Cualquier benchmark citado con un harness propio es una afirmación sobre el harness tanto como sobre el modelo.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Astra también queda debajo de Claude Fable 5.1 en Humanity’s Last Exam con herramientas (57.2 % contra 65.0 %). “El modelo más capaz jamás desplegado” es una afirmación defendible en agregado; no es cierta en todos los ejes, y los ejes donde no lo es incluyen razonamiento con herramientas, que es lo que hacen los agentes todo el día.&lt;/p&gt;
&lt;h2 id=&quot;la-parte-que-de-verdad-necesita-la-mayoría-el-23-de-octubre&quot;&gt;La parte que de verdad necesita la mayoría: el 23 de octubre&lt;/h2&gt;
&lt;p&gt;Aquí es donde el ruido del lanzamiento choca con la fecha real.&lt;/p&gt;
&lt;p&gt;El &lt;strong&gt;23 de octubre de 2026&lt;/strong&gt; OpenAI apaga &lt;code&gt;gpt-3.5-turbo&lt;/code&gt;, &lt;code&gt;gpt-4&lt;/code&gt;, &lt;code&gt;gpt-4-turbo&lt;/code&gt;, &lt;code&gt;o1&lt;/code&gt;, &lt;code&gt;o1-pro&lt;/code&gt;, &lt;code&gt;o3-mini&lt;/code&gt; y &lt;code&gt;o4-mini&lt;/code&gt; en la API. Después de esa fecha, las llamadas a esos IDs devuelven errores, no respuestas. Ese es el cambio con ruptura que tienes en el calendario, y está a 48 días de este post.&lt;/p&gt;
&lt;p&gt;El lanzamiento de Astra va a empujar a mucha gente a migrar directo hacia él. Para la mayoría &lt;strong&gt;eso es un error caro.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Lo típico conectado a &lt;code&gt;gpt-3.5-turbo&lt;/code&gt; es un chatbot de soporte o un clasificador construido en 2023. &lt;code&gt;gpt-3.5-turbo&lt;/code&gt; cerró con un precio publicado de alrededor de $0.50 por millón de entrada y $1.50 por millón de salida. Astra son $10 y $50. Eso es &lt;strong&gt;20 veces el costo de entrada y 33 veces el de salida&lt;/strong&gt; — para una carga que se eligió precisamente porque era la barata. Migrar un clasificador de alto volumen de gpt-3.5 a Astra no lo moderniza: convierte una línea de $40 al mes en algo que sí vas a notar.&lt;/p&gt;
&lt;p&gt;La escalera de migración que sí tiene sentido:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Chatbot o clasificador en &lt;code&gt;gpt-3.5-turbo&lt;/code&gt; →&lt;/strong&gt; el nivel small/mini actual. Más barato que lo que traes, y muchísimo más capaz. Astra ni entra en la conversación.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Carga general en &lt;code&gt;gpt-4&lt;/code&gt; / &lt;code&gt;gpt-4-turbo&lt;/code&gt; →&lt;/strong&gt; el nivel medio actual. Prueba regresiones de prompt; una década de folclore de prompt engineering se afinó contra las manías específicas de gpt-4.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Carga de razonamiento en &lt;code&gt;o1&lt;/code&gt; / &lt;code&gt;o3-mini&lt;/code&gt; / &lt;code&gt;o4-mini&lt;/code&gt; →&lt;/strong&gt; aquí Astra sí es candidato genuino, porque ya aceptaste el costo de tokens de razonamiento. Mide con &lt;code&gt;high&lt;/code&gt; antes de irte a &lt;code&gt;xhigh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agente de horizonte largo que opera software →&lt;/strong&gt; Astra, y este es el caso para el que se construyó. Presupuesta lo de la sección 1.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El calendario completo, la consulta de auditoría para saber si estás expuesto y la lista de migración están en &lt;a href=&quot;https://ortamarco.me/blog/retiran-el-modelo-de-ia-que-usa-tu-negocio/&quot;&gt;el calendario de apagones de modelos de IA de 2026&lt;/a&gt; — incluidas las dos fechas de septiembre que caen antes de que Astra siquiera esté disponible en general: la Videos API y &lt;code&gt;sora-2&lt;/code&gt; se retiran el 24 de septiembre, y los snapshots legacy el 28.&lt;/p&gt;
&lt;h2 id=&quot;entonces-te-cambias&quot;&gt;Entonces, ¿te cambias?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Traes un chatbot, un clasificador o cualquier cosa de alto volumen y contexto corto:&lt;/strong&gt; no. Astra es la clase de precio equivocada. Vete al nivel chico, y vete antes del 23 de octubre.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Traes un agente de programación:&lt;/strong&gt; no con estos números solos. La diferencia en programación contra la generación anterior es marginal y el precio no. Pruébalo en las tareas difíciles —las que hoy fallan— y deja el modelo más barato como predeterminado.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Traes un agente de horizonte largo que maneja un navegador, una terminal o un escritorio:&lt;/strong&gt; sí, y es la única categoría donde la respuesta es un sí directo. El salto en OSWorld y la arquitectura de notas persistentes apuntan exactamente a ti. Pon el techo de tokens primero.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Haces trabajo de seguridad defensiva:&lt;/strong&gt; sí, y empieza ya la solicitud a Trusted Access, porque la API estándar va a rechazar justo el trabajo para el que la necesitas.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tienes algo afinado con fine-tuning:&lt;/strong&gt; Astra no es tu camino. Planea eso aparte, con la fecha de enero de 2027 en la misma hoja.&lt;/p&gt;
&lt;p&gt;Para seguir leyendo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/retiran-el-modelo-de-ia-que-usa-tu-negocio/&quot;&gt;El calendario de apagones de modelos de IA de 2026&lt;/a&gt; — la fecha del 23 de octubre completa, y cómo comprobar si tu código está expuesto.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/cuanto-cuesta-programar-con-agentes-de-ia/&quot;&gt;Cuánto cuesta programar con agentes de IA&lt;/a&gt; — 7,970 millones de tokens medidos, y la aritmética que hace que el precipicio de 272K importe.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/llm-local-empresa-privacidad/&quot;&gt;IA sin sacar tus datos: LLM locales&lt;/a&gt; — la otra respuesta cuando el precio del modelo insignia y la pregunta de retención de datos caen mal las dos.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/migrar-servidor-mcp-2026-07-28/&quot;&gt;Migrar tu servidor MCP a la spec 2026-07-28&lt;/a&gt; — Astra soporta MCP y búsqueda de herramientas; la spec de abajo también se movió.&lt;/li&gt;
&lt;/ul&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuánto cuesta la API de GPT-6 Astra? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; El precio estándar es de 10 dólares por millón de tokens de entrada y 50 dólares por millón de tokens de salida, con la entrada en caché a 1 dólar por millón y las escrituras de caché a 12.50 por millón. El procesamiento Batch y Flex corre al 50 % de la tarifa estándar, y el modo Fast cuesta el doble del estándar a cambio de hasta 2.5 veces la velocidad. Lo crítico: cualquier petición con más de 272,000 tokens de entrada se factura al doble de las tarifas de entrada y caché y a 1.5 veces la de salida sobre la petición completa, no solo sobre los tokens que pasan el umbral. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuál es la ventana de contexto de GPT-6 Astra? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; La ventana de contexto es de 1,050,000 tokens, con un máximo de 922,000 tokens de entrada y 128,000 de salida en una sola petición. El corte de conocimiento es el 30 de abril de 2026. Ten en cuenta que usar más de 272,000 tokens de entrada dispara un recargo que aplica a la petición completa, así que la ventana usable y la ventana económicamente sensata no son el mismo número. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Por qué no veo gpt-6-astra en mi cuenta? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; El acceso va por etapas. En el lanzamiento Astra llegó a un grupo limitado de organizaciones dentro de los programas Daybreak y Trusted Access de OpenAI, después a los usuarios de ChatGPT Plus, Pro, Business y Enterprise, y luego a la API y a Amazon Bedrock. Los espacios de Enterprise lo traen deshabilitado por defecto y un administrador tiene que activarlo. En Codex CLI, ejecutar &amp;quot;codex models&amp;quot; muestra solo lo que tu cuenta tiene habilitado, así que un modelo ausente normalmente significa restricción de acceso y no un bug. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Puedo usar GPT-6 Astra para investigación de seguridad o pruebas de penetración? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No a través del acceso estándar de la API. Astra es el primer modelo que OpenAI clasifica en nivel Crítico de ciberseguridad bajo su Preparedness Framework, y el acceso estándar rechaza de plano el trabajo avanzado de ciberseguridad, como el descubrimiento de exploits, en lugar de pausarlo para revisión. La capacidad avanzada está detrás de los programas Daybreak y Trusted Access. Las cuentas fuera de esos programas también pueden ver lentitud, pausas o bloqueos, según los reportes a veces durante trabajo no relacionado. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿GPT-6 Astra soporta fine-tuning? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No. Astra soporta Chat Completions, Responses y Batch, y no soporta fine-tuning, Realtime, Assistants, embeddings, generación de imágenes, audio, moderación ni el endpoint legacy de Completions. Esto importa junto a otro cambio de OpenAI: la creación de nuevos trabajos de fine-tuning autoservicio cierra el 6 de enero de 2027, así que cualquier producto que dependa de un modelo afinado necesita un plan que no pase por Astra. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Debo migrar mi chatbot de gpt-3.5-turbo a GPT-6 Astra antes del 23 de octubre de 2026? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Casi seguro que no. gpt-3.5-turbo se apaga el 23 de octubre de 2026 junto con gpt-4, gpt-4-turbo, o1, o1-pro, o3-mini y o4-mini, así que sí tienes que moverte. Pero gpt-3.5-turbo cerró con un precio publicado de alrededor de 0.50 dólares por millón de entrada y 1.50 por millón de salida, contra los 10 y 50 de Astra: unas 20 veces el costo de entrada y 33 veces el de salida. Las cargas de alto volumen y contexto corto, como chatbots y clasificadores, pertenecen al nivel small o mini actual, que es a la vez más barato que gpt-3.5-turbo y bastante más capaz. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué son los niveles de razonamiento xhigh y max? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Astra agrega dos niveles arriba de high, para un total de cinco: low, medium, high, xhigh y max. Cualquier código que valide reasoning_effort contra una lista quemada —una unión de TypeScript, un esquema de Zod o Pydantic, un enum de JSON Schema, una restricción de base de datos— va a rechazar los valores nuevos hasta que se actualice, y el error resultante parece venir de tu propio código. Los tokens de razonamiento se facturan como tokens de salida, así que max sobre una petición de contexto largo es la combinación más cara que ofrece la API hoy. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué son las notas de contexto y en qué se diferencian de la compactación? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; La compactación resume una conversación completa en una más corta cuando se llena la ventana de contexto, lo que pierde de forma irreversible detalles como mensajes de error exactos o enfoques que se probaron y se abandonaron. Astra en cambio mantiene notas persistentes entre ventanas de contexto y deja consultables las ventanas anteriores, así que puede recuperar un requisito o el resultado de una prueba de una ventana previa incluso cuando la nota no lo capturó. En Codex CLI v0.153.1 o posterior esto se configura subiendo auto_compact_token_limit para que las notas de Astra se encarguen de la continuidad en vez de que el harness compacte antes de tiempo. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Desarrollo Web</category><category>IA</category><category>OpenAI</category><category>API</category><category>Costos</category><category>Agentes</category><category>Migración</category><author>Marco Orta</author></item><item><title>Kubernetes 1.37: qué se rompe el día que actualizas (y por qué es tu plano de control)</title><link>https://ortamarco.me/blog/kubernetes-1-37-que-se-rompe/</link><guid isPermaLink="true">https://ortamarco.me/blog/kubernetes-1-37-que-se-rompe/</guid><description>La 1.37 no introduce la restricción de los pods estáticos: quita el interruptor de apagado. Si desactivaste PreventStaticPodAPIReferences entre la 1.34 y la 1.36, lo que no arranca es etcd y kube-apiserver. Tres bloqueadores, con los comandos de auditoría.</description><pubDate>Sat, 05 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Kubernetes v1.37 “Garhwal” salió el 26 de agosto de 2026, y su cambio con ruptura principal se está contando mal en casi todos lados. La 1.37 no introduce la regla de que los pods estáticos no pueden referenciar objetos de la API — eso aterrizó en la 1.34. Lo que la 1.37 quita es la salida de emergencia.&lt;/strong&gt; El feature gate &lt;code&gt;PreventStaticPodAPIReferences&lt;/code&gt; desapareció y, según la nota de versión oficial, “ya no se puede desactivar”.&lt;/p&gt;
&lt;p&gt;Eso invierte quién está en riesgo. Si nunca te topaste con esto, estás bien. Los clústeres que se rompen el día de la actualización son justo los que &lt;em&gt;ya se toparon&lt;/em&gt; en la 1.34, la 1.35 o la 1.36, apagaron el gate para poder avanzar y archivaron el arreglo de verdad para después. Después es ahora, y lo que no arranca es el plano de control.&lt;/p&gt;
&lt;h2 id=&quot;los-tres-bloqueadores-por-orden&quot;&gt;Los tres bloqueadores, por orden&lt;/h2&gt;





























&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;/th&gt;&lt;th&gt;Cambio&lt;/th&gt;&lt;th&gt;Cómo falla&lt;/th&gt;&lt;th&gt;Urgencia&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;Se elimina el gate &lt;code&gt;PreventStaticPodAPIReferences&lt;/code&gt;&lt;/td&gt;&lt;td&gt;El kubelet &lt;strong&gt;rechaza la admisión&lt;/strong&gt; de los pods estáticos que referencian objetos de la API — incluidos los manifiestos de etcd y kube-apiserver&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Bloquea la actualización&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;Se hace cumplir el rechazo de cgroup v1&lt;/td&gt;&lt;td&gt;El kubelet &lt;strong&gt;sale al arrancar&lt;/strong&gt; salvo que pongas &lt;code&gt;failCgroupV1: false&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Bloquea la actualización&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;Se deprecia el modo &lt;code&gt;ipvs&lt;/code&gt; de kube-proxy&lt;/td&gt;&lt;td&gt;Solo advertencia al arrancar. Desactivado por defecto en la 1.40, eliminado en la 1.43&lt;/td&gt;&lt;td&gt;Planéalo, no corras&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Todo lo demás de esta versión —&lt;code&gt;kube-dns&lt;/code&gt; depreciado en favor de CoreDNS, &lt;code&gt;kubectl run --filename&lt;/code&gt;/&lt;code&gt;-f&lt;/code&gt; depreciado, dieciséis funciones que pasan a estable— es limpieza. Los dos que impiden que un nodo vuelva son los dos primeros.&lt;/p&gt;
&lt;h2 id=&quot;1-los-pods-estáticos-ya-no-pueden-referenciar-objetos-de-la-api&quot;&gt;1. Los pods estáticos ya no pueden referenciar objetos de la API&lt;/h2&gt;
&lt;h3 id=&quot;cuál-es-la-regla-de-verdad&quot;&gt;Cuál es la regla de verdad&lt;/h3&gt;
&lt;p&gt;La formulación limpia no es “nada de Secrets ni ConfigMaps”. Es la del pull request que lo implementó: &lt;strong&gt;los pods estáticos solo pueden usar volúmenes &lt;code&gt;hostPath&lt;/code&gt; y &lt;code&gt;emptyDir&lt;/code&gt;&lt;/strong&gt;, y no pueden referenciar objetos de la API en absoluto.&lt;/p&gt;
&lt;p&gt;La nota de versión oficial del &lt;a href=&quot;https://github.com/kubernetes/kubernetes/pull/131837&quot;&gt;PR #131837&lt;/a&gt;, que lleva un &lt;code&gt;ACTION REQUIRED&lt;/code&gt; explícito, nombra la superficie completa:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Antes de actualizar, asegúrate de que los pods estáticos no estén referenciando objetos de la API como &lt;strong&gt;ServiceAccounts, ConfigMaps, Secrets, ResourceClaims, CSIDrivers, PersistentVolumeClaims o ClusterTrustBundles.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Fíjate en &lt;code&gt;ServiceAccounts&lt;/code&gt; dentro de esa lista. Casi toda la cobertura secundaria de esta versión menciona solo &lt;code&gt;configMapRef&lt;/code&gt; y &lt;code&gt;secretRef&lt;/code&gt;, y eso se queda corto: un pod estático con un &lt;code&gt;serviceAccountName&lt;/code&gt;, una entrada de &lt;code&gt;imagePullSecrets&lt;/code&gt; o un volumen respaldado por un PVC queda rechazado igual. En la práctica, los campos que hay que cazar son:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;env.valueFrom.configMapKeyRef&lt;/code&gt; y &lt;code&gt;env.valueFrom.secretKeyRef&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;envFrom.configMapRef&lt;/code&gt; y &lt;code&gt;envFrom.secretRef&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;volumes[].configMap&lt;/code&gt;, &lt;code&gt;volumes[].secret&lt;/code&gt;, &lt;code&gt;volumes[].persistentVolumeClaim&lt;/code&gt;, &lt;code&gt;volumes[].projected&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;serviceAccountName&lt;/code&gt;, &lt;code&gt;imagePullSecrets&lt;/code&gt;, &lt;code&gt;resourceClaims&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;por-qué-es-un-problema-del-plano-de-control-y-no-de-tus-cargas&quot;&gt;Por qué es un problema del plano de control y no de tus cargas&lt;/h3&gt;
&lt;p&gt;Los pods estáticos son los que el kubelet corre directo del disco desde &lt;code&gt;/etc/kubernetes/manifests/&lt;/code&gt;, sin que intervenga el API server. En un clúster de kubeadm ese directorio contiene &lt;strong&gt;etcd, kube-apiserver, kube-controller-manager y kube-scheduler&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Así que la forma de fallar no es “uno de mis pods no se programó”. Es: actualizas un nodo del plano de control, el kubelet rechaza la admisión del manifiesto de etcd, y el API server nunca levanta para decirte por qué. Estás depurando desde &lt;code&gt;journalctl -u kubelet&lt;/code&gt; en el nodo, no desde &lt;code&gt;kubectl&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;el-comportamiento-viejo-era-silencioso-y-por-eso-existe-esto&quot;&gt;El comportamiento viejo era silencioso, y por eso existe esto&lt;/h3&gt;
&lt;p&gt;Antes de la 1.34 esto no daba error. El pod estático corría, y lo único que fallaba en reconciliar era el pod &lt;em&gt;espejo&lt;/em&gt; — la representación de solo lectura que el kubelet crea en la API para que el pod aparezca en &lt;code&gt;kubectl get pods&lt;/code&gt;. El contenedor estaba vivo en el nodo y era efectivamente invisible para la API. El &lt;a href=&quot;https://github.com/kubernetes/kubernetes/pull/131837&quot;&gt;PR #131837&lt;/a&gt; cerró eso rechazando la admisión de plano, para que el contenedor nunca se cree en lugar de correr sin que nadie lo vea.&lt;/p&gt;
&lt;p&gt;Esa historia es la razón de que el gate existiera: los revisores pidieron que la validación saliera activada por defecto pero desactivable “durante unas cuantas versiones”. Tres ciclos de versión después, el &lt;a href=&quot;https://github.com/kubernetes/kubernetes/pull/140226&quot;&gt;PR #140226&lt;/a&gt; lo eliminó, con milestone v1.37.&lt;/p&gt;
&lt;h3 id=&quot;la-auditoría-antes-de-tocar-un-solo-nodo&quot;&gt;La auditoría, antes de tocar un solo nodo&lt;/h3&gt;
&lt;p&gt;Corre esto en &lt;strong&gt;todos&lt;/strong&gt; los nodos, de plano de control y de trabajo, no en uno:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rlE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;configMapRef|secretRef|configMapKeyRef|secretKeyRef|serviceAccountName|imagePullSecrets|persistentVolumeClaim&amp;#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  /etc/kubernetes/manifests/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cada archivo que imprima es un manifiesto que tienes que rehacer antes de actualizar ese nodo. El arreglo siempre tiene la misma forma: dejar de sacar el valor de la API y ponerlo en disco, como montaje &lt;code&gt;hostPath&lt;/code&gt; o como literal dentro del manifiesto, ya que &lt;code&gt;hostPath&lt;/code&gt; y &lt;code&gt;emptyDir&lt;/code&gt; son los únicos tipos de volumen que siguen permitidos.&lt;/p&gt;
&lt;p&gt;Si tus manifiestos son generados —kubeadm, Cluster API, un chart de Helm que plantilla la config del nodo, un rol de Ansible— audita la plantilla, no solo la salida renderizada, o el siguiente nodo que aprovisiones vuelve a meter el problema.&lt;/p&gt;
&lt;p&gt;Antes de dar por bueno un manifiesto rehecho, comprueba que sigue siendo YAML válido y que no rompiste la indentación al arrancar un bloque de volúmenes:&lt;/p&gt;
&lt;aside class=&quot;not-prose my-8 flex flex-wrap items-center gap-4 rounded-2xl border border-neutral-100 bg-white p-5 transition-all duration-200 hover:shadow-[5px_5px_rgba(0,98,90,0.3),10px_10px_rgba(0,98,90,0.2),15px_15px_rgba(0,98,90,0.1)] dark:border-zinc-800 dark:bg-zinc-900/40&quot; style=&quot;border-left:4px solid #f43f5e&quot;&gt; &lt;span class=&quot;grid size-12 shrink-0 place-items-center rounded-xl&quot; style=&quot;background:#f43f5e1a;color:#f43f5e&quot;&gt;  &lt;/span&gt; &lt;div class=&quot;flex min-w-0 flex-1 flex-col gap-0.5&quot;&gt; &lt;span class=&quot;text-xs font-semibold tracking-wide text-zinc-500 uppercase dark:text-zinc-400&quot;&gt; Herramienta gratuita &lt;/span&gt; &lt;span class=&quot;text-lg leading-snug font-bold text-blacktext dark:text-mint-50&quot;&gt; Conversor de JSON y YAML &lt;/span&gt; &lt;span class=&quot;text-sm text-pretty text-zinc-600 dark:text-zinc-400&quot;&gt; Convierte JSON a YAML y YAML a JSON al instante, con indentación configurable, descarga y copia. Todo en tu navegador, sin subir nada. &lt;/span&gt; &lt;/div&gt; &lt;a href=&quot;https://ortamarco.me/herramientas/conversor-json-yaml/&quot; data-umami-event=&quot;tool_callout_click&quot; data-umami-event-tool=&quot;conversor-json-yaml&quot; class=&quot;ml-auto shrink-0 rounded-xl bg-mint-600 px-4 py-2 text-sm! font-semibold text-white! no-underline! transition-colors hover:bg-mint-700 hover:text-white!&quot;&gt; Abrir herramienta → &lt;/a&gt; &lt;/aside&gt;
&lt;h2 id=&quot;2-cgroup-v1-el-kubelet-simplemente-sale&quot;&gt;2. cgroup v1: el kubelet simplemente sale&lt;/h2&gt;
&lt;p&gt;Este es corto y absoluto. El kubelet se niega a correr en un host que usa cgroup v1. No advierte y degrada: sale al arrancar con&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;kubelet is configured to not run on a host using cgroup v1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El valor por defecto pasó a fallar en la 1.35, y la 1.37 lo mantiene. La única anulación va en &lt;code&gt;KubeletConfiguration&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; data-language=&quot;yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;failCgroupV1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dónde lo pones depende de tu distribución, y el momento importa:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;kubeadm:&lt;/strong&gt; edita el ConfigMap &lt;code&gt;kube-system/kubelet-config&lt;/code&gt; y añade el campo &lt;strong&gt;antes&lt;/strong&gt; de actualizar. Ponerlo después de que el kubelet ya se negó a arrancar significa editar configuración en un nodo cuyo plano de control quizá esté caído.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RKE2 / K3s:&lt;/strong&gt; pasa el argumento del kubelet &lt;code&gt;fail-cgroupv1=false&lt;/code&gt; en &lt;code&gt;/etc/rancher/rke2/config.yaml&lt;/code&gt; o &lt;code&gt;/etc/rancher/k3s/config.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Trátalo como un puente, no como un arreglo. Es exactamente la misma categoría de decisión que el gate de los pods estáticos por el que estás leyendo este post: un interruptor de apagado que upstream está retirando.&lt;/p&gt;
&lt;p&gt;Comprueba en qué versión de cgroup está cada nodo antes de actualizarlo:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;stat&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-fc&lt;/span&gt; %T /sys/fs/cgroup
&lt;span class=&quot;token comment&quot;&gt;# cgroup2fs -&amp;gt; v2, vas bien&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# tmpfs     -&amp;gt; v1, no vas bien&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;ls&lt;/span&gt; /sys/fs/cgroup/cgroup.controllers   &lt;span class=&quot;token comment&quot;&gt;# solo existe en v2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Los nodos que tropiezan con esto son los viejos: hosts de CentOS 7 y Ubuntu 18.04 de larga vida, y cualquier cosa aprovisionada desde una AMI o imagen construida hace años y nunca reconstruida. Si corres un servicio gestionado (EKS, GKE, AKS) con imágenes de nodo actuales, casi seguro ya estás en v2 — pero “casi seguro” está a un &lt;code&gt;stat&lt;/code&gt; de ser seguro.&lt;/p&gt;
&lt;h2 id=&quot;3-ipvs-de-kube-proxy-depreciado-no-eliminado&quot;&gt;3. IPVS de kube-proxy: depreciado, no eliminado&lt;/h2&gt;
&lt;p&gt;El &lt;code&gt;mode: ipvs&lt;/code&gt; en &lt;code&gt;KubeProxyConfiguration&lt;/code&gt; queda depreciado a partir de la 1.37 (&lt;a href=&quot;https://github.com/kubernetes/enhancements/issues/5495&quot;&gt;KEP-5495&lt;/a&gt;). Hoy no se rompe nada: te salen advertencias en los logs de arranque de kube-proxy. La línea de tiempo:&lt;/p&gt;

























&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Versión&lt;/th&gt;&lt;th&gt;Estado&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;v1.35&lt;/td&gt;&lt;td&gt;Empiezan los logs de advertencia&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;v1.37&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Depreciado formalmente, siguen las advertencias&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;v1.40&lt;/td&gt;&lt;td&gt;Desactivado por defecto, se reactiva solo con feature gate&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;v1.43&lt;/td&gt;&lt;td&gt;Código eliminado por completo&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Vale la pena conocer el motivo porque tumba la objeción habitual: el modo IPVS nunca fue un reemplazo completo de iptables — sigue necesitando iptables por debajo y no puede implementar los Services de Kubernetes por sí solo. El sucesor es &lt;code&gt;nftables&lt;/code&gt;, no una vuelta a &lt;code&gt;iptables&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Comprueba qué estás corriendo:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; kube-system get cm kube-proxy &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;jsonpath&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;{.data.config\.conf}&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; mode:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si dice &lt;code&gt;ipvs&lt;/code&gt;, tienes de ahora hasta la 1.40 para pasarte a &lt;code&gt;mode: nftables&lt;/code&gt;. Eso es más o menos un año de versiones — suficiente para hacerlo a tu ritmo, que es justo la razón para meterlo en el calendario ahora en vez de encontrártelo como la gente se está encontrando el gate de los pods estáticos este mes.&lt;/p&gt;
&lt;h2 id=&quot;la-lista-previa-a-actualizar&quot;&gt;La lista previa a actualizar&lt;/h2&gt;
&lt;p&gt;Corre todo esto antes del primer nodo, en cada nodo:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 1. Referencias a la API en pods estáticos — el bloqueador&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rlE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;configMapRef|secretRef|configMapKeyRef|secretKeyRef|serviceAccountName|imagePullSecrets|persistentVolumeClaim&amp;#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  /etc/kubernetes/manifests/

&lt;span class=&quot;token comment&quot;&gt;# 2. Versión de cgroup — el otro bloqueador&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;stat&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-fc&lt;/span&gt; %T /sys/fs/cgroup

&lt;span class=&quot;token comment&quot;&gt;# 3. Modo de kube-proxy — planear, no bloquear&lt;/span&gt;
kubectl &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; kube-system get cm kube-proxy &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;jsonpath&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;{.data.config\.conf}&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; mode:

&lt;span class=&quot;token comment&quot;&gt;# 4. ¿Sigues en kube-dns en vez de CoreDNS?&lt;/span&gt;
kubectl &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; kube-system get deploy &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;kube-dns|coredns&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Salida vacía en el 1, &lt;code&gt;cgroup2fs&lt;/code&gt; en el 2 y &lt;code&gt;nftables&lt;/code&gt; o &lt;code&gt;iptables&lt;/code&gt; en el 3 significa que la actualización va a ser aburrida. Ese es el objetivo.&lt;/p&gt;
&lt;h2 id=&quot;quién-tiene-que-actuar-de-verdad&quot;&gt;Quién tiene que actuar de verdad&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Desactivaste &lt;code&gt;PreventStaticPodAPIReferences&lt;/code&gt; en algún momento desde la 1.34:&lt;/strong&gt; eres el destinatario de todo este post. Ese gate ya no existe, así que lo que te llevó a desactivarlo ahora bloquea la actualización. Arregla los manifiestos primero, actualiza después.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Corres planos de control de kubeadm que has editado a mano:&lt;/strong&gt; corre el grep específicamente en los nodos del plano de control. Los archivos editados a mano en &lt;code&gt;/etc/kubernetes/manifests/&lt;/code&gt; son justo donde alguien añade un &lt;code&gt;secretRef&lt;/code&gt; o un &lt;code&gt;serviceAccountName&lt;/code&gt; resolviendo un problema a las 2 de la mañana.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tienes nodos de más de unos tres años:&lt;/strong&gt; comprueba cgroups antes que nada. Un kubelet que sale al arrancar se ve idéntico a una actualización rota, y vas a perder una hora encontrando una respuesta de una línea.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Estás en Kubernetes gestionado con imágenes de nodo actuales:&lt;/strong&gt; probablemente estás limpio en el 1 y el 2. Revisa el 3 y mete la migración de IPVS en un trimestre, no en un sprint.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Corres &lt;code&gt;mode: ipvs&lt;/code&gt;:&lt;/strong&gt; hoy no se rompe nada. Agenda el paso a &lt;code&gt;nftables&lt;/code&gt; antes de la 1.40.&lt;/p&gt;
&lt;p&gt;Para seguir leyendo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/node-26-lts-que-se-rompe/&quot;&gt;Node 26: qué se rompe al actualizar&lt;/a&gt; — el mismo formato para el runtime, y el mismo patrón de fallos que no dan error.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/vite-8-que-se-rompe/&quot;&gt;Vite 8: qué se rompe al actualizar&lt;/a&gt; — la versión que heredaste sin elegirla, que es la otra forma en que muerden estas actualizaciones.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/gpt-6-astra-para-desarrolladores/&quot;&gt;GPT-6 Astra para desarrolladores&lt;/a&gt; — si corres agentes con acceso a shell contra un clúster, la sección de seguridad y política de aprobación te aplica directo.&lt;/li&gt;
&lt;/ul&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué se rompe al actualizar a Kubernetes 1.37? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Dos cambios pueden bloquear la actualización de plano. El primero: se eliminó el feature gate PreventStaticPodAPIReferences, así que los pods estáticos que referencian objetos de la API reciben rechazo de admisión del kubelet sin forma de optar por salirse, y en clústeres de kubeadm esos pods estáticos incluyen etcd y kube-apiserver. El segundo: el kubelet se niega a arrancar en hosts que usan cgroup v1 salvo que failCgroupV1 esté puesto explícitamente en false. Un tercer cambio, la depreciación del modo IPVS de kube-proxy, solo produce advertencias en la 1.37 y todavía no rompe nada. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Por qué fallan los pods estáticos después de actualizar a Kubernetes 1.37? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Porque se quitó la salida de emergencia, no porque la regla sea nueva. La restricción sobre pods estáticos que referencian objetos de la API salió en la 1.34 detrás del feature gate PreventStaticPodAPIReferences, que venía activado por defecto pero se podía desactivar. Kubernetes 1.37 eliminó ese gate por completo, y la nota de versión oficial dice que ya no se puede desactivar. Los clústeres que apagaron el gate para posponer el arreglo son exactamente los que se rompen al actualizar. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué objetos de la API no puede referenciar un pod estático en Kubernetes 1.37? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; La nota de versión oficial lista ServiceAccounts, ConfigMaps, Secrets, ResourceClaims, CSIDrivers, PersistentVolumeClaims y ClusterTrustBundles. La forma más simple de retener la regla es la del pull request que lo implementó: los pods estáticos solo pueden usar volúmenes hostPath y emptyDir. Eso significa que serviceAccountName, imagePullSecrets, envFrom.configMapRef, envFrom.secretRef, env.valueFrom.secretKeyRef, los volúmenes projected y los respaldados por PVC quedan rechazados todos, no solo las referencias a ConfigMap y Secret que mencionan la mayoría de los resúmenes. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cómo compruebo si mi clúster está afectado antes de actualizar? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; En cada nodo de plano de control y de trabajo, corre: grep -rlE &amp;#39;configMapRef|secretRef|configMapKeyRef|secretKeyRef|serviceAccountName|imagePullSecrets|persistentVolumeClaim&amp;#39; /etc/kubernetes/manifests/ — cualquier archivo que imprima hay que rehacerlo antes de actualizar ese nodo. Aparte, corre stat -fc %T /sys/fs/cgroup en cada nodo: cgroup2fs es seguro y tmpfs significa cgroup v1, que impide que el kubelet arranque. Si tus manifiestos los generan kubeadm, Cluster API, Ansible o Helm, audita también la plantilla o los nodos nuevos volverán a meter el problema. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Se elimina el modo IPVS de kube-proxy en Kubernetes 1.37? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No. Queda depreciado en la 1.37 bajo el KEP-5495 y solo registra advertencias al arrancar. Se espera que quede desactivado por defecto en la 1.40, donde todavía se puede reactivar mediante feature gate, y eliminado por completo en la 1.43. El reemplazo es el modo nftables, no iptables. El razonamiento es que el modo IPVS siempre necesitó iptables por debajo y nunca pudo implementar los Services de Kubernetes por sí solo. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué pasaba con los pods estáticos que referenciaban Secrets antes de Kubernetes 1.34? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Fallaba en silencio, y por eso existe la restricción. El pod estático corría en el nodo, pero el pod espejo —la representación de solo lectura en la API que hace visible el pod para kubectl— no lograba reconciliar. El contenedor estaba vivo y era efectivamente invisible para el API server. El PR #131837 cambió esto para rechazar la admisión de plano, de modo que el contenedor nunca se crea en lugar de correr sin que nadie lo observe. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuándo salió Kubernetes 1.37 y cómo se llama? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Kubernetes v1.37, con nombre en clave &amp;quot;Garhwal&amp;quot;, llegó a disponibilidad general el 26 de agosto de 2026. Junto con las depreciaciones graduó dieciséis funciones a estable, entre ellas el estado de dispositivo de ResourceClaim (KEP-4817), los taints y tolerations de dispositivo (KEP-5055), el estado de salud de recursos para pods (KEP-4680) y SELinuxMount con SELinuxChangePolicy (KEP-1710). También deprecó el subproyecto kube-dns en favor de CoreDNS y el flag --filename de kubectl run. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Desarrollo Web</category><category>Kubernetes</category><category>DevOps</category><category>Infraestructura</category><category>Migración</category><category>Upgrade</category><author>Marco Orta</author></item><item><title>ESLint 10: qué se rompe al actualizar (y por qué ESLint 9 ya es inseguro)</title><link>https://ortamarco.me/blog/eslint-10-que-se-rompe/</link><guid isPermaLink="true">https://ortamarco.me/blog/eslint-10-que-se-rompe/</guid><description>ESLint 9 llegó a su fin de vida el 6 de agosto de 2026. ESLint 10 elimina eslintrc por completo: qué se rompe, qué se rompe en silencio y cómo migrar hoy mismo.</description><pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;ESLint 9 llegó a su fin de vida el 6 de agosto de 2026. Si tu CI todavía corre &lt;code&gt;eslint@9&lt;/code&gt;, lleva cuatro semanas funcionando sin parches de seguridad, y nadie te lo va a avisar.&lt;/strong&gt; ESLint 10 salió en general availability el 6 de febrero de 2026, lo que significa que la ventana de seis meses de traslape que el proyecto siempre da ya se agotó. El cambio central no es una regla nueva ni un parser más rápido: es que el sistema de configuración &lt;code&gt;eslintrc&lt;/code&gt;, el que se arma alrededor de &lt;code&gt;.eslintrc.json&lt;/code&gt; y &lt;code&gt;.eslintrc.js&lt;/code&gt;, se eliminó del código. No se deprecó. No quedó detrás de un flag. Se eliminó. Hay exactamente una forma de configurar ESLint 10: &lt;code&gt;eslint.config.js&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Esto pesa más que la mayoría de saltos de versión mayor porque ESLint viene anunciando esta eliminación exacta desde que flat config se volvió el default en la v9, allá por abril de 2024. Los equipos tuvieron dos años de aviso. Muchos usaron ese tiempo para no hacer nada, porque la config vieja seguía funcionando. Ahora deja de funcionar.&lt;/p&gt;
&lt;h2 id=&quot;de-dónde-vienes&quot;&gt;¿De dónde vienes?&lt;/h2&gt;
&lt;p&gt;El costo de la migración se divide fuerte según tres puntos de partida.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Desde ESLint 9 con flat config ya instalado.&lt;/strong&gt; Estás en la mejor posición. Tu &lt;code&gt;eslint.config.js&lt;/code&gt; ya existe y ya funciona como ESLint 10 lo espera. Lo que sí te puede tocar: el piso de versión de Node.js se movió, el set de reglas por default ganó tres reglas que no habías visto, y —si trabajas en un monorepo— el algoritmo de búsqueda del archivo de configuración cambió de una forma que puede alterar en silencio qué reglas aplican a qué archivos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Desde ESLint 9 apoyado todavía en el shim &lt;code&gt;FlatCompat&lt;/code&gt; de &lt;code&gt;@eslint/eslintrc&lt;/code&gt;.&lt;/strong&gt; Este fue el camino intermedio pragmático que tomaron muchos equipos en 2024-2025: escribir un &lt;code&gt;eslint.config.js&lt;/code&gt;, pero usar &lt;code&gt;FlatCompat.extends()&lt;/code&gt; adentro para seguir jalando una config compartida de estilo viejo (&lt;code&gt;airbnb-base&lt;/code&gt;, un paquete interno &lt;code&gt;eslint-config-company&lt;/code&gt;) que nunca sacó release de flat config. La buena noticia, y no es obvia, es que este camino sigue funcionando en ESLint 10 —&lt;code&gt;FlatCompat&lt;/code&gt; no murió con &lt;code&gt;eslintrc&lt;/code&gt;, es un paquete aparte y sigue mantenido. Sí necesitas asegurarte de que &lt;code&gt;@eslint/eslintrc&lt;/code&gt; esté en una versión actual, porque las copias viejas son anteriores a los internos de ESLint 10.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Desde ESLint 8, o desde ESLint 9 sin ningún &lt;code&gt;eslint.config.js&lt;/code&gt;&lt;/strong&gt; (es decir, dependías del fallback automático de ESLint 9 al formato legado cuando no encontraba flat config). Este es el grupo que se rompe más duro, porque ese fallback es justo lo que se eliminó. Apunta ESLint 10 a un proyecto que solo tiene &lt;code&gt;.eslintrc.json&lt;/code&gt; y ningún &lt;code&gt;eslint.config.js&lt;/code&gt;, y no va a usar el archivo viejo calladito —va a fallar en encontrar cualquier configuración.&lt;/p&gt;
&lt;h2 id=&quot;las-rupturas-ordenadas-por-lo-que-te-van-a-costar&quot;&gt;Las rupturas, ordenadas por lo que te van a costar&lt;/h2&gt;
&lt;h3 id=&quot;1-eslintrc-desapareció-no-hay-flag-para-traerlo-de-vuelta&quot;&gt;1. &lt;code&gt;eslintrc&lt;/code&gt; desapareció. No hay flag para traerlo de vuelta.&lt;/h3&gt;
&lt;p&gt;Esta es la que todo mundo medio espera y aun así lo agarra, porque la eliminación real es más amplia que “el formato de archivo viejo deja de leerse”. Cuatro cosas se van juntas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.eslintrc.js&lt;/code&gt;, &lt;code&gt;.eslintrc.json&lt;/code&gt;, &lt;code&gt;.eslintrc.yml&lt;/code&gt; y &lt;code&gt;.eslintignore&lt;/code&gt; ya no se leen, punto.&lt;/li&gt;
&lt;li&gt;La variable de entorno &lt;code&gt;ESLINT_USE_FLAT_CONFIG&lt;/code&gt;, que antes te dejaba forzar a ESLint 9 de vuelta al sistema legado, ya no se respeta. Configurarla no hace nada —no truena, simplemente se ignora.&lt;/li&gt;
&lt;li&gt;Los flags del CLI que solo tenían sentido para eslintrc desaparecieron: &lt;code&gt;--no-eslintrc&lt;/code&gt;, &lt;code&gt;--env&lt;/code&gt;, &lt;code&gt;--resolve-plugins-relative-to&lt;/code&gt;, &lt;code&gt;--rulesdir&lt;/code&gt;, &lt;code&gt;--ignore-path&lt;/code&gt;. Los scripts que los pasan ahora fallan al arrancar.&lt;/li&gt;
&lt;li&gt;La opción del constructor &lt;code&gt;configType&lt;/code&gt; de &lt;code&gt;Linter&lt;/code&gt; solo acepta &lt;code&gt;&amp;quot;flat&amp;quot;&lt;/code&gt;. Pásale &lt;code&gt;&amp;quot;eslintrc&amp;quot;&lt;/code&gt; y truena.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La migración en sí, de &lt;code&gt;.eslintrc.json&lt;/code&gt; a &lt;code&gt;eslint.config.js&lt;/code&gt;, es mecánica una vez que la haces. Aquí va una real, antes y después:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Antes — &lt;code&gt;.eslintrc.json&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-json&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&amp;quot;root&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&amp;quot;env&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&amp;quot;browser&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&amp;quot;es2021&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&amp;quot;node&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&amp;quot;extends&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;eslint:recommended&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;plugin:@typescript-eslint/recommended&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&amp;quot;parser&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;@typescript-eslint/parser&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&amp;quot;parserOptions&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&amp;quot;ecmaVersion&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;latest&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&amp;quot;sourceType&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;module&amp;quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&amp;quot;plugins&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;@typescript-eslint&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&amp;quot;rules&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&amp;quot;no-unused-vars&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;off&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&amp;quot;@typescript-eslint/no-unused-vars&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;warn&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&amp;quot;argsIgnorePattern&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;^_&amp;quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&amp;quot;ignorePatterns&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;dist&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;node_modules&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Después — &lt;code&gt;eslint.config.js&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; js &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;@eslint/js&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; globals &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;globals&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; tseslint &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;typescript-eslint&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; tseslint&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;ignores&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;dist/**&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;node_modules/**&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  js&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;configs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;recommended&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;tseslint&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;configs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;recommended&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;languageOptions&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;globals&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;globals&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;browser&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;globals&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&amp;#39;@typescript-eslint/no-unused-vars&amp;#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;warn&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;argsIgnorePattern&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;^_&amp;#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tres cosas cambiaron de forma, no solo de sintaxis: &lt;code&gt;env&lt;/code&gt; se volvió &lt;code&gt;languageOptions.globals&lt;/code&gt; jalado del paquete &lt;code&gt;globals&lt;/code&gt;, &lt;code&gt;extends&lt;/code&gt; se volvió un arreglo que esparces directo (&lt;code&gt;tseslint.configs.recommended&lt;/code&gt; ya es un arreglo de objetos de configuración), y &lt;code&gt;no-unused-vars: off&lt;/code&gt; desapareció —&lt;code&gt;@typescript-eslint/no-unused-vars&lt;/code&gt; ya no necesita que silencies a mano la regla base porque &lt;code&gt;tseslint.configs.recommended&lt;/code&gt; ya la delimita bien por archivo.&lt;/p&gt;
&lt;p&gt;Si tu único bloqueo restante es una config compartida sin release de flat config, para eso es &lt;code&gt;FlatCompat&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; FlatCompat &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;@eslint/eslintrc&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;node:path&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; fileURLToPath &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;node:url&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; compat &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FlatCompat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;baseDirectory&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;dirname&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fileURLToPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;meta&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;compat&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;airbnb-base&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// el resto de tu flat config&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Eso sigue siendo un &lt;code&gt;eslint.config.js&lt;/code&gt; de verdad —&lt;code&gt;FlatCompat&lt;/code&gt; nada más deja que una config compartida de formato viejo viva adentro. Es un puente, no el regreso de eslintrc.&lt;/p&gt;
&lt;h3 id=&quot;2-el-algoritmo-de-búsqueda-de-config-cambió-silencioso-en-monorepos&quot;&gt;2. El algoritmo de búsqueda de config cambió —silencioso en monorepos&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Esta es silenciosa por diseño, y es el cambio con más probabilidad de morder a quien hizo todo “bien”.&lt;/strong&gt; ESLint 10 dejó de resolver &lt;code&gt;eslint.config.*&lt;/code&gt; desde el directorio de trabajo actual. En vez de eso, arranca la búsqueda desde el directorio de &lt;em&gt;cada archivo que se está analizando&lt;/em&gt; y sube desde ahí. El objetivo declarado es que un monorepo pueda tener un &lt;code&gt;eslint.config.js&lt;/code&gt; distinto por paquete sin cableado extra —lo cual es genuinamente útil. El costo es que un archivo ahora puede recoger en silencio una config distinta a la que tenía en ESLint 9, sin aviso ni error: el lint simplemente pasa o falla distinto a antes, y la razón es un archivo de configuración dos directorios más allá que se te olvidó que existía. Si corres un monorepo, analiza cada workspace por separado justo después de actualizar y compara la salida contra ESLint 9 antes de confiar en ella.&lt;/p&gt;
&lt;h3 id=&quot;3-tres-reglas-se-acaban-de-activar-en-eslintrecommended-también-en-silencio&quot;&gt;3. Tres reglas se acaban de activar en &lt;code&gt;eslint:recommended&lt;/code&gt; —también en silencio&lt;/h3&gt;
&lt;p&gt;Si tu config incluye &lt;code&gt;js.configs.recommended&lt;/code&gt; (antes &lt;code&gt;eslint:recommended&lt;/code&gt;), actualizar agrega &lt;code&gt;no-unassigned-vars&lt;/code&gt;, &lt;code&gt;no-useless-assignment&lt;/code&gt; y &lt;code&gt;preserve-caught-error&lt;/code&gt; a lo que estás exigiendo —sin que toques una sola línea de tu propia config. &lt;strong&gt;Esta es la ruptura silenciosa clásica: corres el mismo comando de siempre, y ahora falla o advierte sobre código que no cambió.&lt;/strong&gt; Si tu CI trata los warnings como fallas, eso es un build en rojo la mañana después del bump, con un diff que no muestra nada que hayas hecho mal.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;no-useless-assignment&lt;/code&gt; atrapa el valor que se sobreescribe antes de leerse:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;total&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; sum &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// esta asignación...&lt;/span&gt;
  sum &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; items&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// ...se reemplaza de inmediato, nunca se lee&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; sum&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;preserve-caught-error&lt;/code&gt; marca un error capturado que descartas en vez de encadenar:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// antes — el error original y su stack se pierden&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;parseConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;raw&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;Config parsing failed&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// después — la causa original sobrevive para quien depure esto después&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;parseConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;raw&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;err&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;Config parsing failed&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;cause&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; err &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nada de esto está mal de exigir —es buen consejo. El problema es enterarte por un deploy fallido en vez de por un changelog.&lt;/p&gt;
&lt;h3 id=&quot;4-los-comentarios--eslint-env--pasan-de-inertes-en-silencio-a-error-explícito&quot;&gt;4. Los comentarios &lt;code&gt;/* eslint-env */&lt;/code&gt; pasan de inertes en silencio a error explícito&lt;/h3&gt;
&lt;p&gt;Esta tiene una historia de dos pasos que vale la pena conocer. Flat config nunca soportó comentarios tipo &lt;code&gt;/* eslint-env browser */&lt;/code&gt; —llevan sin hacer nada en silencio desde que te moviste a flat config en ESLint 9, te hayas dado cuenta o no. ESLint 10 cierra ese silencio: el mismo comentario ahora se reporta como error de lint en vez de ignorarse calladito. Si nunca migraste esos comentarios a &lt;code&gt;languageOptions.globals&lt;/code&gt;, vas a ver errores nuevecitos apuntando a código que “funcionaba” desde hace año y medio, porque los globals que necesitaba nunca se estaban declarando de verdad —el comentario simplemente no hacía nada.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* eslint-env browser, node */&lt;/span&gt;
window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;dispatchEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;ready&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mueve la intención a la config, igual que el ejemplo de &lt;code&gt;.eslintrc&lt;/code&gt; de arriba hizo con &lt;code&gt;env&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;languageOptions&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;globals&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;globals&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;browser&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;globals&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;node &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5-el-piso-de-nodejs-se-movió-y-no-es-solo-un-bump-de-número-de-versión&quot;&gt;5. El piso de Node.js se movió, y no es solo un bump de número de versión&lt;/h3&gt;
&lt;p&gt;ESLint 10 requiere Node.js &lt;code&gt;^20.19.0 || ^22.13.0 || &amp;gt;=24&lt;/code&gt;. Node 21.x y 23.x —los lanzamientos impares que no son LTS— quedan sin soporte de plano, y cualquier cosa debajo de 20.19.0 o 22.13.0 no tiene soporte aunque el mayor sea el correcto. En &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// antes&lt;/span&gt;
&lt;span class=&quot;token property&quot;&gt;&amp;quot;engines&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&amp;quot;node&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;&amp;gt;=18&amp;quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// después&lt;/span&gt;
&lt;span class=&quot;token property&quot;&gt;&amp;quot;engines&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&amp;quot;node&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;^20.19.0 || ^22.13.0 || &amp;gt;=24&amp;quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si tu imagen de CI fija un patch viejo dentro de 20.x o 22.x, &lt;code&gt;npm install&lt;/code&gt; puede seguir teniendo éxito mientras el linter mismo falla al arrancar con un error que no parece tener nada que ver con Node —instalas ESLint y ya está roto antes de leer un solo archivo.&lt;/p&gt;
&lt;h3 id=&quot;6-métodos-eliminados-de-context-y-sourcecode-rompen-reglas-propias-y-plugins-viejos&quot;&gt;6. Métodos eliminados de &lt;code&gt;context&lt;/code&gt; y &lt;code&gt;SourceCode&lt;/code&gt; rompen reglas propias y plugins viejos&lt;/h3&gt;
&lt;p&gt;Si escribes reglas propias, o dependes de un plugin interno que nadie toca desde 2023, aquí es donde muerde: &lt;code&gt;context.getCwd()&lt;/code&gt;, &lt;code&gt;context.getFilename()&lt;/code&gt;, &lt;code&gt;context.getPhysicalFilename()&lt;/code&gt;, &lt;code&gt;context.getSourceCode()&lt;/code&gt;, &lt;code&gt;context.parserOptions&lt;/code&gt; y &lt;code&gt;context.parserPath&lt;/code&gt; desaparecieron todos, junto con &lt;code&gt;SourceCode#getTokenOrCommentBefore()&lt;/code&gt;, &lt;code&gt;getTokenOrCommentAfter()&lt;/code&gt;, &lt;code&gt;isSpaceBetweenTokens()&lt;/code&gt; y &lt;code&gt;getJSDocComment()&lt;/code&gt;. La clase &lt;code&gt;Linter&lt;/code&gt; en sí perdió &lt;code&gt;defineParser()&lt;/code&gt;, &lt;code&gt;defineRule()&lt;/code&gt;, &lt;code&gt;defineRules()&lt;/code&gt; y &lt;code&gt;getRules()&lt;/code&gt;. Existen reemplazos para los que importan (&lt;code&gt;context.cwd&lt;/code&gt;, &lt;code&gt;context.filename&lt;/code&gt;, &lt;code&gt;context.sourceCode&lt;/code&gt;, &lt;code&gt;isSpaceBetween()&lt;/code&gt;), pero una regla que solo llama a la API eliminada en una ruta de código rara no truena hasta que esa ruta corre —lo que puede significar que pasa el CI durante semanas y luego falla en un archivo específico.&lt;/p&gt;
&lt;h3 id=&quot;7-eslintconfigts-necesita-un-jiti-actual&quot;&gt;7. &lt;code&gt;eslint.config.ts&lt;/code&gt; necesita un &lt;code&gt;jiti&lt;/code&gt; actual&lt;/h3&gt;
&lt;p&gt;ESLint 10 deja de soportar versiones de &lt;code&gt;jiti&lt;/code&gt; anteriores a 2.2.0, lo cual importa solo si escribes tu config en TypeScript (&lt;code&gt;eslint.config.ts&lt;/code&gt;) y dejas que &lt;code&gt;jiti&lt;/code&gt; la transpile al vuelo. Debajo de 2.2.0, la falla no dice “actualiza jiti” —sale como un error de resolución de módulos sin relación aparente, que manda a la mayoría por el camino de depuración equivocado primero.&lt;/p&gt;
&lt;h2 id=&quot;lo-que-no-se-rompe&quot;&gt;Lo que no se rompe&lt;/h2&gt;
&lt;p&gt;Vale la pena decirlo claro, porque el marco de “todo se rompe” tampoco es exacto. &lt;code&gt;typescript-eslint&lt;/code&gt; ya soporta ESLint 10 (su rango de peer declarado es &lt;code&gt;^8.57.0 || ^9.0.0 || ^10.0.0&lt;/code&gt;), así que un proyecto en una versión moderna de &lt;code&gt;typescript-eslint&lt;/code&gt; no necesita tocarlo para esta actualización. Y &lt;code&gt;FlatCompat&lt;/code&gt; —la válvula de escape real, ya cubierta arriba— sigue viva y con releases.&lt;/p&gt;
&lt;p&gt;El único lugar donde este stack te obliga la mano es Astro. El release actual de &lt;code&gt;eslint-plugin-astro&lt;/code&gt; exige ESLint 10 de plano, así que un proyecto de Astro que actualiza el plugin de Astro por cualquier otra razón arrastra a ESLint con él, haya sido planeado el bump de ESLint o no. Revisa los dos juntos antes de tocar cualquiera.&lt;/p&gt;
&lt;h2 id=&quot;cuándo-actualizar&quot;&gt;¿Cuándo actualizar?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Ahora mismo, si sigues en ESLint 9 o antes.&lt;/strong&gt; El fin de vida ya pasó hace un mes al momento de escribir esto; ya no queda versión de “espera a que se asiente el polvo” de ese argumento.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Esta semana, si estás en ESLint 9 con flat config ya migrado.&lt;/strong&gt; Tu superficie de riesgo es el cambio de búsqueda en monorepo y las tres reglas nuevas por default —las dos están a una corrida de lint de hacerse visibles, no a una reescritura.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Después de una auditoría, si mantienes reglas propias o un plugin interno.&lt;/strong&gt; Haz grep de tus implementaciones de reglas buscando &lt;code&gt;getCwd&lt;/code&gt;, &lt;code&gt;getFilename&lt;/code&gt;, &lt;code&gt;getSourceCode&lt;/code&gt; y &lt;code&gt;parserOptions&lt;/code&gt; antes de subir la versión, no después de que el CI te avise.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;En una rama dedicada primero, si corres un monorepo.&lt;/strong&gt; Analiza cada workspace de forma aislada, compara los resultados contra ESLint 9, y solo entonces haz merge —el cambio de búsqueda de config es exactamente el tipo de cosa que se ve bien en local y se porta mal en el paquete que no probaste tú mismo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Con &lt;code&gt;FlatCompat&lt;/code&gt; como puente, si dependes de una config compartida que nunca sacó soporte flat.&lt;/strong&gt; Eso no es razón para quedarte en un mayor sin soporte —es la herramienta que te deja actualizar hoy y terminar la migración real después.&lt;/p&gt;
&lt;p&gt;Para seguir:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/typescript-7-que-se-rompe/&quot;&gt;Qué se rompe al actualizar a TypeScript 7&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/node-26-lts-que-se-rompe/&quot;&gt;Node 26: qué se rompe al actualizar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/actualizar-a-astro-7/&quot;&gt;Actualizar a Astro 7&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuándo llegó ESLint 9 a su fin de vida? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; El 6 de agosto de 2026. Son seis meses después de que ESLint 10 salió en general availability, que es la ventana de soporte estándar que el proyecto ESLint le da al mayor anterior. Después de esa fecha, ESLint 9.x ya no recibe parches de seguridad ni corrección de bugs. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuándo se lanzó ESLint 10? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; ESLint 10.0.0 llegó a general availability el 6 de febrero de 2026, después de una alpha en noviembre de 2025. Al momento de escribir esto, la línea de patches actual está en el rango 10.4.x. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Puedo seguir usando .eslintrc.json con ESLint 10? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No. ESLint 10 no lee .eslintrc.js, .eslintrc.json, .eslintrc.yml ni .eslintignore bajo ninguna configuración o variable de entorno. La variable ESLINT_USE_FLAT_CONFIG, que antes forzaba el comportamiento viejo en ESLint 9, ya no se respeta. El único archivo de configuración soportado es eslint.config.js (o .mjs/.ts/.cjs). &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿FlatCompat sigue funcionando en ESLint 10? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Sí. FlatCompat viene del paquete separado @eslint/eslintrc y sigue funcionando en ESLint 10, dejándote jalar una config compartida de formato viejo dentro de un eslint.config.js real con compat.extends(). Asegúrate de que @eslint/eslintrc esté en una versión actual, porque las copias viejas son anteriores a ESLint 10. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuál es la versión mínima de Node.js para ESLint 10? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Node.js ^20.19.0, ^22.13.0, o 24 en adelante. Node 21.x y 23.x no tienen soporte, y tampoco lo tiene ninguna versión de patch debajo de 20.19.0 o 22.13.0 dentro de un mayor por lo demás soportado. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿typescript-eslint soporta ESLint 10? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Sí. El release actual de typescript-eslint declara un rango de peer dependency de ^8.57.0 || ^9.0.0 || ^10.0.0, así que un proyecto que ya está en una versión reciente de typescript-eslint no necesita ningún cambio ahí para pasar a ESLint 10. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Desarrollo Web</category><category>JavaScript</category><category>TypeScript</category><category>ESLint</category><category>Migración</category><category>Upgrade</category><author>Marco Orta</author></item><item><title>Pest 5 y PHPUnit 13: qué se rompe al actualizar (y qué falla en silencio)</title><link>https://ortamarco.me/blog/pest-5-que-se-rompe/</link><guid isPermaLink="true">https://ortamarco.me/blog/pest-5-que-se-rompe/</guid><description>Pest 5 solo pide una línea en composer.json. El trabajo real es PHPUnit 13, y lo que más duele no lanza error: deja de verificar lo que crees que verifica.</description><pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Pest 5 salió a finales de julio de 2026, corre directamente sobre PHPUnit 13 y exige PHP 8.4 como mínimo sin excepciones.&lt;/strong&gt; Pest en sí mismo no trae rupturas a nivel de API — todo el dolor viene heredado de las reglas de mocking más estrictas de PHPUnit 13. Y lo que de verdad le pega a los equipos no es un error fatal: es que PHPUnit 13 convierte varios patrones de mocking comunes en avisos de deprecación en vez de fallas, así que tu suite puede quedar completamente en verde mientras deja de verificar, en silencio, justo lo que creías que verificaba. Si estás en Laravel 12, hay un segundo problema más contundente: el plugin oficial de Pest para Laravel en su versión 5 pide Laravel 13.23 o superior, así que el plugin ni siquiera se instala.&lt;/p&gt;
&lt;h2 id=&quot;de-dónde-vienes&quot;&gt;¿De dónde vienes?&lt;/h2&gt;
&lt;p&gt;El esfuerzo del salto no es parejo. Depende de cuántas versiones mayores de PHPUnit te estés saltando de un solo golpe.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pest 4 (PHPUnit 12) → Pest 5 (PHPUnit 13).&lt;/strong&gt; Es el salto pensado, el más pequeño. Pest 4 ya pedía PHP 8.3, así que el único piso que subes es de 8.3 a 8.4. Absorbes una sola tanda de cambios de PHPUnit (12 → 13).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pest 3 o anterior (PHPUnit 11 o más viejo) → Pest 5.&lt;/strong&gt; Aquí absorbes dos tandas apiladas: PHPUnit 11 → 12 ya había eliminado los metadatos por docblock (necesitas atributos nativos de PHP: &lt;code&gt;#[Test]&lt;/code&gt;, &lt;code&gt;#[DataProvider]&lt;/code&gt;, etc.) y los mocks de clases abstractas o traits, encima de todo lo que cambia PHPUnit 13 más abajo. Saltar de 3 a 5 en un solo &lt;code&gt;composer update&lt;/code&gt; es como una actualización de 20 minutos se convierte en una de dos días — pasa primero por 4, o al menos prueba el salto a 12 de forma aislada.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PHPUnit puro, sin Pest, en PHPUnit 12.5.&lt;/strong&gt; La propia regla de actualización de PHPUnit es explícita: no muevas a 13 si tu suite todavía no corre limpia en 12.5. Si &lt;code&gt;phpunit --display-deprecations&lt;/code&gt; muestra algo, arréglalo primero, en su lugar, antes de tocar la restricción de versión de Pest o PHPUnit.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;el-piso-que-no-se-negocia-php-84&quot;&gt;El piso que no se negocia: PHP 8.4&lt;/h2&gt;
&lt;p&gt;Pest 5 exige &lt;strong&gt;PHP 8.4.0 o superior&lt;/strong&gt;, punto. No hay un “en la práctica funciona en 8.3” como sí existe en algunos saltos de versión de Laravel. Si tu equipo sigue en PHP 8.1, 8.2 u 8.3, eso es trabajo de infraestructura que pasa &lt;em&gt;antes&lt;/em&gt; de tocar el &lt;code&gt;composer.json&lt;/code&gt; de Pest — no en paralelo.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;php &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt;          &lt;span class=&quot;token comment&quot;&gt;# necesitas 8.4.0 o superior&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Haz el salto de PHP como su propio despliegue, verificado en verde con el stack de pruebas viejo, antes de cambiar también el stack de pruebas. De lo contrario, un CI en rojo después de la actualización no te dice cuál de los dos cambios fue el culpable.&lt;/p&gt;
&lt;h2 id=&quot;rupturas-ordenadas-por-lo-que-de-verdad-duele&quot;&gt;Rupturas, ordenadas por lo que de verdad duele&lt;/h2&gt;
&lt;h3 id=&quot;1-laravel-12-no-puede-usar-el-plugin-de-pest-5-para-laravel-bloqueante-no-silencioso&quot;&gt;1. Laravel 12 no puede usar el plugin de Pest 5 para Laravel (bloqueante, no silencioso)&lt;/h3&gt;
&lt;p&gt;Esta es la que nadie menciona cuando venden la actualización como “solo una línea”, y es lo primero que detiene en seco a un equipo de Laravel. &lt;code&gt;pestphp/pest-plugin-laravel&lt;/code&gt; en su versión 5.0.1 declara:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&amp;quot;require&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&amp;quot;php&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;^8.4&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&amp;quot;laravel/framework&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;^13.23.0&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&amp;quot;pestphp/pest&amp;quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;^5.0.1&amp;quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si tu app está en Laravel 12, &lt;code&gt;composer update&lt;/code&gt; se va a negar a resolver &lt;code&gt;pest-plugin-laravel ^5.0&lt;/code&gt; — Composer reporta un conflicto, no una falla de prueba misteriosa. La solución no es un problema de Pest: necesitas Laravel 13.23+ antes de que el plugin de Laravel para Pest 5 sea siquiera instalable. Si estás actualizando ambos frentes a la vez, ordénalo — primero Laravel, después Pest — o te vas a pasar una tarde depurando un árbol de dependencias que nunca iba a resolver.&lt;/p&gt;
&lt;h3 id=&quot;2-any-y-with-sin-expects--la-suite-que-pasa-pero-deja-de-verificar-silencioso&quot;&gt;2. &lt;code&gt;any()&lt;/code&gt; y &lt;code&gt;with*()&lt;/code&gt; sin &lt;code&gt;expects()&lt;/code&gt; — la suite que pasa pero deja de verificar (silencioso)&lt;/h3&gt;
&lt;p&gt;Este es el cambio que más importa, porque nada en tu salida de CI te avisa que ocurrió. PHPUnit 13 deprecra de forma dura dos patrones que antes eran la forma cómoda y por defecto de configurar un mock:&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Antes: funciona, y significa en silencio &amp;quot;cualquier número de llamadas, sin verificar orden&amp;quot;&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$mock&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;handle&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;equalTo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$payload&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// PHPUnit 13: emite una deprecación, pero igual corre y sigue &amp;quot;pasando&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Las deprecaciones en PHPUnit son avisos, no fallas, a menos que configures explícitamente &lt;code&gt;failOnDeprecation&lt;/code&gt; (o &lt;code&gt;failOnPhpunitDeprecation&lt;/code&gt;, la propia de PHPUnit) en tu suite. Lo que significa que el escenario realista no es “la actualización rompe el CI el día uno” — es “el CI se queda en verde durante semanas mientras acumula decenas de avisos de deprecación que nadie lee, y después alguien activa el fallo estricto por deprecación, o PHPUnit 14 elimina el patrón de golpe y la suite se pone roja toda junta, en una fecha que tú no controlas”.&lt;/p&gt;
&lt;p&gt;La corrección es hacer explícita la intención en vez de depender del comportamiento implícito por defecto:&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Si de verdad quieres verificar que la llamada ocurre:&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$mock&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;expects&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;once&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;handle&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;equalTo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$payload&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Si no te importa si se llama o no — usa un stub, no un mock:&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$stub&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createStub&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name static-context&quot;&gt;HandlerInterface&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$stub&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;handle&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;willReturn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El propio matcher &lt;code&gt;any()&lt;/code&gt; de conteo de invocaciones queda deprecado de forma dura por la misma razón: combinar &lt;code&gt;expects($this-&amp;gt;any())&lt;/code&gt; con un mock es una contradicción — los mocks existen para verificar interacciones, y &lt;code&gt;any()&lt;/code&gt; significa “no me importa si esto pasa”. Los propios mantenedores de PHPUnit lo dicen sin rodeos: si no quieres verificar nada, lo que querías era un stub.&lt;/p&gt;
&lt;h3 id=&quot;3-createpartialmock-de-laravel-choca-con-la-regla-2-silencioso-específico-de-laravel&quot;&gt;3. &lt;code&gt;createPartialMock()&lt;/code&gt; de Laravel choca con la regla #2 (silencioso, específico de Laravel)&lt;/h3&gt;
&lt;p&gt;Es la misma deprecación de arriba, pero vale la pena señalarla aparte porque golpea a un helper que la mayoría de las suites de Laravel usan sin pensarlo dos veces. El trait de testing de Laravel envuelve el &lt;code&gt;createPartialMock()&lt;/code&gt; de PHPUnit, y la implementación interna de ese envoltorio dispara la deprecación de “with*() sin expects()” en PHPUnit 13 aunque tu código de prueba se vea perfectamente sano:&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Dispara la deprecación en PHPUnit 13, aunque se vea inocente&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$class&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createPartialMock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name static-context&quot;&gt;NightlyCommand&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;runSequenceOfCommands&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Corregido: te saltas el envoltorio y usas el builder de PHPUnit directamente&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$class&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getMockBuilder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name static-context&quot;&gt;NightlyCommand&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;onlyMethods&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;runSequenceOfCommands&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getMock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Búscalo con grep antes de actualizar, porque el número de deprecaciones puede ser grande si el patrón es común en tu suite:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rln&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;createPartialMock&amp;quot;&lt;/span&gt; tests/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4-eliminaciones-reales--estas-sí-fallan-de-inmediato-sin-ambigüedad&quot;&gt;4. Eliminaciones reales — estas sí fallan, de inmediato, sin ambigüedad&lt;/h3&gt;
&lt;p&gt;A diferencia de las dos anteriores, estas ya venían deprecadas de forma dura desde PHPUnit 12 y en 13 simplemente ya no existen. Lanzan un &lt;code&gt;Error&lt;/code&gt; fatal, no un aviso, así que salen a la luz desde la primera corrida — que en realidad es la categoría fácil de resolver:&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Eliminado en PHPUnit 13 — error fatal, no una deprecación&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;string&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;assertContainsOnly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;string&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$collection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;assertNotContainsOnly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;int&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$collection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Sus reemplazos especializados, disponibles desde PHPUnit 11.5&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// o directamente assertIsString($value)&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;assertContainsOnlyString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$collection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;assertContainsNotOnlyInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$collection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Para colecciones de objetos sigue existiendo la de siempre&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;assertContainsOnlyInstancesOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name static-context&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$collection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;También se eliminó el flag de CLI &lt;code&gt;--dont-report-useless-tests&lt;/code&gt;, &lt;code&gt;Configuration::includeTestSuite()&lt;/code&gt; / &lt;code&gt;excludeTestSuite()&lt;/code&gt;, el atributo &lt;code&gt;#[CoversNothing]&lt;/code&gt; en métodos individuales (ahora solo a nivel de clase), el atributo &lt;code&gt;#[RunClassInSeparateProcess]&lt;/code&gt;, y el soporte para cadenas de restricción de versión sin un operador de comparación explícito. Ninguno de estos es común en una suite típica de Laravel, pero si mantienes un paquete con extensiones propias de PHPUnit o herramientas de CI que invocan &lt;code&gt;phpunit&lt;/code&gt; directamente, revisa tus flags.&lt;/p&gt;
&lt;p&gt;Si vienes de &lt;code&gt;withConsecutive()&lt;/code&gt; específicamente: eso ya es viejo — se eliminó desde PHPUnit 10 — pero PHPUnit 13 por fin trae reemplazos decentes en vez de dejarte escribiendo workarounds incómodos de conteo de llamadas:&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Nuevo en PHPUnit 13&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$mock&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;expects&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;once&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;handle&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;withParameterSetsInOrder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;first&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;second&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token variable&quot;&gt;$mock&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;expects&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;exactly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;handle&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;withParameterSetsInAnyOrder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;first&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;second&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5-el-test-impact-analysis-necesita-un-driver-de-cobertura-o-simplemente-no-corre-silencioso&quot;&gt;5. El Test Impact Analysis necesita un driver de cobertura, o simplemente no corre (silencioso)&lt;/h3&gt;
&lt;p&gt;La función estrella de Pest 5 — el Tia Engine, que promete reducir una suite de Laravel de 10 minutos a unos 4 segundos en corridas posteriores al solo volver a correr las pruebas afectadas por tu diff — necesita PCOV o Xdebug instalado para grabar su grafo de dependencias inicial. Sin uno de los dos, &lt;strong&gt;Pest no lanza error ni avisa fuerte; TIA simplemente nunca se activa&lt;/strong&gt;, y tu suite sigue corriendo completa cada vez, en silencio. Si activaste TIA esperando la mejora de velocidad y no pasó nada, revisa si tienes un driver de cobertura antes de asumir que la función está rota:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;php &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-iE&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;pcov|xdebug&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Para una configuración de equipo, el patrón sensato es que el CI grabe el grafo base una vez por cada merge a &lt;code&gt;main&lt;/code&gt; (donde activar un driver de cobertura sale barato) y que las máquinas locales consuman ese grafo en caché, en vez de que cada desarrollador corra una línea base instrumentada con cobertura en su propia máquina.&lt;/p&gt;
&lt;h3 id=&quot;6-sin-cambios-de-configuración--de-verdad-no-hay-nada-que-hacer-aquí&quot;&gt;6. Sin cambios de configuración — de verdad no hay nada que hacer aquí&lt;/h3&gt;
&lt;p&gt;Vale la pena decirlo de forma explícita porque la gente lo espera: la guía de actualización de Pest 5 documenta &lt;strong&gt;ningún cambio en &lt;code&gt;phpunit.xml&lt;/code&gt; ni en ningún archivo de configuración de Pest&lt;/strong&gt;. Si tu suite corre limpia bajo las reglas de PHPUnit 13, el bump de versión en &lt;code&gt;composer.json&lt;/code&gt; es toda la migración. Eso es lo bastante raro en este ecosistema como para no darle más vueltas de las necesarias.&lt;/p&gt;
&lt;h2 id=&quot;cuándo-actualizar&quot;&gt;¿Cuándo actualizar?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Estás en Pest 4 / PHPUnit 12, PHP 8.4 ya es tu piso, y &lt;code&gt;phpunit --display-deprecations&lt;/code&gt; sale limpio.&lt;/strong&gt; Actualiza ahora. Esto se acerca a los “2 minutos” que promete la documentación, y solo TIA ya vale la pena si tu suite pasó de un par de miles de pruebas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Estás en Pest 4 pero todavía en PHP 8.3.&lt;/strong&gt; Sube PHP primero, verifica que la suite actual siga en verde y sin deprecaciones en 8.4, y luego sube Pest como un paso aparte.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Estás en Pest 3 o en PHPUnit 11 o anterior, sin Pest.&lt;/strong&gt; No te saltes directo a 5. Pasa primero por la tanda de PHPUnit 12 (atributos en vez de docblocks, sin mocks de abstractas ni traits), confirma que la suite está limpia, y toma el salto de 12 a 13 aparte.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Eres un equipo de Laravel todavía en Laravel 12.&lt;/strong&gt; El plugin de Laravel para Pest 5 está fuera de tu alcance hasta que estés en Laravel 13.23+. O actualizas Laravel primero, o te quedas en &lt;code&gt;pest-plugin-laravel ^4.0&lt;/code&gt; con Pest 4 hasta que lo hagas — no dejes que el resolvedor de dependencias de Composer tome esa decisión por ti a medio camino.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cualquiera con uso pesado de mocks en la suite, sin importar la versión.&lt;/strong&gt; Corre el grep de deprecaciones antes de tocar siquiera el &lt;code&gt;composer.json&lt;/code&gt;. El costo real de esta actualización no es el bump de versión — es descubrir cuántos de tus mocks solo “pasaban” porque en realidad no estaban verificando nada.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Para seguir leyendo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/laravel-13-breaking-changes/&quot;&gt;Qué se rompe al actualizar a Laravel 13&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/php-8-6-novedades/&quot;&gt;PHP 8.6: todas las novedades y qué se rompe al actualizar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/como-actualizar-un-proyecto-de-laravel-9-a-laravel-10/&quot;&gt;Cómo actualizar un proyecto de Laravel 9 a Laravel 10&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Pest 5 exige PHP 8.4? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Sí. Pest 5 requiere PHP 8.4.0 o superior como mínimo absoluto, sin excepciones para PHP 8.1, 8.2 u 8.3. Esto viene de que Pest 5 corre sobre PHPUnit 13, que a su vez exige PHP 8.4 o posterior. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Puedo usar Pest 5 con Laravel 12? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No con el plugin oficial. Desde la versión 5.0.1 de pest-plugin-laravel, el paquete exige laravel/framework ^13.23.0, así que Composer se niega a instalarlo en una aplicación con Laravel 12. Necesitas actualizar a Laravel 13.23 o superior primero, o quedarte en Pest 4 con pest-plugin-laravel ^4.0 mientras tanto. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué pasa si mi suite de pruebas ya tiene avisos de deprecación de PHPUnit antes de actualizar? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Los avisos de deprecación de PHPUnit no hacen fallar un build por defecto, así que una suite puede aparentar que pasa mientras usa patrones que PHPUnit 13 marca, como el matcher de invocaciones any() o las llamadas con*() sin un expects() explícito. PHPUnit recomienda oficialmente no actualizar a 13 a menos que tu suite ya corra limpia, sin avisos de deprecación, en PHPUnit 12.5, porque esos avisos se convierten en eliminaciones definitivas en una futura versión mayor sin más aviso. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Se eliminó el matcher any() en PHPUnit 13? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No, todavía no. El matcher de conteo de invocaciones any() está deprecado de forma dura en PHPUnit 13, lo que significa que todavía funciona pero dispara un aviso de deprecación, y está programado para eliminarse en PHPUnit 14. La corrección recomendada es usar una expectativa explícita como expects($this-&amp;gt;once()) si quieres verificar que una llamada ocurre, o cambiar a createStub() si no necesitas verificación en absoluto. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Necesito Xdebug o PCOV para usar el Test Impact Analysis de Pest 5? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Sí. El Tia Engine necesita un driver de cobertura de código, ya sea PCOV o Xdebug, para grabar el grafo de dependencias en su primera corrida. Sin uno instalado y activado, el Test Impact Analysis no lanza ningún error; simplemente no se activa, y tu suite sigue corriendo todas las pruebas en cada corrida. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuánto tarda de verdad la actualización a Pest 5? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; La documentación oficial estima unos 2 minutos para el bump de versión en composer.json, y esa estimación es precisa para la parte específica de Pest. El costo real de tiempo está en auditar tu suite contra las reglas de PHPUnit 13 antes de actualizar, sobre todo los patrones de uso de mocks y, para las apps de Laravel, confirmar que ya estás en Laravel 13.23 o superior. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Desarrollo Web</category><category>PHP</category><category>Laravel</category><category>Testing</category><category>Migración</category><category>Upgrade</category><author>Marco Orta</author></item><item><title>PHP 8.6: qué se rompe de verdad al actualizar</title><link>https://ortamarco.me/blog/php-8-6-que-se-rompe/</link><guid isPermaLink="true">https://ortamarco.me/blog/php-8-6-que-se-rompe/</guid><description>PHP 8.6 llega el 19 de noviembre de 2026: tres defaults de sesión que tumban logins en silencio, el trim() que cambia y toda la lista de deprecaciones.</description><pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;PHP 8.6 sale el 19 de noviembre de 2026, y lo que más probablemente te va a tumbar producción no es ninguna función eliminada — es que cambian los defaults de tres directivas de sesión.&lt;/strong&gt; &lt;code&gt;session.use_strict_mode&lt;/code&gt;, &lt;code&gt;session.cookie_httponly&lt;/code&gt; y &lt;code&gt;session.cookie_samesite&lt;/code&gt; cambian de valor, y el modo de falla es silencioso: nada lanza excepción, tus usuarios simplemente dejan de mantener la sesión iniciada en peticiones cross-site.&lt;/p&gt;
&lt;p&gt;Todo lo demás son deprecaciones, es decir, avisos y no errores fatales. Pero hay cuatro cambios de comportamiento genuinos escondidos entre ellas, y dos de esos cambian el resultado sin decir una palabra.&lt;/p&gt;
&lt;p&gt;Esta es la lista de rupturas. Si buscas las novedades — Partial Function Application, &lt;code&gt;clamp()&lt;/code&gt;, el enum &lt;code&gt;SortDirection&lt;/code&gt; — están en &lt;a href=&quot;https://ortamarco.me/blog/php-8-6-novedades/&quot;&gt;todas las novedades de PHP 8.6&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;qué-tan-cerrada-está-esta-lista&quot;&gt;Qué tan cerrada está esta lista&lt;/h2&gt;
&lt;p&gt;Vale la pena aclararlo, porque los artículos de “qué rompe PHP 8.6” empezaron a publicarse antes de que hubiera algo que reportar.&lt;/p&gt;
&lt;p&gt;El calendario es público: la alpha 1 salió el 2 de julio, la beta 1 con el &lt;em&gt;soft feature freeze&lt;/em&gt; llegó el 13 de agosto, el &lt;strong&gt;hard feature freeze es el 22 de septiembre&lt;/strong&gt;, la RC1 llega el 24 de septiembre y la RC4 el 5 de noviembre, y la disponibilidad general es el &lt;strong&gt;19 de noviembre de 2026&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Lo que eso significa para este artículo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Las deprecaciones ya están cerradas.&lt;/strong&gt; Vienen de RFCs que ya se votaron, y una RFC votada no se desvota.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;La lista todavía puede crecer.&lt;/strong&gt; Entre hoy y el hard freeze pueden entrar cambios aprobados por los &lt;em&gt;release managers&lt;/em&gt;, y algunos bugfixes posteriores añaden notas de actualización.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Todavía no hay página oficial de migración.&lt;/strong&gt; &lt;code&gt;php.net/manual/es/migration86.php&lt;/code&gt; no existe al momento de escribir esto — la fuente de verdad hoy es el archivo &lt;code&gt;UPGRADING&lt;/code&gt; en la rama master de php-src, de donde sale todo lo de abajo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Voy a actualizar este artículo después del hard freeze y la RC1 del 24 de septiembre, cuando la lista quede prácticamente cerrada.&lt;/p&gt;
&lt;h2 id=&quot;la-que-sí-va-a-tumbar-producción&quot;&gt;La que sí va a tumbar producción&lt;/h2&gt;
&lt;h3 id=&quot;los-defaults-de-seguridad-de-sesión-cambian&quot;&gt;Los defaults de seguridad de sesión cambian&lt;/h3&gt;
&lt;p&gt;Tres valores de &lt;code&gt;php.ini&lt;/code&gt; cambian:&lt;/p&gt;

























&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Directiva&lt;/th&gt;&lt;th&gt;Antes&lt;/th&gt;&lt;th&gt;En PHP 8.6&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;session.use_strict_mode&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;session.cookie_httponly&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;session.cookie_samesite&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;quot;&amp;quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;quot;Lax&amp;quot;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Cada uno por separado es el default correcto. Juntos son el cambio más disruptivo de la versión, porque &lt;strong&gt;ninguno de los tres lanza un error&lt;/strong&gt;. Tu aplicación sigue corriendo y algunos usuarios simplemente dejan de estar logueados.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;cookie_samesite = &amp;quot;Lax&amp;quot;&lt;/code&gt;&lt;/strong&gt; es el que muerde. Una cookie &lt;code&gt;Lax&lt;/code&gt; no viaja en peticiones &lt;strong&gt;POST&lt;/strong&gt; cross-site. Si algo le hace POST a tu aplicación desde otro origen — una pasarela de pago que regresa al usuario por POST, el callback de un proveedor de identidad, un formulario embebido, un webhook que depende de la sesión — esa petición ahora llega sin sesión. El usuario aparece en la pantalla de login sin ninguna razón visible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;cookie_httponly = 1&lt;/code&gt;&lt;/strong&gt; significa que JavaScript ya no puede leer la cookie de sesión con &lt;code&gt;document.cookie&lt;/code&gt;. Es lo correcto, y rompe cualquier código de front-end que leyera el ID de sesión directamente — algunos snippets viejos de analítica y auth por AJAX hecho a mano hacen justo eso.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;use_strict_mode = 1&lt;/code&gt;&lt;/strong&gt; hace que PHP rechace IDs de sesión que él mismo no generó, que es la corrección real contra la fijación de sesión (&lt;em&gt;session fixation&lt;/em&gt;). Rompe flujos que meten un ID de sesión desde afuera.&lt;/p&gt;
&lt;p&gt;Si necesitas el comportamiento anterior de forma temporal, fíjalo explícitamente en vez de depender del default previo:&lt;/p&gt;
&lt;pre class=&quot;language-ini&quot; data-language=&quot;ini&quot;&gt;&lt;code class=&quot;language-ini&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;; solo como parche mientras arreglas la causa real&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;session.cookie_samesite&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token value attr-value&quot;&gt;&amp;quot;&lt;span class=&quot;token inner-value&quot;&gt;None&lt;/span&gt;&amp;quot;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;session.cookie_secure&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token value attr-value&quot;&gt;1   ; obligatorio siempre que uses SameSite=None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ojo: los navegadores rechazan &lt;code&gt;SameSite=None&lt;/code&gt; sin &lt;code&gt;Secure&lt;/code&gt;, así que esto no es un revert de una línea.&lt;/p&gt;
&lt;p&gt;El consejo honesto: no lo reviertas. Fija estas tres directivas explícitamente en tu &lt;code&gt;php.ini&lt;/code&gt; &lt;strong&gt;hoy mismo&lt;/strong&gt;, en PHP 8.5, y averigua qué se rompe mientras todavía controlas cuándo. Eso convierte una sorpresa del día del lanzamiento en una tarde de martes cualquiera.&lt;/p&gt;
&lt;h2 id=&quot;los-cambios-de-comportamiento-que-no-hacen-ruido&quot;&gt;Los cambios de comportamiento que no hacen ruido&lt;/h2&gt;
&lt;h3 id=&quot;trim-ahora-elimina-el-form-feed&quot;&gt;&lt;code&gt;trim()&lt;/code&gt; ahora elimina el form feed&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;trim()&lt;/code&gt;, &lt;code&gt;ltrim()&lt;/code&gt; y &lt;code&gt;rtrim()&lt;/code&gt; agregan &lt;code&gt;\f&lt;/code&gt; (form feed, &lt;code&gt;0x0C&lt;/code&gt;) a su lista de caracteres por defecto. Era el único carácter de espacio en blanco ASCII que no eliminaban.&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&amp;quot;hola\f&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// PHP 8.5 → &amp;quot;hola\f&amp;quot;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// PHP 8.6 → &amp;quot;hola&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Casi siempre es lo que querías. Lo listo aquí porque cambia la salida en silencio, y si tienes pruebas que comparan cadenas exactas al parsear archivos de ancho fijo, streams de impresión viejos o cualquier cosa que arrastre form feeds, van a empezar a fallar sin causa obvia.&lt;/p&gt;
&lt;h3 id=&quot;los-nul-bytes-ahora-lanzan-en-vez-de-tolerarse&quot;&gt;Los NUL bytes ahora lanzan en vez de tolerarse&lt;/h3&gt;
&lt;p&gt;Un grupo grande de funciones ahora lanza &lt;code&gt;ValueError&lt;/code&gt; cuando reciben una cadena con un byte NUL, en vez de truncar o comportarse de forma impredecible. La lista incluye &lt;code&gt;getenv()&lt;/code&gt;, &lt;code&gt;putenv()&lt;/code&gt;, &lt;code&gt;parse_str()&lt;/code&gt;, &lt;code&gt;setlocale()&lt;/code&gt;, &lt;code&gt;dl()&lt;/code&gt;, &lt;code&gt;openlog()&lt;/code&gt;, &lt;code&gt;proc_open()&lt;/code&gt; (el argumento &lt;code&gt;$cwd&lt;/code&gt;), y cerca de veinte funciones de sistema de archivos — &lt;code&gt;file_exists()&lt;/code&gt;, &lt;code&gt;is_file()&lt;/code&gt;, &lt;code&gt;filesize()&lt;/code&gt;, &lt;code&gt;stat()&lt;/code&gt; y sus parientes.&lt;/p&gt;
&lt;p&gt;Es un endurecimiento de seguridad: los NUL bytes en esos argumentos han sido históricamente un vector de &lt;em&gt;path traversal&lt;/em&gt;. El impacto práctico es que el código que pasa entrada de usuario sin sanear a validaciones de archivos ahora recibe una excepción ruidosa en vez de un silencio sin sentido:&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// PHP 8.5: devuelve false, sin ninguna señal&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// PHP 8.6: lanza ValueError&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;file_exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$_GET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;path&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// cuando $_GET[&amp;#39;path&amp;#39;] contiene &amp;quot;\0&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si tu aplicación hace esto, la excepción te está avisando de un bug que ya existía.&lt;/p&gt;
&lt;h3 id=&quot;unpack-reinterpreta--y-&quot;&gt;&lt;code&gt;unpack()&lt;/code&gt; reinterpreta &lt;code&gt;&amp;lt;&lt;/code&gt; y &lt;code&gt;&amp;gt;&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;unpack()&lt;/code&gt; ahora trata &lt;code&gt;&amp;lt;&lt;/code&gt; y &lt;code&gt;&amp;gt;&lt;/code&gt; después de un código de formato como modificadores de &lt;em&gt;endianness&lt;/em&gt;, no como parte del nombre. Esto cambia cómo se parsean cadenas de formato que ya tenías:&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token function&quot;&gt;unpack&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&amp;quot;s&amp;lt;valor&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// PHP 8.5 → clave &amp;quot;&amp;lt;valor&amp;quot;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// PHP 8.6 → short little-endian, clave &amp;quot;valor&amp;quot;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;unpack&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&amp;quot;C&amp;gt;nombre&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// PHP 8.6 → lanza excepción (C no tiene endianness)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Es un caso puntual, pero si parseas formatos binarios es una ruptura dura, no una deprecación. Busca &lt;code&gt;unpack(&lt;/code&gt; en tu código y revisa si algún formato tiene &lt;code&gt;&amp;lt;&lt;/code&gt; o &lt;code&gt;&amp;gt;&lt;/code&gt; dentro de un nombre.&lt;/p&gt;
&lt;h3 id=&quot;sessionhandlerinterface-pide-dos-métodos-más&quot;&gt;&lt;code&gt;SessionHandlerInterface&lt;/code&gt; pide dos métodos más&lt;/h3&gt;
&lt;p&gt;Los manejadores de sesión personalizados que no implementan &lt;code&gt;create_sid()&lt;/code&gt; ni &lt;code&gt;validateId()&lt;/code&gt; ahora emiten deprecaciones. Si escribiste a mano un manejador de sesiones en base de datos o Redis, lo más probable es que implemente los seis métodos originales y no estos dos — y &lt;code&gt;validateId()&lt;/code&gt; es justo lo que hace funcionar de verdad a &lt;code&gt;use_strict_mode&lt;/code&gt;, así que este punto se conecta directo con el cambio de arriba.&lt;/p&gt;
&lt;h2 id=&quot;las-deprecaciones&quot;&gt;Las deprecaciones&lt;/h2&gt;
&lt;p&gt;Ninguna de estas es fatal en 8.6. Emiten &lt;code&gt;E_DEPRECATED&lt;/code&gt;, y son la lista de eliminación para PHP 9.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;return&lt;/code&gt; dentro de &lt;code&gt;finally&lt;/code&gt;.&lt;/strong&gt; Regresar un valor desde un bloque &lt;code&gt;finally&lt;/code&gt; descarta cualquier valor de retorno o excepción del &lt;code&gt;try&lt;/code&gt;, que casi siempre está tapando un bug. Ahora está deprecado.&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;finally&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// deprecado — gana en silencio, regresa 2&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Regresar un valor desde un constructor.&lt;/strong&gt; Ya está cubierto con detalle en el post de novedades, pero la versión corta: es deprecación &lt;strong&gt;en tiempo de compilación&lt;/strong&gt;, la ves aunque el código nunca se ejecute, y pasó la votación 39 a 0.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;array_filter()&lt;/code&gt; lanza excepción con un &lt;code&gt;$mode&lt;/code&gt; inválido.&lt;/strong&gt; Antes ignoraba en silencio un modo incorrecto; ahora lanza &lt;code&gt;ValueError&lt;/code&gt;. Es una mejora estricta, pero puede tumbar código que llevaba años pasando una constante equivocada sin que nadie se diera cuenta.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;php://filter&lt;/code&gt; limita los filtros encadenados.&lt;/strong&gt; Endurecimiento contra una técnica conocida de explotación por cadenas de filtros. Solo te toca si construyes cadenas de filtros a propósito.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Todo mbregex.&lt;/strong&gt; Cada función &lt;code&gt;mb_ereg*&lt;/code&gt; queda deprecada, porque Oniguruma, la librería detrás de ellas, dejó de tener mantenimiento. Es la deprecación más grande de la versión en superficie de código. La migración es a PCRE — &lt;code&gt;preg_match()&lt;/code&gt; y compañía, con el modificador &lt;code&gt;u&lt;/code&gt; para Unicode. El post de novedades trae el detalle completo de las 14 funciones y el checklist de migración; aquí basta con saber que &lt;strong&gt;no es opcional para siempre&lt;/strong&gt;: se elimina en PHP 9.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Alias de verificación y conversión de tipos.&lt;/strong&gt; &lt;code&gt;is_double()&lt;/code&gt;, &lt;code&gt;is_long()&lt;/code&gt;, &lt;code&gt;is_integer()&lt;/code&gt; y &lt;code&gt;doubleval()&lt;/code&gt; quedan deprecadas a favor de &lt;code&gt;is_float()&lt;/code&gt;, &lt;code&gt;is_int()&lt;/code&gt; y &lt;code&gt;floatval()&lt;/code&gt;. También se deprecan &lt;code&gt;metaphone()&lt;/code&gt;, &lt;code&gt;strcoll()&lt;/code&gt;, &lt;code&gt;spl_object_hash()&lt;/code&gt;, &lt;code&gt;spl_classes()&lt;/code&gt; y la bandera de ordenamiento &lt;code&gt;SORT_LOCALE_STRING&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;spl_object_hash()&lt;/code&gt; es la que vale la pena marcar: reemplázala por &lt;code&gt;spl_object_id()&lt;/code&gt;, que casi seguro es lo que en realidad querías.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Métodos de CSV en SPL y de &lt;code&gt;ArrayIterator&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;SplFileObject::fgetcsv()&lt;/code&gt;, &lt;code&gt;fputcsv()&lt;/code&gt;, &lt;code&gt;setCsvControl()&lt;/code&gt; y &lt;code&gt;getCsvControl()&lt;/code&gt; quedan deprecados, junto con diez métodos de &lt;code&gt;ArrayIterator&lt;/code&gt;: &lt;code&gt;getFlags()&lt;/code&gt;, &lt;code&gt;setFlags()&lt;/code&gt;, &lt;code&gt;asort()&lt;/code&gt;, &lt;code&gt;ksort()&lt;/code&gt;, &lt;code&gt;uasort()&lt;/code&gt;, &lt;code&gt;uksort()&lt;/code&gt;, &lt;code&gt;natsort()&lt;/code&gt;, &lt;code&gt;natcasesort()&lt;/code&gt;, &lt;code&gt;serialize()&lt;/code&gt; y &lt;code&gt;unserialize()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Argumentos de objeto donde nunca tuvieron sentido.&lt;/strong&gt; &lt;code&gt;array_walk()&lt;/code&gt;, &lt;code&gt;mb_convert_variables()&lt;/code&gt; y los filtros de stream de zlib y bz2 ahora deprecan argumentos tipo objeto.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cabos sueltos de mysqli.&lt;/strong&gt; &lt;code&gt;mysqli_get_charset()&lt;/code&gt; y &lt;code&gt;mysqli_stmt_init()&lt;/code&gt; quedan deprecadas. La opción &lt;code&gt;classmap&lt;/code&gt; de SOAP ahora rechaza claves enteras.&lt;/p&gt;
&lt;h2 id=&quot;qué-hacer-en-realidad-en-orden&quot;&gt;Qué hacer en realidad, en orden&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fija las tres directivas de sesión explícitamente en PHP 8.5, hoy.&lt;/strong&gt; Es todo el riesgo de la versión concentrado en un solo cambio, y es el único que puedes probar antes de que 8.6 exista siquiera. Revisa cada POST cross-site que le llegue a tu aplicación.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Activa el reporte de deprecaciones y corre tu suite de tests.&lt;/strong&gt; &lt;code&gt;error_reporting(E_ALL)&lt;/code&gt; con un log que de verdad revises. La mayor parte de esta versión aparece ahí.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Busca los nombres específicos.&lt;/strong&gt; &lt;code&gt;mb_ereg&lt;/code&gt;, &lt;code&gt;is_double&lt;/code&gt;, &lt;code&gt;is_long&lt;/code&gt;, &lt;code&gt;is_integer&lt;/code&gt;, &lt;code&gt;doubleval&lt;/code&gt;, &lt;code&gt;spl_object_hash&lt;/code&gt;, &lt;code&gt;metaphone&lt;/code&gt;, &lt;code&gt;strcoll&lt;/code&gt;, &lt;code&gt;unpack(&lt;/code&gt;. Es una pasada de veinte minutos y cubre casi toda la lista de deprecaciones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Revisa tus manejadores de sesión personalizados&lt;/strong&gt; en busca de &lt;code&gt;create_sid()&lt;/code&gt; y &lt;code&gt;validateId()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No toques &lt;code&gt;trim()&lt;/code&gt;&lt;/strong&gt; a menos que falle alguna prueba. Si falla una, aprendiste algo sobre tus datos de entrada.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;lo-que-no-se-rompe&quot;&gt;Lo que NO se rompe&lt;/h2&gt;
&lt;p&gt;Vale la pena decirlo, porque la ansiedad de actualizar rellena huecos que no existen. PHP 8.6 no elimina nada que estuviera deprecado en 8.x — las eliminaciones llegan hasta PHP 9. Tus propiedades tipadas, enums, clases &lt;code&gt;readonly&lt;/code&gt;, fibers y atributos siguen intactos. Si tu aplicación corre limpia en 8.5 con las deprecaciones silenciadas, el peor caso realista para 8.6 es un log ruidoso más el cambio de sesión.&lt;/p&gt;
&lt;p&gt;Ese cambio de sesión, eso sí, no es un detalle menor. Es la razón por la que existe este artículo.&lt;/p&gt;
&lt;h2 id=&quot;preguntas-frecuentes&quot;&gt;Preguntas frecuentes&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;¿Cuándo se libera la versión estable de PHP 8.6?&lt;/strong&gt;
El 19 de noviembre de 2026. El hard feature freeze es el 22 de septiembre y las release candidates corren del 24 de septiembre al 5 de noviembre.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¿Cuál es el cambio más peligroso de PHP 8.6?&lt;/strong&gt;
Los defaults de sesión: &lt;code&gt;session.use_strict_mode&lt;/code&gt; y &lt;code&gt;session.cookie_httponly&lt;/code&gt; pasan a &lt;code&gt;1&lt;/code&gt;, y &lt;code&gt;session.cookie_samesite&lt;/code&gt; pasa a &lt;code&gt;&amp;quot;Lax&amp;quot;&lt;/code&gt;. Fallan en silencio — los usuarios dejan de mantener sesión en POSTs cross-site en vez de recibir un error.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¿PHP 8.6 elimina funciones existentes?&lt;/strong&gt;
No. Deprecia muchísimo — todo mbregex, varios alias de verificación de tipos, métodos CSV de SPL — pero las eliminaciones están programadas para PHP 9. Las deprecaciones emiten avisos, no errores fatales.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¿De verdad va a desaparecer &lt;code&gt;mb_ereg&lt;/code&gt;?&lt;/strong&gt;
Está deprecada en 8.6 porque Oniguruma, la librería detrás de ella, dejó de tener mantenimiento, pero todavía funciona. Migra a PCRE (&lt;code&gt;preg_match&lt;/code&gt; y compañía con el modificador &lt;code&gt;u&lt;/code&gt;). Los patrones no siempre se traducen uno a uno, así que conviene probar en vez de hacer un find-and-replace.&lt;/p&gt;

&lt;h2 id=&quot;para-seguir&quot;&gt;Para seguir&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/php-8-6-novedades/&quot;&gt;Todas las novedades de PHP 8.6&lt;/a&gt; — el lado de las funciones nuevas: Partial Function Application, &lt;code&gt;clamp()&lt;/code&gt;, el enum &lt;code&gt;SortDirection&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/laravel-13-breaking-changes/&quot;&gt;Qué se rompe al actualizar a Laravel 13&lt;/a&gt; — Laravel 13 pide PHP 8.3 o superior, así que estas dos actualizaciones suelen caer en el mismo sprint.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>Desarrollo Web</category><category>PHP</category><category>PHP 8.6</category><category>Backend</category><category>Migración</category><category>Upgrade</category><category>Seguridad</category><author>Marco Orta</author></item><item><title>Symfony 8: qué se rompe al actualizar desde 7.4 y cómo decidir entre LTS y la rama normal</title><link>https://ortamarco.me/blog/symfony-8-que-se-rompe/</link><guid isPermaLink="true">https://ortamarco.me/blog/symfony-8-que-se-rompe/</guid><description>Symfony 8.0 se quedó sin soporte en julio de 2026. La config XML desapareció, Security cambió de forma y 7.4 LTS sigue vivo. Cada ruptura, ordenada por impacto.</description><pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Si estás en Symfony 8.0, llevas corriendo código sin soporte desde el 30 de julio de 2026 — 8.0 tuvo exactamente ocho meses de vida y ya se acabaron.&lt;/strong&gt; Si estás en 7.4, tienes años por delante, pero igual te toca decidir si te quedas en el LTS o brincas a 8.1, publicado el 29 de mayo de 2026. Ninguno de los dos caminos es una reescritura. Los dos tienen cambios que no lanzan error — simplemente cambian en silencio qué hace tu aplicación.&lt;/p&gt;
&lt;p&gt;Symfony 7.4 y 8.0 salieron el mismo día, el 27 de noviembre de 2025, con &lt;strong&gt;funcionalidades idénticas&lt;/strong&gt;. La única diferencia es que 7.4 conservó las capas de deprecación acumuladas desde 7.0, y 8.0 las borró todas. Esa es toda la historia de “Symfony 8”: es 7.4 sin la red de seguridad. 8.1, que salió medio año después, sí agrega funcionalidad nueva encima y es la versión a la que llegas si sigues la rama normal (no LTS).&lt;/p&gt;
&lt;h2 id=&quot;de-dónde-vienes&quot;&gt;¿De dónde vienes?&lt;/h2&gt;
&lt;div style=&quot;overflow-x:auto&quot;&gt;
























&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Estás en&lt;/th&gt;&lt;th&gt;Estado hoy (sep 2026)&lt;/th&gt;&lt;th&gt;Qué hacer&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Symfony 8.0&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Sin mantenimiento desde el 30 jul 2026 — ni fixes ni parches de seguridad&lt;/td&gt;&lt;td&gt;Sube a &lt;strong&gt;8.1&lt;/strong&gt; ya. Es el mismo código sin deprecaciones, sin cambios obligatorios bajo la promesa de BC&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Symfony 7.4 (LTS)&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Bug fixes hasta nov 2028, seguridad hasta nov 2029&lt;/td&gt;&lt;td&gt;Quédate, o brinca a 8.1 por las funciones nuevas. No hay urgencia de ningún lado&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Symfony 6.4 (LTS) o anterior&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;6.4: bug fixes hasta nov 2026 y seguridad hasta nov 2027. De las anteriores solo 5.4 LTS recibe parches, y solo de seguridad (hasta feb 2029)&lt;/td&gt;&lt;td&gt;Pasa por 7.4 primero, sin esperar a que se acabe la ventana de 6.4. Corrige cada deprecación ahí con &lt;code&gt;phpunit --display-deprecations&lt;/code&gt;, y luego decide entre 7.4-LTS y 8.x&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;La tabla de referencia, tal cual sale de &lt;code&gt;symfony.com/releases&lt;/code&gt;:&lt;/p&gt;





































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Versión&lt;/th&gt;&lt;th&gt;Tipo&lt;/th&gt;&lt;th&gt;Lanzamiento&lt;/th&gt;&lt;th&gt;PHP mínimo&lt;/th&gt;&lt;th&gt;Bug fixes hasta&lt;/th&gt;&lt;th&gt;Seguridad hasta&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;7.4&lt;/td&gt;&lt;td&gt;LTS&lt;/td&gt;&lt;td&gt;27 nov 2025&lt;/td&gt;&lt;td&gt;8.2.0&lt;/td&gt;&lt;td&gt;nov 2028&lt;/td&gt;&lt;td&gt;nov 2029&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;8.0&lt;/td&gt;&lt;td&gt;Normal&lt;/td&gt;&lt;td&gt;27 nov 2025&lt;/td&gt;&lt;td&gt;8.4.0&lt;/td&gt;&lt;td&gt;jul 2026&lt;/td&gt;&lt;td&gt;jul 2026&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;8.1&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Normal&lt;/td&gt;&lt;td&gt;29 may 2026&lt;/td&gt;&lt;td&gt;8.4.0&lt;/td&gt;&lt;td&gt;ene 2027&lt;/td&gt;&lt;td&gt;ene 2027&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Hay dos cosas que vale la pena leer dos veces: &lt;strong&gt;toda la ventana de soporte de 8.0 fue de ocho meses&lt;/strong&gt;, bug fixes y seguridad juntos — y esa es la política estándar para un minor no-LTS, no una recortada a propósito. Y &lt;strong&gt;8.1 exige PHP 8.4&lt;/strong&gt;, dos versiones completas por delante de lo que pide 7.4. Si sigues en PHP 8.2 u 8.3, subir la versión de PHP es trabajo de infraestructura real, aparte de todo lo que sigue en esta lista — hazlo primero, verifícalo, y hasta entonces toca &lt;code&gt;composer.json&lt;/code&gt;.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;impacto-alto&quot;&gt;Impacto alto&lt;/h2&gt;
&lt;h3 id=&quot;1-la-configuración-en-xml-desapareció--y-rompió-en-silencio-primero&quot;&gt;1. La configuración en XML desapareció — y rompió en silencio primero&lt;/h3&gt;
&lt;p&gt;Esta es la que agarra a la gente desprevenida, porque no falla el día que debería. &lt;strong&gt;Symfony 7.4 deprecó la configuración en XML; Symfony 8.0 la eliminó por completo.&lt;/strong&gt; Si tus bundles, rutas o servicios están configurados en XML y nadie estaba revisando el log de deprecaciones durante la ventana de 7.4, el salto a 8.0 u 8.1 es un alto total: el loader ya no existe, no es que esté desaconsejado.&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Eliminado sin alternativa en Symfony 8.0&lt;/span&gt;
&lt;span class=&quot;token class-name static-context&quot;&gt;ExtensionInterface&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getXsdValidationBasePath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token class-name static-context&quot;&gt;ExtensionInterface&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getNamespace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Routing, FrameworkBundle y WebProfilerBundle ya no cargan rutas en XML, punto. YAML sigue siendo el formato por default y sigue totalmente soportado. PHP es la alternativa recomendada para configuración basada en código, y también cambió de forma (ver el punto 4). Existe un convertidor automático para mantenedores de bundles — &lt;a href=&quot;https://github.com/GromNaN/symfony-config-xml-to-php/&quot;&gt;&lt;code&gt;symfony-config-xml-to-php&lt;/code&gt;&lt;/a&gt; —, pero la configuración XML a nivel de aplicación la vas a reescribir a mano.&lt;/p&gt;
&lt;p&gt;La razón por la que esto califica como “rompe en silencio”: el aviso de deprecación en 7.4 es nada más una línea de log. Nadie hace grep de logs de deprecación con regularidad. La falla aparece meses después, en el upgrade a 8.x, como un error fatal que parece no tener nada que ver con lo que tocaste esa semana.&lt;/p&gt;
&lt;h3 id=&quot;2-security-credenciales-borradas-listeners-del-firewall-exposición-de-errores&quot;&gt;2. Security: credenciales borradas, listeners del firewall, exposición de errores&lt;/h3&gt;
&lt;p&gt;Tres cambios caen en el mismo componente y se acumulan:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;UserInterface::eraseCredentials()&lt;/code&gt; y &lt;code&gt;TokenInterface::eraseCredentials()&lt;/code&gt; se eliminaron.&lt;/strong&gt; Usa &lt;code&gt;__serialize()&lt;/code&gt; para controlar qué sobrevive en el token en su lugar.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Los listeners del firewall tienen que extender &lt;code&gt;AbstractListener&lt;/code&gt; o implementar &lt;code&gt;FirewallListenerInterface&lt;/code&gt;.&lt;/strong&gt; Registrar un callable simple como listener ya no funciona.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;hide_user_not_found&lt;/code&gt; desapareció; usa &lt;code&gt;expose_security_errors&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-yaml&quot; data-language=&quot;yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Symfony &amp;lt;= 7.x&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;security&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;hide_user_not_found&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Symfony &amp;gt;= 8.0&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;security&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;expose_security_errors&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; account_status  &lt;span class=&quot;token comment&quot;&gt;# none | account_status | all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;expose_security_errors&lt;/code&gt; es más preciso que el booleano al que reemplaza — &lt;code&gt;account_status&lt;/code&gt; muestra excepciones solo a usuarios que ya dieron la contraseña correcta, &lt;code&gt;all&lt;/code&gt; no oculta nada, &lt;code&gt;none&lt;/code&gt; es el default seguro. Si tu &lt;code&gt;security.yaml&lt;/code&gt; todavía trae &lt;code&gt;hide_user_not_found&lt;/code&gt;, el contenedor no compila en 8.x. Ese es ruidoso. Lo que no es ruidoso: las cookies de remember-me. &lt;code&gt;RememberMeDetails&lt;/code&gt; ya no incluye el nombre completo de la clase del usuario dentro del payload de la cookie. Las cookies viejas, emitidas antes del upgrade, &lt;strong&gt;dejan de autenticar en silencio&lt;/strong&gt; — al usuario no le sale ningún error, simplemente se le cierra la sesión y tiene que volver a iniciarla. Si manejas una app con tráfico alto, espera un pico de tickets de soporte la semana que despliegues, no una alerta de incidente.&lt;/p&gt;
&lt;h3 id=&quot;3-comandos-de-consola-se-eliminaron-los-métodos-estáticos-de-metadata&quot;&gt;3. Comandos de consola: se eliminaron los métodos estáticos de metadata&lt;/h3&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Symfony &amp;lt;= 7.x&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name-definition class-name&quot;&gt;SyncCommand&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Command&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;getDefaultName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword return-type&quot;&gt;string&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;app:sync&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Symfony &amp;gt;= 8.0&lt;/span&gt;
&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;token attribute-content&quot;&gt;&lt;span class=&quot;token attribute-class-name class-name&quot;&gt;AsCommand&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token attribute-class-name class-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;app:sync&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name-definition class-name&quot;&gt;SyncCommand&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Command&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Command::getDefaultName()&lt;/code&gt; y &lt;code&gt;getDefaultDescription()&lt;/code&gt; se eliminaron — usa el atributo &lt;code&gt;#[AsCommand]&lt;/code&gt;. &lt;code&gt;Application::add()&lt;/code&gt; se reemplaza por &lt;code&gt;Application::addCommand()&lt;/code&gt;. Si registras comandos de forma dinámica instanciando subclases de &lt;code&gt;Command&lt;/code&gt; y metiéndolas a la aplicación, esto es un cambio mecánico pero obligatorio.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;impacto-medio&quot;&gt;Impacto medio&lt;/h2&gt;
&lt;h3 id=&quot;4-la-config-php-fluida-desapareció-hay-un-formato-nuevo-de-array-shape&quot;&gt;4. La config PHP fluida desapareció; hay un formato nuevo de array-shape&lt;/h3&gt;
&lt;p&gt;Symfony 5.3 introdujo un formato de configuración PHP fluido, tipo builder (&lt;code&gt;SecurityConfig&lt;/code&gt;, &lt;code&gt;FrameworkConfig&lt;/code&gt; y compañía). Se eliminó en 8.0. Lo que lo reemplaza no es un regreso a los arrays de siempre — es un &lt;strong&gt;formato PHP nuevo de array-shape&lt;/strong&gt; con metadata tipada que los IDEs y los analizadores estáticos sí pueden entender.&lt;/p&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Eliminado: configuración PHP fluida&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;token package&quot;&gt;Symfony&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;Config&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;SecurityConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name type-declaration&quot;&gt;SecurityConfig&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$security&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token variable&quot;&gt;$security&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;firewall&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;main&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;pattern&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;^/*&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lazy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Nuevo: configuración PHP array-shape&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;token package&quot;&gt;Symfony&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;Component&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;DependencyInjection&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;Loader&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;Configurator&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token class-name static-context&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;security&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;firewalls&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
            &lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;main&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;pattern&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;^/*&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;lazy&amp;#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token constant boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La razón declarada es que los builders fluidos no podían representar cualquier forma semántica del árbol de configuración, lo que hacía innecesariamente difícil mantener actualizadas las recipes de Symfony de forma automática. YAML sigue siendo el default recomendado; este formato es para equipos que específicamente quieren PHP.&lt;/p&gt;
&lt;h3 id=&quot;5-doctrine-ya-no-auto-mapea-entidades-en-los-argumentos-del-controller&quot;&gt;5. Doctrine: ya no auto-mapea entidades en los argumentos del controller&lt;/h3&gt;
&lt;p&gt;El auto-mapeo estilo ParamConverter, el que resolvía parámetros de ruta directo en argumentos de entidades Doctrine, se eliminó. Si una acción de un controller tipaba una entidad Doctrine y confiaba en que el bundle la resolviera implícitamente desde la ruta, esa resolución ya no existe — ahora lo cableas explícitamente con &lt;code&gt;#[MapEntity]&lt;/code&gt; o la obtienes tú mismo dentro del cuerpo de la acción. &lt;code&gt;DoctrineExtractor::getTypes()&lt;/code&gt; también desapareció; usa &lt;code&gt;getType()&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;6-httpfoundation-requestget-eliminado-el-method-override-se-acotó&quot;&gt;6. HttpFoundation: &lt;code&gt;Request::get()&lt;/code&gt; eliminado, el method override se acotó&lt;/h3&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Eliminado&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Usa el bag específico&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;attributes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Request::get()&lt;/code&gt; buscaba primero en attributes, luego en query, luego en el body de la petición, en ese orden — cómodo, y una fuente frecuente de bugs cuando dos bags tenían la misma llave. Ya no existe; hay que ser explícito sobre qué bag quieres. Aparte, el method override por HTTP (&lt;code&gt;X-HTTP-METHOD-OVERRIDE&lt;/code&gt; / &lt;code&gt;_method&lt;/code&gt;) ya no aplica a &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;HEAD&lt;/code&gt;, &lt;code&gt;CONNECT&lt;/code&gt; ni &lt;code&gt;TRACE&lt;/code&gt; — esos métodos están pensados para ser seguros e idempotentes, y ahora Symfony lo hace cumplir en la capa de request en vez de confiar en el header de override.&lt;/p&gt;
&lt;h3 id=&quot;7-form-urltype-deja-de-adivinar-un-protocolo&quot;&gt;7. Form: &lt;code&gt;UrlType&lt;/code&gt; deja de adivinar un protocolo&lt;/h3&gt;
&lt;pre class=&quot;language-php&quot; data-language=&quot;php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Symfony &amp;lt;= 7.4: default_protocol usa &amp;#39;http&amp;#39; por default&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// escribir &amp;quot;example.com&amp;quot; en el campo guarda &amp;quot;http://example.com&amp;quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Symfony &amp;gt;= 8.0: default_protocol usa null por default&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// escribir &amp;quot;example.com&amp;quot; guarda &amp;quot;example.com&amp;quot; — sin protocolo agregado&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Es de las pocas rupturas de 8.0 que no truenan. Desde 7.1, no configurar &lt;code&gt;default_protocol&lt;/code&gt; emitía una deprecación; si nadie la leyó, en 8.0 el valor por default cambia sin avisar. Si tus formularios usan &lt;code&gt;UrlType&lt;/code&gt; y después muestras o enlazas ese valor asumiendo que siempre trae un esquema, ahora ya no lo trae a menos que el usuario lo haya escrito. Nada lanza error; te queda un string con pinta de ruta relativa que se comporta como un link roto la primera vez que alguien le da clic. Para conservar el comportamiento anterior, declara &lt;code&gt;&amp;#39;default_protocol&amp;#39; =&amp;gt; &amp;#39;http&amp;#39;&lt;/code&gt; en el campo.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;novedades-de-81-que-cambian-el-comportamiento-sin-lanzar-error&quot;&gt;Novedades de 8.1 que cambian el comportamiento sin lanzar error&lt;/h2&gt;
&lt;p&gt;Esta sección vale la pena leerla con calma si ya pasaste 8.0 y estás evaluando 8.1, porque ninguno de estos cambios lanza excepción — simplemente hacen algo distinto.&lt;/p&gt;
&lt;h3 id=&quot;8-messenger-ya-no-lanza-excepción-al-fallar-el-decode&quot;&gt;8. Messenger ya no lanza excepción al fallar el decode&lt;/h3&gt;
&lt;p&gt;Los serializers ahora regresan un &lt;code&gt;Envelope&lt;/code&gt; que envuelve una &lt;code&gt;MessageDecodingFailedException&lt;/code&gt; en vez de lanzarla. Si tu transporte o middleware capturaba &lt;code&gt;MessageDecodingFailedException&lt;/code&gt; alrededor del decode, ese bloque &lt;code&gt;catch&lt;/code&gt; deja de dispararse. La falla ya no se expone como excepción — es un dato dentro del envelope que tienes que revisar tú. El código que no se actualice para esto va a procesar (o descartar) en silencio un fallo de decode que antes era imposible de pasar por alto.&lt;/p&gt;
&lt;h3 id=&quot;9-parameterbaggetint--getboolean-empiezan-a-lanzar-excepción&quot;&gt;9. &lt;code&gt;ParameterBag::getInt()&lt;/code&gt; / &lt;code&gt;getBoolean()&lt;/code&gt; empiezan a lanzar excepción&lt;/h3&gt;
&lt;p&gt;En dirección contraria al punto anterior: estos métodos antes forzaban en silencio un valor no convertible a &lt;code&gt;0&lt;/code&gt; o &lt;code&gt;false&lt;/code&gt;. En 8.1 lanzan &lt;code&gt;UnexpectedValueException&lt;/code&gt; en su lugar. Este es un caso donde 8.1 convierte una falla &lt;em&gt;anterior&lt;/em&gt; que era silenciosa en una ruidosa — bueno para detectar input malo, pero significa que el código que dependía del fallback viejo ahora necesita un try/catch o validación previa.&lt;/p&gt;
&lt;h3 id=&quot;10-el-choicetype-colapsado-y-requerido-renderiza-su-placeholder-como-hidden&quot;&gt;10. El &lt;code&gt;ChoiceType&lt;/code&gt; colapsado y requerido renderiza su placeholder como &lt;code&gt;hidden&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Un campo &lt;code&gt;ChoiceType&lt;/code&gt; requerido y no expandido ahora marca su &lt;code&gt;&amp;lt;option&amp;gt;&lt;/code&gt; de placeholder con el atributo &lt;code&gt;hidden&lt;/code&gt; en vez de ser nada más una opción de valor vacío. Es puramente visual, pero si tienes JavaScript o CSS que selecciona sobre la opción de placeholder, o pruebas de UI automatizadas que hacen assert sobre ese markup, es un cambio silencioso en el DOM. Puedes restaurar el comportamiento anterior con &lt;code&gt;placeholder_attr: []&lt;/code&gt; si lo necesitas.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;lo-que-no-se-rompe&quot;&gt;Lo que NO se rompe&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Symfony 8.0 y 8.1 no son “el siguiente 7.x”.&lt;/strong&gt; Eliminaron deprecaciones; no agregaron una ola de comportamiento incompatible nuevo más allá de esa eliminación. La mayor parte de lo que rompe en el salto 7.4→8.0 es algo que ya estaba deprecado y registrado en 7.x — si limpiaste deprecaciones en 7.4, el upgrade a 8.0 es casi un no-evento.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;YAML como configuración no va a ningún lado.&lt;/strong&gt; Es explícitamente el formato que las recipes de Symfony siguen apuntando.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El upgrade de 8.0 a 8.1 no exige cambios de código bajo la promesa de retrocompatibilidad&lt;/strong&gt; — 8.1 es un minor sobre 8.0, no un major.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;el-flujo-real-de-migración&quot;&gt;El flujo real de migración&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Aterriza en 7.4 primero si no estás ahí.&lt;/strong&gt; Aunque tu destino sea 8.x, subir al LTS te da la capa de deprecación contra la cual trabajar.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Corre el reporte de deprecaciones y arregla lo que es tuyo:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;php bin/phpunit --display-deprecations
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Para que la suite falle solo por las deprecaciones de tu propio código, y no por las de paquetes de terceros que todavía no controlas, no busques el viejo &lt;code&gt;SYMFONY_DEPRECATIONS_HELPER=weak_vendors&lt;/code&gt;: ese modo se eliminó en &lt;code&gt;symfony/phpunit-bridge&lt;/code&gt; 5.0, y en PHPUnit 10 o superior el bridge ni siquiera registra su manejador de deprecaciones. Lo vigente es configurarlo en &lt;code&gt;phpunit.dist.xml&lt;/code&gt;, como lo trae la recipe de Flex:&lt;/p&gt;
&lt;pre class=&quot;language-xml&quot; data-language=&quot;xml&quot;&gt;&lt;code class=&quot;language-xml&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;phpunit&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;failOnDeprecation&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;quot;&lt;/span&gt;true&lt;span class=&quot;token punctuation&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;source&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;ignoreSuppressionOfDeprecations&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;quot;&lt;/span&gt;true&lt;span class=&quot;token punctuation&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;ignoreIndirectDeprecations&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;quot;&lt;/span&gt;true&lt;span class=&quot;token punctuation&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;include&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;directory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;src&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;directory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;include&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;deprecationTrigger&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;trigger_deprecation&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;deprecationTrigger&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;phpunit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ignoreIndirectDeprecations&lt;/code&gt; descarta las deprecaciones que dispara código de terceros — útil para priorizar qué es de verdad accionable este sprint. Si tu suite sigue en PHPUnit 9 con &lt;code&gt;simple-phpunit&lt;/code&gt;, el equivalente es &lt;code&gt;SYMFONY_DEPRECATIONS_HELPER=&amp;#39;max[self]=0&amp;#39;&lt;/code&gt;.&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;Automatiza los renombres mecánicos con Rector.&lt;/strong&gt; &lt;a href=&quot;https://github.com/rectorphp/rector-symfony&quot;&gt;&lt;code&gt;rector/rector-symfony&lt;/code&gt;&lt;/a&gt; trae sets de upgrade que cubren buena parte de esta lista — &lt;code&gt;getDefaultName()&lt;/code&gt; a atributo, llamadas a métodos deprecados y similares — sin editar archivo por archivo a mano.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Actualiza dependencias y deja que Flex aplique las recipes nuevas:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;composer&lt;/span&gt; require symfony/symfony:^8.1 --with-all-dependencies
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;&lt;strong&gt;Haz grep de lo específico de tu app&lt;/strong&gt; antes de asumir que la automatización ya lo cubrió:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rln&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;&amp;lt;?xml&amp;#39;&lt;/span&gt; config/
&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rn&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;hide_user_not_found\|-&amp;gt;get(&amp;quot;&lt;/span&gt; config/ src/
&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rn&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;getDefaultName\|getDefaultDescription&amp;quot;&lt;/span&gt; src/
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;&lt;strong&gt;Compara tu árbol de &lt;code&gt;config/&lt;/code&gt; contra un &lt;code&gt;symfony/skeleton&lt;/code&gt; fresco en la rama destino.&lt;/strong&gt; La mayor parte de lo que está en esta lista vive en archivos de configuración, no en código del framework, y &lt;code&gt;composer update&lt;/code&gt; no te toca la configuración.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;cuándo-deberías-actualizar&quot;&gt;¿Cuándo deberías actualizar?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Si ya estás en 8.0:&lt;/strong&gt; muévete a 8.1 de inmediato. No hay costo de cambio de código bajo la promesa de BC, y cada día después del 30 de julio de 2026 es un día sin parches de seguridad.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Si estás en 7.4, estable, sin presión por funciones nuevas:&lt;/strong&gt; quédate. Tienes hasta noviembre de 2028 para bug fixes y noviembre de 2029 para seguridad. No hay reloj empujándote a moverte.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Si estás en 7.4 y quieres lo más nuevo o planeas seguir el ritmo de la rama normal:&lt;/strong&gt; ve a 8.1 ya, y luego sigue los minors semestrales (8.2 llega en noviembre de 2026). Arregla las deprecaciones de config en XML, config PHP fluida y &lt;code&gt;hide_user_not_found&lt;/code&gt; antes del salto, no durante.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Si estás en 6.4 o anterior:&lt;/strong&gt; 6.4 LTS deja de recibir bug fixes en noviembre de 2026 y parches de seguridad en noviembre de 2027; las anteriores ya no reciben nada, salvo 5.4 LTS, que conserva solo los de seguridad hasta febrero de 2029. Pasa por 7.4, limpia cada deprecación con PHPUnit, y luego elige entre LTS y rama normal según qué tanto quieras lo más nuevo contra una ventana de soporte larga y tranquila.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Para seguir leyendo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/php-8-6-novedades/&quot;&gt;Las novedades de PHP 8.6&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/laravel-13-breaking-changes/&quot;&gt;Qué se rompe al actualizar a Laravel 13&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/mejores-ides-para-php-desarrolladores/&quot;&gt;Los mejores IDEs para desarrolladores PHP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Symfony 8.0 todavía tiene soporte? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No. Symfony 8.0 fue una versión normal (no LTS) con una ventana de soporte de ocho meses que cubría tanto bug fixes como parches de seguridad. Se lanzó el 27 de noviembre de 2025 y su soporte terminó el 30 de julio de 2026. Quien siga corriéndolo está en código sin mantenimiento y debería actualizar a 8.1. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuál es la diferencia entre Symfony 7.4 y Symfony 8.0? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Salieron el mismo día, el 27 de noviembre de 2025, con funcionalidades idénticas. Symfony 7.4 es la versión LTS y conserva las capas de deprecación acumuladas desde 7.0, así que el código que usa APIs deprecadas sigue funcionando. Symfony 8.0 eliminó todas esas APIs deprecadas. Symfony 7.4 requiere PHP 8.2; Symfony 8.0 requiere PHP 8.4. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué cambio incompatible causa más fallas silenciosas en Symfony 8? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; La configuración en XML. Se deprecó en Symfony 7.4 como una línea de log que nadie suele monitorear, y luego se eliminó por completo en Symfony 8.0. Los equipos que actualizan sin limpiar primero las deprecaciones de 7.4 se topan con una falla dura en 8.0 que parece no tener relación con nada que hayan cambiado. Las cookies de autenticación remember-me son un segundo caso silencioso: su formato cambió y las cookies viejas dejan de autenticar sin ningún error, solo forzando un nuevo inicio de sesión. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Actualizar de Symfony 8.0 a 8.1 exige cambios de código? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No. Symfony 8.1 es un minor sobre 8.0 y cae bajo la promesa de retrocompatibilidad. Las consideraciones principales son de comportamiento: Messenger ya no lanza excepción al fallar el decode de un mensaje, ParameterBag::getInt/getBoolean ahora lanzan excepción con input malo en vez de forzarlo en silencio, y el placeholder de un ChoiceType requerido se renderiza como hidden. El cambio de UrlType, que deja de agregar http:// a las URLs sin esquema, no es de 8.1: llegó con 8.0, así que si ya estás en 8.0 ya lo tienes. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué versión de PHP necesita Symfony 8? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Symfony 8.0 y 8.1 requieren ambos PHP 8.4.0 o superior. Symfony 7.4 LTS solo requiere PHP 8.2.0 o superior, que es una de las razones por las que los equipos que todavía no pueden subir de versión de PHP eligen quedarse en 7.4 en vez de saltar a 8.x. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Debería actualizar a Symfony 8.1 o quedarme en 7.4 LTS? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Si necesitas las funciones de PHP 8.4 o lo más nuevo de Symfony y puedes seguir el ritmo semestral de lanzamientos, ve a 8.1. Si quieres una ventana de soporte larga y tranquila sin perseguir cada minor, quédate en 7.4: recibe bug fixes hasta noviembre de 2028 y parches de seguridad hasta noviembre de 2029, sin fecha límite que te empuje. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Desarrollo Web</category><category>PHP</category><category>Symfony</category><category>Backend</category><category>Migración</category><category>Upgrade</category><author>Marco Orta</author></item><item><title>Vite 8: qué se rompe al actualizar (y por qué ya lo trae Astro 7)</title><link>https://ortamarco.me/blog/vite-8-que-se-rompe/</link><guid isPermaLink="true">https://ortamarco.me/blog/vite-8-que-se-rompe/</guid><description>Vite 8 cambia Rollup y esbuild por Rolldown. Astro 7 ya lo trae de fábrica desde su primer release: actualizarlo heredó estas rupturas sin avisar. Por impacto.</description><pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Vite 8 llegó a estable el 12 de marzo de 2026 y reemplaza a Rollup y esbuild por un solo bundler en Rust: Rolldown. Casi todo lo que se rompe lo hace en silencio — sin error rojo, nada más un output distinto.&lt;/strong&gt; Y aquí está la parte que agarra a la gente en curva: si actualizaste a Astro 7 en cualquier momento después de su lanzamiento de junio de 2026, te llevaste Vite 8 de fábrica, lo hayas pedido o no. Este sitio corre Astro 7.2.2, y su &lt;code&gt;node_modules/astro/package.json&lt;/code&gt; fija &lt;code&gt;&amp;quot;vite&amp;quot;: &amp;quot;^8.0.13&amp;quot;&lt;/code&gt;. Si seguiste &lt;a href=&quot;https://ortamarco.me/blog/actualizar-a-astro-7/&quot;&gt;la migración a Astro 7&lt;/a&gt;, este post es la parte que nadie te contó.&lt;/p&gt;
&lt;h2 id=&quot;de-dónde-vienes&quot;&gt;¿De dónde vienes?&lt;/h2&gt;
&lt;p&gt;La lista de abajo aplica distinto según de dónde vengas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Corres Vite directo y ya estás en Vite 7.&lt;/strong&gt; El caso sencillo. Lee los renombres de config y la sección de interop CJS, corre un build, listo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Estás en Vite 6 o antes.&lt;/strong&gt; Estás saltando dos majors de cambios de un jalón — la migración a Vite 7 (Node 20.19+, salida ESM) más todo lo de abajo. Hazlo en dos pasos: aterriza primero en 7, verifica, y luego brinca a 8.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No tocaste Vite. Actualizaste Astro, Nuxt, SvelteKit u otro meta-framework, y Vite 8 se vino de arrastre.&lt;/strong&gt; Este es el grupo para el que en realidad es este post. Nadie puso “nuevo bundler” en tu changelog; nada más apareció como una línea de &lt;code&gt;vite&lt;/code&gt; en tu &lt;code&gt;package-lock.json&lt;/code&gt;. Todo lo de aquí en adelante te aplica igual — nada más no elegiste cuándo.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;qué-cambió-de-verdad-en-una-sola-tabla&quot;&gt;Qué cambió de verdad, en una sola tabla&lt;/h2&gt;























































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Área&lt;/th&gt;&lt;th&gt;Vite 7&lt;/th&gt;&lt;th&gt;Vite 8&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Bundler de producción&lt;/td&gt;&lt;td&gt;Rollup&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Rolldown&lt;/strong&gt; (Rust)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Pre-bundling de dependencias en dev&lt;/td&gt;&lt;td&gt;esbuild&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Rolldown&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Transform / minify de JS&lt;/td&gt;&lt;td&gt;esbuild&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Oxc&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Minify de CSS&lt;/td&gt;&lt;td&gt;esbuild&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Lightning CSS&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;build.rollupOptions&lt;/code&gt;&lt;/td&gt;&lt;td&gt;válido&lt;/td&gt;&lt;td&gt;&lt;strong&gt;deprecado&lt;/strong&gt;, renombrado a &lt;code&gt;build.rolldownOptions&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Distribución&lt;/td&gt;&lt;td&gt;ESM&lt;/td&gt;&lt;td&gt;Solo ESM (sin cambio, nada más más estricto)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Mínimo de Node.js&lt;/td&gt;&lt;td&gt;20.19+ / 22.12+&lt;/td&gt;&lt;td&gt;igual&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Tamaño de instalación&lt;/td&gt;&lt;td&gt;base&lt;/td&gt;&lt;td&gt;&lt;strong&gt;+~15 MB&lt;/strong&gt; (binarios nativos)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;build.target&lt;/code&gt; por defecto&lt;/td&gt;&lt;td&gt;Chrome 107, Safari 16.0&lt;/td&gt;&lt;td&gt;Chrome 111, Safari 16.4&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;El número que se roba el titular es real: &lt;a href=&quot;https://vite.dev/blog/announcing-vite8&quot;&gt;el propio anuncio de Vite&lt;/a&gt; cita el build de producción de Linear bajando de 46s a 6s, Ramp 57% más rápido, Beehiiv 64%, Mercedes-Benz.io 38%. Eso es lo bueno. El resto de este post es la parte que no sale en el titular del release.&lt;/p&gt;
&lt;h2 id=&quot;rupturas-ordenadas-por-impacto&quot;&gt;Rupturas, ordenadas por impacto&lt;/h2&gt;
&lt;h3 id=&quot;1-los-imports-default-de-commonjs-resuelven-distinto--en-silencio&quot;&gt;1. Los imports &lt;code&gt;default&lt;/code&gt; de CommonJS resuelven distinto — en silencio&lt;/h3&gt;
&lt;p&gt;Esta es la que vale la pena leer dos veces, porque no produce &lt;strong&gt;ningún error de build&lt;/strong&gt;. La guía de migración de Vite 8 lo dice claro: el import &lt;code&gt;default&lt;/code&gt; de un módulo CJS solía resolver distinto entre dev y build; ahora es consistente, pero la regla cambió. El import &lt;code&gt;default&lt;/code&gt; es el valor completo de &lt;code&gt;module.exports&lt;/code&gt; solo si se cumple una de estas — el que importa es &lt;code&gt;.mjs&lt;/code&gt;/&lt;code&gt;.mts&lt;/code&gt;, el &lt;code&gt;package.json&lt;/code&gt; más cercano al que importa tiene &lt;code&gt;&amp;quot;type&amp;quot;: &amp;quot;module&amp;quot;&lt;/code&gt;, o la bandera &lt;code&gt;__esModule&lt;/code&gt; del módulo CJS no es &lt;code&gt;true&lt;/code&gt;. Si no, te toca &lt;code&gt;module.exports.default&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;En la práctica, esto significa que un import que ya funcionaba puede empezar a devolver un objeto envoltorio en vez de la función que esperas:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// cjs-pkg exporta: module.exports = { __esModule: true, default: function greet() {} }&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; greet &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;cjs-pkg&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;greet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Vite 7, en dev: funcionaba&lt;/span&gt;
          &lt;span class=&quot;token comment&quot;&gt;// Vite 8, .js normal sin &amp;quot;type&amp;quot;: &amp;quot;module&amp;quot;: TypeError: greet is not a function&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La válvula de escape es una línea, y te compra tiempo para arreglar imports uno por uno en vez de todos de golpe:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// vite.config.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;defineConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;legacy&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;inconsistentCjsInterop&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// restaura el comportamiento viejo e inconsistente&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Está marcada como deprecada a propósito — trátala como puente, no como destino final. Si es un paquete de terceros el que dispara esto, &lt;a href=&quot;https://vite.dev/guide/migration&quot;&gt;la documentación pide&lt;/a&gt; reportarlo al autor del paquete con un link a la explicación de interop CJS de Rolldown, no solo parcharlo por siempre.&lt;/p&gt;
&lt;h3 id=&quot;2-buildrollupoptions-ahora-es-buildrolldownoptions&quot;&gt;2. &lt;code&gt;build.rollupOptions&lt;/code&gt; ahora es &lt;code&gt;build.rolldownOptions&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Esta al menos falla lo bastante fuerte como para notarla — una advertencia de deprecación en cada build, todavía no un crash, porque un shim de compatibilidad mantiene viva la llave vieja:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Antes (Vite 7)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;defineConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;rollupOptions&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;./src/main.ts&amp;#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Después (Vite 8)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;defineConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;rolldownOptions&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;./src/main.ts&amp;#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;worker.rollupOptions&lt;/code&gt; recibe el mismo trato (&lt;code&gt;worker.rolldownOptions&lt;/code&gt;). Si tienes ambas en una config de monorepo compartida entre paquetes, un &lt;code&gt;grep -rn &amp;quot;rollupOptions&amp;quot; .&lt;/code&gt; antes de empezar te encuentra todos los lugares de un jalón.&lt;/p&gt;
&lt;h3 id=&quot;3-manualchunks-como-objeto-no-está-deprecado--está-eliminado&quot;&gt;3. &lt;code&gt;manualChunks&lt;/code&gt; como objeto no está deprecado — está eliminado&lt;/h3&gt;
&lt;p&gt;Escondido dentro del renombre de arriba hay un filo más cortante: &lt;strong&gt;la forma de objeto de &lt;code&gt;output.manualChunks&lt;/code&gt; ya no está soportada&lt;/strong&gt;, punto. Solo la forma de función sigue funcionando, y también está marcada como deprecada, a favor de la opción propia de Rolldown, &lt;code&gt;codeSplitting&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Vite 7 — este patrón aparece seguido en configs reales&lt;/span&gt;
&lt;span class=&quot;token literal-property property&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;rollupOptions&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;manualChunks&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;vendor&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;react&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;react-dom&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// forma de objeto&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Vite 8 — la forma de objeto truena en build; convertir a función es el arreglo mínimo&lt;/span&gt;
&lt;span class=&quot;token literal-property property&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;rolldownOptions&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;manualChunks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#39;node_modules/react&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#39;vendor&amp;#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esta es la que más probablemente rompa tu build de verdad (no nada más avise), porque un montón de configs de Vite copiadas y pegadas usan la forma de objeto para separar vendors.&lt;/p&gt;
&lt;h3 id=&quot;4-buildcommonjsoptions-ahora-es-un-no-op&quot;&gt;4. &lt;code&gt;build.commonjsOptions&lt;/code&gt; ahora es un no-op&lt;/h3&gt;
&lt;p&gt;Sin advertencia, sin error — nada más se ignora. Si tu config trae &lt;code&gt;build.commonjsOptions.include&lt;/code&gt;, &lt;code&gt;.exclude&lt;/code&gt;, o &lt;code&gt;.requireReturnsDefault&lt;/code&gt; para rodear algún paquete CJS específico, &lt;strong&gt;ese parche deja de aplicar en silencio&lt;/strong&gt; después del upgrade. El síntoma aparece río abajo, en lo que sea que la opción arreglaba, no en la línea de la config — lo que hace fácil culpar a lo que no es.&lt;/p&gt;
&lt;h3 id=&quot;5-nodejs-2019--2212-es-un-piso-duro-y-es-por-requireesm&quot;&gt;5. Node.js 20.19+ / 22.12+ es un piso duro, y es por &lt;code&gt;require(esm)&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Es el mismo requisito de Vite 7, así que si vienes de ahí esto no cambia nada. Pero si brincas desde Vite 6, esto es nuevo: Vite se distribuye &lt;strong&gt;solo en ESM&lt;/strong&gt;, y esas versiones de patch específicas son justo las que soportan &lt;code&gt;require(esm)&lt;/code&gt; sin flag — el mecanismo que deja a un toolchain de la era CJS seguir cargando un paquete que solo existe en ESM. Cualquier versión más vieja ni siquiera instala bien, no nada más corre raro.&lt;/p&gt;
&lt;h3 id=&quot;6-yarn-pnp-sin-veredicto-oficial-pero-tampoco-está-sólido&quot;&gt;6. Yarn PnP: sin veredicto oficial, pero tampoco está sólido&lt;/h3&gt;
&lt;p&gt;La documentación de Vite no trae una línea de “Yarn PnP: no soportado”, pero el &lt;a href=&quot;https://github.com/vitejs/rolldown-vite/issues/215&quot;&gt;issue tracker de rolldown-vite&lt;/a&gt; tiene varios reportes abiertos de resolución de dependencias fallando específicamente bajo el modo Plug’n’Play de Yarn — paquetes que resuelven bien con &lt;code&gt;node_modules&lt;/code&gt; y truenan con &lt;code&gt;Failed to resolve import&lt;/code&gt; bajo PnP. Si tu equipo corre Yarn Berry con &lt;code&gt;nodeLinker: pnp&lt;/code&gt;, trata a Vite 8 como “prueba en una rama antes de tocar CI”, no como un reemplazo seguro todavía.&lt;/p&gt;
&lt;h3 id=&quot;7-el-tamaño-de-instalación-crece-unos-15-mb&quot;&gt;7. El tamaño de instalación crece unos 15 MB&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://vite.dev/blog/announcing-vite8&quot;&gt;Confirmado en el anuncio de la release&lt;/a&gt;: cerca de 10 MB de Lightning CSS (ahora el minificador de CSS por defecto, ya no opcional) y cerca de 5 MB del binario nativo de Rolldown. Nada se rompe funcionalmente, pero si construyes imágenes Docker con presupuesto de capas apretado o metes &lt;code&gt;node_modules&lt;/code&gt; completo en un bundle de Lambda, es una línea real que vale la pena revisar — y aplica aunque nunca toques Vite directamente, porque viaja de polizón como dependencia de Astro (o Nuxt, o SvelteKit).&lt;/p&gt;
&lt;h3 id=&quot;8-los-targets-de-navegador-por-defecto-subieron--y-los-navegadores-viejos-fallan-sin-avisar-en-el-build&quot;&gt;8. Los targets de navegador por defecto subieron — y los navegadores viejos fallan sin avisar en el build&lt;/h3&gt;
&lt;p&gt;Los valores por defecto de &lt;code&gt;build.target&lt;/code&gt; subieron: Chrome 107→111, Edge 107→111, Firefox 104→114, Safari 16.0→16.4, alineados a Baseline Widely Available de enero de 2026. Vite compila sin quejarse de cualquier forma — la falla, si la tienes, aparece como JavaScript roto en un Safari viejo dentro del analytics de producción de alguien, semanas después, sin nada en tu log de build que señale la causa. Si tienes una matriz de soporte de navegadores documentada que incluya algo más viejo que Safari 16.4, fija &lt;code&gt;build.target&lt;/code&gt; a mano en vez de confiar en el default.&lt;/p&gt;
&lt;h3 id=&quot;9-los-plugins-en-general-nada-más-funcionan--con-dos-excepciones-reales&quot;&gt;9. Los plugins en general nada más funcionan — con dos excepciones reales&lt;/h3&gt;
&lt;p&gt;Rolldown implementa la misma API de plugins que Rollup, y los plugins de framework que importan ya están al día: &lt;code&gt;@vitejs/plugin-react&lt;/code&gt; v6 usa Oxc en vez de Babel para el transform de Refresh (la v5 sigue corriendo bien en Vite 8 si todavía no la actualizas), y &lt;code&gt;@vitejs/plugin-vue&lt;/code&gt; no necesita cambios. Donde sí se rompe de verdad:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Plugins que dependen del hook &lt;code&gt;moduleParsed&lt;/code&gt;: está &lt;a href=&quot;https://vite.dev/guide/api-plugin&quot;&gt;documentado que no se llama en dev&lt;/a&gt; en absoluto, para evitar un parse completo de AST en cada archivo.&lt;/li&gt;
&lt;li&gt;Plugins construidos contra el formato propio de esbuild (&lt;code&gt;onLoad&lt;/code&gt;/&lt;code&gt;onResolve&lt;/code&gt;, no el de Rollup) — esa es una API completamente distinta y Rolldown no la habla. Si el README de un plugin menciona “esbuild plugin”, busca un reemplazo nativo de Rolldown antes de asumir que se actualiza gratis.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;cuándo-conviene-actualizar&quot;&gt;¿Cuándo conviene actualizar?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Corres Vite directo, ya en 7, sin dependencias CJS exóticas:&lt;/strong&gt; actualiza ya. La ganancia en tiempo de build es real y la superficie de ruptura es chica si no dependes de &lt;code&gt;manualChunks&lt;/code&gt; como objeto.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Estás en Vite 6:&lt;/strong&gt; pasa por 7 primero. Vite mantiene un paquete &lt;code&gt;rolldown-vite&lt;/code&gt; que es “Vite 7 con Rolldown, nada más” pensado como paso intermedio — úsalo para aislar el cambio de bundler del salto de versión.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Llegaste aquí vía Astro 7 (o Nuxt, SvelteKit, similar):&lt;/strong&gt; ya lo estás corriendo. La movida no es “¿le entro?” — es auditar tu &lt;code&gt;vite.config&lt;/code&gt; existente en busca de &lt;code&gt;rollupOptions.output.manualChunks&lt;/code&gt; como objeto y cualquier override de &lt;code&gt;commonjsOptions&lt;/code&gt;, porque esas son las dos que fallan o se apagan en silencio sin que hayas tocado la versión del framework para nada.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tu equipo corre Yarn PnP:&lt;/strong&gt; espera, o fija &lt;code&gt;rolldown-vite@7.x&lt;/code&gt; y vigila los issues abiertos antes de moverte a Vite 8 de verdad.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mantienes un plugin de Rollup con uso pesado de &lt;code&gt;moduleParsed&lt;/code&gt; o de hooks de output:&lt;/strong&gt; presupuesta tiempo real, no un ajuste de config — es un problema de forma de API, no un renombre.&lt;/p&gt;
&lt;p&gt;Para seguir:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/actualizar-a-astro-7/&quot;&gt;Actualizar a Astro 7&lt;/a&gt; — el upgrade de framework que trae Vite 8 de arrastre, con su propio set de trampas aparte.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/node-26-lts-que-se-rompe/&quot;&gt;Node 26: qué se rompe al actualizar&lt;/a&gt; — el lado del runtime del mismo problema de “se rompe en silencio”, para la versión sobre la que corren la mayoría de instalaciones de Vite 8.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ortamarco.me/blog/typescript-7-que-se-rompe/&quot;&gt;TypeScript 7: qué se rompe al actualizar&lt;/a&gt; — mismo formato, mismo instinto: ordenar por impacto, marcar lo que falla sin avisar.&lt;/li&gt;
&lt;/ul&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Astro 7 usa Vite 8? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Sí. Astro 7.0.0 ya fija &amp;quot;vite&amp;quot;: &amp;quot;^8.0.13&amp;quot; en su package.json, y cada release 7.x desde entonces lo hereda. Si actualizaste de Astro 6 a Astro 7, te llevaste Vite 8 y su bundler Rolldown automáticamente, sin un paso de actualización de Vite aparte. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué se rompe en silencio al actualizar a Vite 8? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Dos cosas no producen ningún error de build: la resolución de imports default de CommonJS cambió de regla (un import x from &amp;#39;paquete-cjs&amp;#39; que ya funcionaba puede empezar a devolver un objeto envoltorio en vez del valor que esperas), y build.commonjsOptions se volvió un no-op, así que cualquier parche de include/exclude/requireReturnsDefault en tu config deja de aplicar en silencio. Las dos fallan río abajo, no en la línea de la config, lo que las hace fáciles de diagnosticar mal. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Por qué build.rollupOptions dejó de funcionar en mi vite.config.js? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Se renombró a build.rolldownOptions porque Vite 8 reemplazó Rollup por Rolldown como su bundler. La llave vieja todavía funciona a través de un shim de deprecación y muestra una advertencia en cada build, pero eventualmente se va a eliminar. worker.rollupOptions sigue el mismo renombre, a worker.rolldownOptions. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Todavía puedo usar manualChunks como objeto en Vite 8? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No. La forma de objeto de output.manualChunks se eliminó por completo, no nada más se deprecó, y usarla truena en tiempo de build. Solo la forma de función sigue funcionando, y esa también está marcada como deprecada a favor de la opción codeSplitting de Rolldown. Las configs de separación de vendors copiadas de tutoriales viejos son el lugar más común donde esto aparece. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Vite 8 es compatible con Yarn PnP? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No hay una postura oficial en ningún sentido en la documentación de Vite, pero el issue tracker de rolldown-vite tiene varios reportes abiertos de resolución de dependencias fallando bajo el modo Plug&amp;#39;n&amp;#39;Play de Yarn al momento de escribir esto. Los equipos con Yarn Berry y nodeLinker: pnp deberían probar en una rama antes de meter Vite 8 a CI, o quedarse en el paquete intermedio rolldown-vite@7.x. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Necesito actualizar @vitejs/plugin-react o @vitejs/plugin-vue para Vite 8? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; No estrictamente. @vitejs/plugin-vue funciona sin modificar, y la v5 de @vitejs/plugin-react sigue corriendo en Vite 8. La v6 de plugin-react es la actualización recomendada porque pasó de Babel a Oxc para el transform de Fast Refresh, lo que es más ligero de instalar y empata con el resto del toolchain de Vite 8, pero no es un requisito duro. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Desarrollo Web</category><category>JavaScript</category><category>Vite</category><category>Astro</category><category>Frontend</category><category>Migración</category><category>Upgrade</category><author>Marco Orta</author></item><item><title>¿Cuánto cuesta una página web en México en 2026? (y cuándo no deberías pagar por una)</title><link>https://ortamarco.me/blog/cuanto-cuesta-pagina-web-mexico-2026/</link><guid isPermaLink="true">https://ortamarco.me/blog/cuanto-cuesta-pagina-web-mexico-2026/</guid><description>Te lo dice un desarrollador, no una agencia: los rangos reales por tipo de sitio, los costos recurrentes que nadie desglosa en la cotización, las tres cosas que de verdad encarecen un proyecto — y los casos donde mi consejo es que no contrates a nadie.</description><pubDate>Sat, 22 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Una página web profesional en México cuesta en 2026 entre $8,000 y $25,000 MXN si es una landing o un sitio informativo, entre $25,000 y $80,000 MXN si es un sitio corporativo a medida, y de $50,000 MXN en adelante si hablamos de e-commerce o una aplicación web. A eso hay que sumarle lo que casi ninguna cotización desglosa: dominio ($200–800 al año), hosting ($40–300 al mes) y mantenimiento ($1,500–4,000 al mes si lo delegas).&lt;/strong&gt; Y hay un cuarto rango del que las agencias no te hablan: &lt;strong&gt;$0 de desarrollo&lt;/strong&gt;, porque hay negocios a los que hoy les conviene más un builder o un perfil de Google que pagar por un sitio.&lt;/p&gt;
&lt;p&gt;Yo desarrollo sitios y sistemas web — es decir, tengo el incentivo de venderte uno. Por eso esta guía va al revés de las de agencia: primero los números del mercado con sus fuentes, luego qué los mueve de verdad, y al final los casos en los que &lt;strong&gt;no&lt;/strong&gt; te conviene contratarme ni a mí ni a nadie.&lt;/p&gt;
&lt;p&gt;Si solo vienes por tu número: mi &lt;a href=&quot;https://ortamarco.me/cotizador/&quot;&gt;cotizador&lt;/a&gt; te da un rango al instante, eligiendo tipo de proyecto, complejidad y funciones. Sin dejar tu correo.&lt;/p&gt;
&lt;h2 id=&quot;los-rangos-del-mercado-en-2026&quot;&gt;Los rangos del mercado en 2026&lt;/h2&gt;
&lt;p&gt;Crucé lo que publican los estudios de precios de este año — &lt;a href=&quot;https://simplixy.com.mx/cuanto-cuesta-una-pagina-web-en-mexico-2026-guia-completa-de-precios/&quot;&gt;Simplixy&lt;/a&gt;, &lt;a href=&quot;https://gedx.com.mx/blog/cuanto-cuesta-pagina-web-mexico-2026/&quot;&gt;GEDX&lt;/a&gt; y &lt;a href=&quot;https://newemage.com.mx/cuanto-cuesta-una-pagina-web-en-mexico/&quot;&gt;New Emage&lt;/a&gt; entre otros — con lo que yo mismo cotizo. Coinciden más de lo que esperarías:&lt;/p&gt;








































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Tipo de sitio&lt;/th&gt;&lt;th&gt;Rango 2026 (MXN)&lt;/th&gt;&lt;th&gt;Qué incluye normalmente&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Hazlo tú mismo (builder)&lt;/td&gt;&lt;td&gt;$0 – $4,000 / año&lt;/td&gt;&lt;td&gt;Plantilla, editor visual, hosting incluido&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Landing page&lt;/td&gt;&lt;td&gt;$8,000 – $25,000&lt;/td&gt;&lt;td&gt;1 página enfocada a captar clientes o validar una idea&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Sitio informativo / profesional&lt;/td&gt;&lt;td&gt;$14,000 – $30,000&lt;/td&gt;&lt;td&gt;5–8 páginas, diseño personalizado, SEO básico&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Sitio corporativo a medida&lt;/td&gt;&lt;td&gt;$25,000 – $80,000&lt;/td&gt;&lt;td&gt;Diseño propio, contenido optimizado, integraciones&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;E-commerce&lt;/td&gt;&lt;td&gt;$27,000 – $126,000&lt;/td&gt;&lt;td&gt;Catálogo, pagos, envíos, facturación&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Aplicación web&lt;/td&gt;&lt;td&gt;$36,000 – $250,000+&lt;/td&gt;&lt;td&gt;Lógica de negocio propia, usuarios, paneles&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Dos advertencias sobre esta tabla, porque las tablas mienten con facilidad:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;El rango dentro de cada fila es más ancho que el rango entre filas.&lt;/strong&gt; Una landing compleja puede costar más que un sitio corporativo simple. El tipo de sitio es el punto de partida, no el precio.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Freelancer vs agencia mueve el número tanto como el proyecto.&lt;/strong&gt; El mismo sitio informativo va de $5,000–25,000 MXN con un freelancer a $15,900 MXN en adelante con agencia. No es que uno esté bien y otro mal: pagas cosas distintas — con la agencia, gestión y continuidad; con el freelancer, trato directo y menos capas.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;lo-que-la-cotización-no-suele-desglosar&quot;&gt;Lo que la cotización no suele desglosar&lt;/h2&gt;
&lt;p&gt;El precio del desarrollo es el número grande, pero es el único que pagas una vez. Estos son los que pagas para siempre:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dominio:&lt;/strong&gt; $200–800 MXN al año. Se renueva cada año y &lt;strong&gt;debe estar a tu nombre&lt;/strong&gt; — si tu proveedor lo registra a nombre suyo, el día que quieras cambiarte descubrirás que tu dominio no es tuyo. Es la letra chiquita más cara de esta industria.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hosting:&lt;/strong&gt; $40–300 MXN al mes en planes básicos; más si el sitio crece. Aplica lo mismo: acceso propio, siempre.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mantenimiento:&lt;/strong&gt; $1,500–4,000 MXN al mes si lo contratas. Aquí sí hay decisión real: un sitio informativo estático puede vivir años sin mantenimiento; un WordPress con plugins, no — sin actualizaciones es cosa de tiempo que lo hackeen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Renovaciones invisibles:&lt;/strong&gt; certificado SSL (hoy debería ser gratis con Let’s Encrypt — sospecha de quien lo cobre aparte), licencias de plugins o plantillas, y el correo corporativo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La pregunta que le haría yo a cualquier cotización: &lt;strong&gt;«¿qué de esto pago una vez y qué pago cada mes, y a nombre de quién queda cada cosa?»&lt;/strong&gt;. La respuesta te dice más del proveedor que su portafolio.&lt;/p&gt;
&lt;h2 id=&quot;las-tres-cosas-que-de-verdad-encarecen-un-proyecto&quot;&gt;Las tres cosas que de verdad encarecen un proyecto&lt;/h2&gt;
&lt;p&gt;Después de cotizar muchos proyectos, los factores que mueven el precio casi siempre son los mismos tres — y ninguno es “el diseño”:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Funcionalidad, no páginas.&lt;/strong&gt; Duplicar el número de páginas apenas mueve el precio; añadir pagos en línea, un área de clientes con sesión, facturación CFDI o una integración con tu sistema interno lo puede duplicar. Cada función de esas es software que hay que construir y probar.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;La complejidad del contenido y las decisiones.&lt;/strong&gt; Un proyecto donde el cliente trae textos, fotos y estructura clara cuesta horas; uno donde todo se decide en juntas cuesta semanas. Se cobra el tiempo, y el tiempo se va en decidir.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;La urgencia.&lt;/strong&gt; «Lo necesito en dos semanas» es un multiplicador, no un detalle. Comprimir un proyecto obliga a apartar otros, y eso se cobra.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Por eso desconfío de los precios cerrados por catálogo («sitio web: $9,999») tanto como de los presupuestos sin desglose: los dos esconden en qué fila de la tabla vas a caer &lt;em&gt;después&lt;/em&gt; de firmar.&lt;/p&gt;
&lt;h2 id=&quot;cuándo-no-deberías-pagar-por-una-página-web&quot;&gt;Cuándo NO deberías pagar por una página web&lt;/h2&gt;
&lt;p&gt;Esta sección no aparece en las guías de agencias, por razones obvias:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Si tu negocio vive de un radio de 5 cuadras&lt;/strong&gt; — una fonda, una estética, un taller — tu inversión digital con mejor retorno es un &lt;strong&gt;perfil de Google Business bien trabajado&lt;/strong&gt; (gratis) y WhatsApp Business (gratis). Una web de $15,000 MXN no te traerá más clientes que eso.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Si todavía estás validando la idea&lt;/strong&gt;, un builder tipo Wix o un sitio de una página hecho con plantilla ($0–4,000 MXN/año) es suficiente para probar. Pagar desarrollo a medida antes de saber si el negocio funciona es construir la casa antes que los cimientos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Si vendes por marketplace y te va bien ahí&lt;/strong&gt;, la web propia es diversificación, no urgencia. Llega, pero no tiene que ser hoy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;¿Cuándo sí? Cuando el sitio tiene un trabajo concreto que hacer — captar leads, vender, automatizar algo que hoy haces a mano — y puedes ponerle número a ese trabajo. En ese momento el sitio deja de ser un gasto de imagen y se vuelve una inversión con retorno medible. De medirlo escribí en la &lt;a href=&quot;https://ortamarco.me/blog/geo-aeo-aparecer-en-chatgpt-2026/&quot;&gt;guía de GEO y AEO&lt;/a&gt;: de nada sirve un sitio de $80,000 que ni Google ni ChatGPT pueden leer.&lt;/p&gt;
&lt;h2 id=&quot;cómo-cotizo-yo-para-que-compares-con-lo-que-te-ofrezcan&quot;&gt;Cómo cotizo yo (para que compares con lo que te ofrezcan)&lt;/h2&gt;
&lt;p&gt;Mi &lt;a href=&quot;https://ortamarco.me/cotizador/&quot;&gt;cotizador&lt;/a&gt; es público y funciona sin registro: eliges tipo de proyecto (landing, corporativo, e-commerce, aplicación web, API, app móvil), nivel de complejidad y funciones concretas, y te da el rango en pesos o dólares al momento. Los rangos que arroja para los proyectos típicos van de &lt;strong&gt;$3,600 MXN&lt;/strong&gt; (una landing mínima) hasta los cientos de miles de una aplicación completa — los mismos órdenes de magnitud de la tabla de arriba, porque el mercado mexicano de desarrollo serio converge en esos números.&lt;/p&gt;
&lt;p&gt;Lo que sí varía entre proveedores — y donde de verdad se compite — es lo de siempre: el desglose claro, a nombre de quién quedan dominio y hosting, y si el que te cotiza va a estar ahí en un año. Pregunta esas tres cosas antes que el precio.&lt;/p&gt;
&lt;p&gt;Si prefieres platicar el proyecto directamente, en &lt;a href=&quot;https://ortamarco.me/servicio-desarrollo-web/&quot;&gt;servicio de desarrollo web&lt;/a&gt; está el detalle de cómo trabajo.&lt;/p&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuánto cuesta una página web profesional en México en 2026? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Los rangos de mercado en 2026: una landing page entre $8,000 y $25,000 MXN; un sitio informativo profesional de 5 a 8 páginas entre $14,000 y $30,000 MXN; un sitio corporativo a medida entre $25,000 y $80,000 MXN; y un e-commerce desde unos $27,000 MXN. A eso se suman los costos recurrentes: dominio ($200–800 al año), hosting ($40–300 al mes) y mantenimiento ($1,500–4,000 al mes si se contrata). &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué es más barato: freelancer o agencia? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; El freelancer casi siempre cotiza más bajo: un sitio informativo va de $5,000 a $25,000 MXN con freelancer, mientras una agencia parte de unos $15,900 MXN por proyectos similares. Pero pagan cosas distintas: la agencia cobra gestión, equipo y continuidad; el freelancer ofrece trato directo y menos capas. El criterio útil no es el precio sino el desglose, la propiedad del dominio y quién dará mantenimiento en un año. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuánto cuesta mantener una página web al mes en México? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Los costos recurrentes mínimos son el hosting ($40–300 MXN al mes en planes básicos) y el dominio (unos $200–800 MXN al año). Si se contrata mantenimiento —actualizaciones, seguridad, cambios menores— el mercado ronda entre $1,500 y $4,000 MXN mensuales. Un sitio estático bien hecho puede vivir con el mínimo; un WordPress con plugins necesita mantenimiento activo o termina hackeado. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuándo conviene NO pagar por una página web? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Cuando el negocio es puramente local y de radio corto (una fonda, una estética), donde un perfil de Google Business y WhatsApp Business gratuitos rinden más; cuando la idea de negocio aún no está validada, donde basta un builder de $0 a $4,000 MXN al año; y cuando las ventas ya funcionan por marketplace y la web propia es diversificación, no urgencia. El sitio a medida conviene cuando tiene un trabajo medible: captar clientes, vender o automatizar. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué encarece más una página web: el diseño o las funciones? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Las funciones. Duplicar el número de páginas apenas mueve el precio, pero añadir pagos en línea, área de clientes, facturación CFDI o integraciones con sistemas internos puede duplicarlo, porque cada función es software que hay que construir y probar. Los otros dos factores que más pesan son la claridad del contenido que aporta el cliente y la urgencia del plazo. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿El dominio y el hosting deben quedar a mi nombre? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Siempre. Si el proveedor registra el dominio a su nombre, el negocio no es dueño de su propia dirección en internet, y cambiar de proveedor se convierte en una negociación. Antes de firmar, hay que confirmar que el dominio queda a nombre del cliente y que se entregan los accesos del hosting. Es la pregunta que más problemas evita en toda la contratación. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Desarrollo Web</category><category>Desarrollo Web</category><category>Precios</category><category>México</category><category>Pymes</category><category>Freelance</category><author>Marco Orta</author></item><item><title>Los mejores servidores MCP para SEO y GEO en 2026: cuál conectar según tu caso</title><link>https://ortamarco.me/blog/mejores-servidores-mcp-seo-2026/</link><guid isPermaLink="true">https://ortamarco.me/blog/mejores-servidores-mcp-seo-2026/</guid><description>«¿Cuál es el mejor MCP para SEO o GEO?» tiene una respuesta distinta según qué datos necesites — y una de las opciones no cuesta nada ni pide API key.</description><pubDate>Sat, 22 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;No existe «el mejor servidor MCP para SEO»: existe el mejor para cada tipo de dato. Si lo que quieres es leer el rendimiento real de tu sitio, el MCP de Google Search Console es gratis y nadie tiene datos más exactos. Si necesitas datos de terceros —keywords, competidores, SERPs— la decisión está entre DataForSEO (pago por consulta) y Ahrefs o Semrush (suscripción). Y si tu pregunta es de GEO — &lt;em&gt;¿puede ChatGPT leer y citar mi web?&lt;/em&gt; — esa capa la cubren herramientas de auditoría como mi seo-geo-mcp, que es open source y no pide ni API key.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A mediados de 2026, prácticamente toda plataforma SEO grande tiene un servidor MCP oficial o está corriendo para publicarlo. Eso es bueno y malo a la vez: hay dónde elegir, pero los listados de «los 12 mejores» mezclan cosas que no compiten entre sí. Esta guía los ordena por la única pregunta que de verdad decide: &lt;strong&gt;¿de quién son los datos que tu agente necesita?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Si todavía no tienes claro qué es MCP o cómo un agente usa estas conexiones, empieza por la &lt;a href=&quot;https://ortamarco.me/blog/ia-agentica-agentes-de-ia-2026/&quot;&gt;guía de IA agéntica, agentes y MCP&lt;/a&gt; y vuelve: esta pieza asume esa base.&lt;/p&gt;
&lt;h2 id=&quot;la-tabla-completa&quot;&gt;La tabla completa&lt;/h2&gt;













































































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Servidor&lt;/th&gt;&lt;th&gt;Datos que expone&lt;/th&gt;&lt;th&gt;Precio&lt;/th&gt;&lt;th&gt;Tipo&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Google Search Console&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Clicks, impresiones, CTR, posiciones &lt;strong&gt;de tu sitio&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Gratis&lt;/td&gt;&lt;td&gt;Solo de la comunidad: Google no tiene uno oficial&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Google Analytics 4&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Eventos, conversiones, embudos de tu sitio&lt;/td&gt;&lt;td&gt;Gratis&lt;/td&gt;&lt;td&gt;Oficial (experimental)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;DataForSEO&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;SERPs, keywords, backlinks, on-page (Google, Bing, Baidu)&lt;/td&gt;&lt;td&gt;Pago por consulta&lt;/td&gt;&lt;td&gt;Oficial&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Ahrefs&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;20 grupos de tools: Site Explorer, Keywords, Site Audit, Rank Tracker, Brand Radar&lt;/td&gt;&lt;td&gt;Por unidades, según plan&lt;/td&gt;&lt;td&gt;Oficial&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Semrush&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Keywords (más de 28.000 M, 142 bases regionales), competencia&lt;/td&gt;&lt;td&gt;Ligado a suscripción&lt;/td&gt;&lt;td&gt;Oficial&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SE Ranking&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;180+ tools: keywords, auditorías, backlinks, visibilidad en IA&lt;/td&gt;&lt;td&gt;Ligado a suscripción o API de pago por uso&lt;/td&gt;&lt;td&gt;Oficial&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Nightwatch&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Rankings (Google/Bing/YouTube) + visibilidad en IA&lt;/td&gt;&lt;td&gt;Desde plan de pago&lt;/td&gt;&lt;td&gt;Oficial&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Advanced Web Ranking&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;48 tools de rank tracking histórico y comparativas&lt;/td&gt;&lt;td&gt;Plan Agency o superior&lt;/td&gt;&lt;td&gt;Oficial&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Screaming Frog&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Orquestar rastreos y auditorías técnicas desde el agente&lt;/td&gt;&lt;td&gt;Con la licencia (v24+)&lt;/td&gt;&lt;td&gt;Oficial, local&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Peec AI&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Citaciones y visibilidad en 8 motores de IA (ChatGPT, Perplexity, Gemini, Claude, Copilot…)&lt;/td&gt;&lt;td&gt;Planes de pago&lt;/td&gt;&lt;td&gt;Oficial&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;seo-geo-mcp&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;17 tools de auditoría on-page + GEO: robots, crawlers de IA, schema, hreflang&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Gratis, sin API keys&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Open source (npm), local o auto-hospedado&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Los datos de tools y precios salen de la documentación de cada plataforma y de los análisis comparativos de &lt;a href=&quot;https://nightwatch.io/blog/best-seo-mcp-servers/&quot;&gt;Nightwatch&lt;/a&gt; y &lt;a href=&quot;https://mcp.directory/blog/best-seo-mcp-servers-2026&quot;&gt;MCP.Directory&lt;/a&gt;; las cifras concretas de tu factura dependen de tu plan.&lt;/p&gt;
&lt;h3 id=&quot;cómo-se-conecta-cada-uno-y-cuánto-cuesta-entrar&quot;&gt;Cómo se conecta cada uno y cuánto cuesta entrar&lt;/h3&gt;
&lt;p&gt;Verificado el 13 de septiembre de 2026 en la documentación y la página de precios de cada plataforma (enlazadas). «Remoto» es un servidor que aloja la plataforma y al que te conectas por HTTP; «local» es un proceso que corre en tu máquina, normalmente por stdio. Precios de lista en dólares o euros, sin impuestos y con pago mensual salvo que se indique.&lt;/p&gt;













































































&lt;div class=&quot;table-responsive&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Servidor&lt;/th&gt;&lt;th&gt;Transporte&lt;/th&gt;&lt;th&gt;Autenticación&lt;/th&gt;&lt;th&gt;Precio de entrada&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Google Search Console&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Local (stdio), implementaciones de la comunidad&lt;/td&gt;&lt;td&gt;Credenciales de Google Cloud (OAuth o service account)&lt;/td&gt;&lt;td&gt;0 USD&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/googleanalytics/google-analytics-mcp&quot;&gt;Google Analytics 4&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Local (stdio, &lt;code&gt;pipx run analytics-mcp&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;Credenciales de Google Cloud, alcance de solo lectura&lt;/td&gt;&lt;td&gt;0 USD&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/dataforseo/mcp-server-typescript&quot;&gt;DataForSEO&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Remoto (&lt;code&gt;mcp.dataforseo.com/v3/mcp&lt;/code&gt;), HTTP local o stdio&lt;/td&gt;&lt;td&gt;OAuth por HTTP; login y contraseña de la API por stdio&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://dataforseo.com/help-center/how-does-your-free-unlimited-trial-work&quot;&gt;1 USD de saldo de prueba&lt;/a&gt; al registrarte; &lt;a href=&quot;https://dataforseo.com/pricing&quot;&gt;recarga mínima de 50 USD&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://docs.ahrefs.com/docs/mcp/reference/introduction&quot;&gt;Ahrefs&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Remoto (&lt;code&gt;api.ahrefs.com/mcp/mcp&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;OAuth o clave MCP&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://ahrefs.com/pricing&quot;&gt;Plan Lite, 129 USD/mes&lt;/a&gt;; consume las unidades de API del plan&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.semrush.com/api/v4/introduction/semrush-mcp/&quot;&gt;Semrush&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Remoto (&lt;code&gt;mcp.semrush.com/v2/mcp&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;OAuth o API key&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.semrush.com/prices/&quot;&gt;Semrush One Starter, 199 USD/mes&lt;/a&gt;, u otro plan de su lista; incluyen 50.000 unidades de API&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://seranking.com/mcp.html&quot;&gt;SE Ranking&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Remoto&lt;/td&gt;&lt;td&gt;Autorizas la conexión con tu cuenta&lt;/td&gt;&lt;td&gt;Prueba con 100.000 créditos sin tarjeta; API de pago por uso desde 50 USD por 250.000 créditos&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://nightwatch.io/seo-mcp/&quot;&gt;Nightwatch&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Remoto (&lt;code&gt;mcp.nightwatch.io&lt;/code&gt;, SSE)&lt;/td&gt;&lt;td&gt;Token Bearer&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://nightwatch.io/pricing/&quot;&gt;Starter, 79 €/mes con pago anual&lt;/a&gt; (948 €/año); el MCP va en todos los planes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://www.advancedwebranking.com/help/use-awr-api-data-in-chatgpt-and-claude&quot;&gt;Advanced Web Ranking&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Remoto (&lt;code&gt;api.advancedwebranking.com/mcp&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;OAuth o API key&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.advancedwebranking.com/pricing&quot;&gt;Plan Agency, 279 USD/mes&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://www.screamingfrog.co.uk/seo-spider/user-guide/configuration/&quot;&gt;Screaming Frog&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Local, junto a SEO Spider v24+ (stdio o Streamable HTTP)&lt;/td&gt;&lt;td&gt;La licencia del programa&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.screamingfrog.co.uk/seo-spider/pricing/&quot;&gt;Licencia de 279 USD/año&lt;/a&gt;; el MCP no funciona en la versión gratuita&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://docs.peec.ai/mcp/introduction&quot;&gt;Peec AI&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Remoto (&lt;code&gt;api.peec.ai/mcp&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;OAuth o token personal&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://peec.ai/pricing&quot;&gt;Starter, 95 USD/mes&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://ortamarco.me/portfolio/seo-geo-mcp/&quot;&gt;seo-geo-mcp&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Local (stdio, &lt;code&gt;npx -y seo-geo-mcp-server&lt;/code&gt;) o HTTP auto-hospedado&lt;/td&gt;&lt;td&gt;Ninguna (token Bearer opcional en HTTP)&lt;/td&gt;&lt;td&gt;0 USD&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Dos avisos que no caben en la tabla. El repositorio local de Ahrefs (&lt;a href=&quot;https://github.com/ahrefs/ahrefs-mcp-server&quot;&gt;&lt;code&gt;ahrefs/ahrefs-mcp-server&lt;/code&gt;&lt;/a&gt;) está archivado y su propio README pide no usarlo: el oficial es el remoto. Y ni Google ni Microsoft publican un MCP para sus consolas: la &lt;a href=&quot;https://github.com/google/mcp&quot;&gt;lista oficial de servidores MCP de Google&lt;/a&gt; incluye Analytics pero no Search Console, y la &lt;a href=&quot;https://github.com/google/mcp/issues/17&quot;&gt;petición de uno oficial&lt;/a&gt; sigue abierta; para Bing Webmaster Tools tampoco encontré uno de Microsoft, solo implementaciones de la comunidad.&lt;/p&gt;
&lt;h2 id=&quot;caso-1-quieres-leer-los-datos-de-tu-sitio&quot;&gt;Caso 1: quieres leer los datos de TU sitio&lt;/h2&gt;
&lt;p&gt;Empieza aquí, porque es gratis y es el dato más exacto que existe. El &lt;strong&gt;MCP de Google Search Console&lt;/strong&gt; le da a tu agente acceso directo a clicks, impresiones, CTR y posición por consulta y por página — el mismo dato de primera mano que ves en la consola, sin muestreos de terceros. Ojo con el nombre: Google no publica un MCP de Search Console. Todas las implementaciones son open source y de la comunidad, y todas piden credenciales de Google Cloud (OAuth o service account), que es el único peaje de instalación.&lt;/p&gt;
&lt;p&gt;Su límite es estructural, no del MCP: Search Console solo retiene &lt;strong&gt;16 meses&lt;/strong&gt; de historia. Si quieres series más largas, tu agente tiene que ir guardando snapshots — el problema deja de ser de conexión y pasa a ser de almacenamiento.&lt;/p&gt;
&lt;p&gt;El &lt;strong&gt;MCP de Google Analytics 4&lt;/strong&gt; (oficial, todavía experimental) es su pareja natural: GSC te dice cómo llegan, GA4 qué hacen después. Juntos cubren el embudo completo sin gastar un centavo.&lt;/p&gt;
&lt;h2 id=&quot;caso-2-necesitas-datos-de-terceros--keywords-serps-competidores&quot;&gt;Caso 2: necesitas datos de terceros — keywords, SERPs, competidores&lt;/h2&gt;
&lt;p&gt;Aquí es donde está la decisión con dinero de por medio, y el criterio es el patrón de uso:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DataForSEO&lt;/strong&gt; cobra &lt;strong&gt;por consulta&lt;/strong&gt;, sin suscripción: 1 USD de prueba al registrarte y recargas desde 50 USD. Para flujos de agente — que hacen ráfagas de consultas puntuales, no monitoreo continuo — suele salir mucho más barato que una suscripción completa. Su contra: no hay interfaz; es una capa de datos pura y la calidad del resultado depende por completo de cómo pregunte tu agente.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ahrefs&lt;/strong&gt; reparte sus herramientas en 20 grupos (Site Explorer, Keywords Explorer, Site Audit, Rank Tracker, Brand Radar…) sobre su índice de enlaces, con un sistema de unidades por plan desde Lite. Son tantas que su propia documentación admite que superan el límite de algunos clientes MCP, y por eso acepta &lt;code&gt;?tools=essentials&lt;/code&gt; o una lista de grupos en la URL. El riesgo práctico es nuevo y muy de 2026: &lt;strong&gt;un agente en bucle puede quemar tus unidades del mes en una tarde&lt;/strong&gt;. Si lo conectas, ponle presupuesto por sesión (Ahrefs deja fijar un tope mensual de unidades por clave).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Semrush&lt;/strong&gt; juega la carta de la cobertura (más de 28.000 millones de keywords en 142 bases regionales). Su MCP funciona en ChatGPT, Claude, Cursor, VS Code y Perplexity, y de su API de proyectos solo expone los métodos de lectura.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SE Ranking&lt;/strong&gt; es el de mayor superficie (180+ tools) — tanta, que conviene podar las que expones por sesión: a los agentes les cuesta elegir entre 180 opciones.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mi lectura práctica: para automatizaciones puntuales y agentes, DataForSEO; si ya pagas Ahrefs o Semrush por otra razón, su MCP es un añadido obvio y no una razón para suscribirse.&lt;/p&gt;
&lt;h2 id=&quot;caso-3-rank-tracking-y-auditoría-técnica&quot;&gt;Caso 3: rank tracking y auditoría técnica&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nightwatch&lt;/strong&gt; y &lt;strong&gt;Advanced Web Ranking&lt;/strong&gt; son rank trackers con MCP: posiciones históricas, ganadores/perdedores, comparativas multi-proyecto. AWR pide plan Agency, así que solo tiene sentido en agencias.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Screaming Frog&lt;/strong&gt; (desde la v24) trae MCP oficial: tu agente puede &lt;strong&gt;orquestar rastreos&lt;/strong&gt; — «rastrea el sitio y dime qué páginas tienen títulos duplicados» — sobre la herramienta que ya usabas. Corre local, atado a tu licencia y a tu máquina: en la versión gratuita del programa el MCP no funciona.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;caso-4-geo--pueden-los-motores-de-ia-leerte-y-citarte&quot;&gt;Caso 4: GEO — ¿pueden los motores de IA leerte y citarte?&lt;/h2&gt;
&lt;p&gt;Esta es la capa nueva, y la que más se confunde con el SEO clásico. La pregunta ya no es «¿posiciono?» sino &lt;strong&gt;«¿puede GPTBot rastrearme, entiende mi schema, y estoy bloqueando sin querer al bot que me citaría?»&lt;/strong&gt;. Dos herramientas la atacan desde ángulos opuestos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Peec AI&lt;/strong&gt; mide el resultado: qué motores de IA te citan, cuánto, y contra quién compites en cada respuesta.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://ortamarco.me/portfolio/seo-geo-mcp/&quot;&gt;seo-geo-mcp&lt;/a&gt;&lt;/strong&gt; — mi servidor, open source — audita la causa: 17 herramientas de solo lectura que revisan si tu página es rastreable y citable. Separa el bloqueo de entrenamiento del de citación (bloquear &lt;code&gt;GPTBot&lt;/code&gt; impide entrenar; bloquear &lt;code&gt;OAI-SearchBot&lt;/code&gt; te borra de las respuestas de ChatGPT — mucha gente hizo lo segundo queriendo lo primero), detecta páginas que renderizan solo en cliente (Googlebot ejecuta JavaScript; &lt;code&gt;GPTBot&lt;/code&gt;, &lt;code&gt;ClaudeBot&lt;/code&gt; y &lt;code&gt;PerplexityBot&lt;/code&gt; en general no), y valida schema, hreflang, sitemaps y redirecciones. Sin API keys: todo se resuelve con HTTP, DNS y robots.txt públicos.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lo que seo-geo-mcp &lt;strong&gt;no&lt;/strong&gt; hace, para que no lo conectes esperando otra cosa: no tiene datos de keywords, ni de backlinks, ni de posiciones. Es el auditor, no la base de datos. La combinación que uso yo: GSC para el rendimiento, seo-geo-mcp para el diagnóstico, y esta pareja cubre el 90 % del trabajo de un sitio propio sin pagar nada.&lt;/p&gt;
&lt;p&gt;El porqué de invertir en esta capa tiene números detrás: el &lt;a href=&quot;https://arxiv.org/abs/2311.09735&quot;&gt;estudio de GEO de Princeton&lt;/a&gt; (KDD 2024) midió que optimizar el contenido para motores generativos —citas, estadísticas, fuentes— puede subir la visibilidad en sus respuestas hasta un &lt;strong&gt;40 %&lt;/strong&gt;. Cómo aplicarlo lo tienes en la &lt;a href=&quot;https://ortamarco.me/blog/geo-aeo-aparecer-en-chatgpt-2026/&quot;&gt;guía de GEO y AEO: aparecer en ChatGPT&lt;/a&gt;.&lt;/p&gt;
&lt;aside class=&quot;not-prose my-8 flex flex-wrap items-center gap-4 rounded-2xl border border-neutral-100 bg-white p-5 transition-all duration-200 hover:shadow-[5px_5px_rgba(0,98,90,0.3),10px_10px_rgba(0,98,90,0.2),15px_15px_rgba(0,98,90,0.1)] dark:border-zinc-800 dark:bg-zinc-900/40&quot; style=&quot;border-left:4px solid #d72cef&quot;&gt; &lt;span class=&quot;grid size-12 shrink-0 place-items-center rounded-xl&quot; style=&quot;background:#d72cef1a;color:#d72cef&quot;&gt;  &lt;/span&gt; &lt;div class=&quot;flex min-w-0 flex-1 flex-col gap-0.5&quot;&gt; &lt;span class=&quot;text-xs font-semibold tracking-wide text-zinc-500 uppercase dark:text-zinc-400&quot;&gt; Herramienta gratuita &lt;/span&gt; &lt;span class=&quot;text-lg leading-snug font-bold text-blacktext dark:text-mint-50&quot;&gt; Informe de Salud del Dominio &lt;/span&gt; &lt;span class=&quot;text-sm text-pretty text-zinc-600 dark:text-zinc-400&quot;&gt; Analiza un dominio entero en una sola consulta: DNS, SPF, DKIM, DMARC, DNSSEC, CAA, certificado TLS y cabeceras de seguridad, con una lista priorizada de qué arreglar. &lt;/span&gt; &lt;/div&gt; &lt;a href=&quot;https://ortamarco.me/herramientas/informe-salud-dominio/&quot; data-umami-event=&quot;tool_callout_click&quot; data-umami-event-tool=&quot;informe-salud-dominio&quot; class=&quot;ml-auto shrink-0 rounded-xl bg-mint-600 px-4 py-2 text-sm! font-semibold text-white! no-underline! transition-colors hover:bg-mint-700 hover:text-white!&quot;&gt; Abrir herramienta → &lt;/a&gt; &lt;/aside&gt;
&lt;h2 id=&quot;conectar-uno-en-60-segundos&quot;&gt;Conectar uno, en 60 segundos&lt;/h2&gt;
&lt;p&gt;Con Claude Code, un MCP remoto se añade con un comando (la URL exacta la da la documentación de cada plataforma):&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;claude mcp &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--transport&lt;/span&gt; http ahrefs https://api.ahrefs.com/mcp/mcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y uno local (como seo-geo-mcp), que Claude Code arranca como proceso y le habla por stdio. Como está publicado en npm, no hace falta clonar nada:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; data-language=&quot;bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;claude mcp &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; seo-geo -- npx &lt;span class=&quot;token parameter variable&quot;&gt;-y&lt;/span&gt; seo-geo-mcp-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dos consejos que ahorran disgustos: &lt;strong&gt;conecta solo los que uses en esa sesión&lt;/strong&gt; — cada servidor suma herramientas al contexto del agente y demasiadas opciones lo vuelven torpe — y en los de pago, &lt;strong&gt;revisa el consumo tras las primeras sesiones&lt;/strong&gt;: el patrón de consulta de un agente no se parece al de un humano.&lt;/p&gt;
&lt;h2 id=&quot;el-resumen-honesto&quot;&gt;El resumen honesto&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sitio propio, presupuesto cero:&lt;/strong&gt; Search Console MCP + seo-geo-mcp. Gratis los dos, cubren rendimiento y diagnóstico.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Investigación de keywords y competidores:&lt;/strong&gt; DataForSEO si pagas por uso; Ahrefs/Semrush si ya tienes la suscripción.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agencia con reporting:&lt;/strong&gt; SE Ranking o Nightwatch, que traen el rank tracking integrado.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auditoría técnica profunda:&lt;/strong&gt; Screaming Frog v24+ con su MCP local.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Medir citaciones en IA:&lt;/strong&gt; Peec AI, cuando ya hiciste los deberes de las capas anteriores.&lt;/li&gt;
&lt;/ul&gt;
 &lt;section class=&quot;not-prose my-10&quot;&gt; &lt;h2 class=&quot;mb-6 font-fraunces text-3xl font-bold text-blacktext dark:text-white&quot;&gt; Preguntas frecuentes &lt;/h2&gt; &lt;div class=&quot;flex flex-col gap-3&quot;&gt; &lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué es un servidor MCP de SEO? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Es un servidor del Model Context Protocol que expone datos o herramientas SEO —posiciones, keywords, backlinks, auditorías— a un agente de IA como Claude o ChatGPT. En vez de copiar y pegar informes, el agente consulta la fuente directamente: pide las posiciones a Search Console, lanza un rastreo a Screaming Frog o audita el robots.txt de una página, y razona sobre el resultado. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Cuál es el mejor servidor MCP para SEO en 2026? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Depende del dato que necesites. Para el rendimiento de tu propio sitio, el MCP de Google Search Console (gratis y con el dato más exacto posible). Para datos de terceros como keywords y SERPs, DataForSEO si prefieres pagar por consulta, o Ahrefs/Semrush si ya tienes su suscripción. Para auditoría GEO —si los motores de IA pueden leer y citar tu web— seo-geo-mcp, que es open source y no requiere API keys. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Hay servidores MCP de SEO gratuitos? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Sí. Los MCP de Google Search Console (todos de la comunidad: Google no publica uno oficial) y el de Google Analytics 4 son gratuitos (solo piden credenciales de Google Cloud), y seo-geo-mcp es open source y funciona sin ninguna API key porque todo lo resuelve con HTTP, DNS y robots.txt públicos. Con esos tres se cubre el rendimiento y el diagnóstico de un sitio propio sin coste. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Qué diferencia hay entre un MCP de SEO y uno de GEO? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; El de SEO responde preguntas del buscador clásico: posiciones, keywords, backlinks. El de GEO responde si los motores generativos —ChatGPT, Perplexity, Gemini— pueden rastrear, leer y citar tu página: si robots.txt bloquea al bot equivocado, si la página depende de JavaScript que esos crawlers no ejecutan, o si el schema está bien formado. Son capas complementarias: puedes posicionar bien en Google y ser invisible para los asistentes de IA. &lt;/p&gt; &lt;/details&gt;&lt;details class=&quot;group rounded-2xl border border-zinc-200 bg-white/60 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-900/40&quot;&gt; &lt;summary class=&quot;flex cursor-pointer list-none items-start justify-between gap-4&quot;&gt; &lt;h3 class=&quot;text-lg font-semibold text-blacktext dark:text-white&quot;&gt; ¿Un agente de IA puede gastar mucho dinero usando un MCP de pago? &lt;/h3&gt;  &lt;/summary&gt; &lt;p class=&quot;mt-3 text-lg leading-relaxed text-zinc-700 max-md:text-base dark:text-zinc-300&quot;&gt; Sí, y es el riesgo práctico más nuevo de esta categoría: un agente en bucle hace ráfagas de consultas que no se parecen al uso humano, y en servidores con sistema de unidades como Ahrefs puede consumir la cuota mensual en una sesión larga. La defensa es ponerle presupuesto por sesión, conectar solo los servidores necesarios y revisar el consumo tras las primeras sesiones de uso. &lt;/p&gt; &lt;/details&gt; &lt;/div&gt; &lt;/section&gt;</content:encoded><category>Desarrollo Web</category><category>SEO</category><category>GEO</category><category>MCP</category><category>IA</category><category>Agentes</category><author>Marco Orta</author></item></channel></rss>