<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Juan Torchia</title>
    <description>The latest articles on DEV Community by Juan Torchia (@jtorchia).</description>
    <link>https://dev.to/jtorchia</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F885942%2F099b05dc-1940-49f6-a022-9c6a392bb405.jpg</url>
      <title>DEV Community: Juan Torchia</title>
      <link>https://dev.to/jtorchia</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jtorchia"/>
    <language>en</language>
    <item>
      <title>Certificate Policies, Path Validation and CRL: Three Layers RFC 5280 Doesn't Require You to Link</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Fri, 18 Sep 2026 14:30:17 +0000</pubDate>
      <link>https://dev.to/jtorchia/certificate-policies-path-validation-and-crl-three-layers-rfc-5280-doesnt-require-you-to-link-4935</link>
      <guid>https://dev.to/jtorchia/certificate-policies-path-validation-and-crl-three-layers-rfc-5280-doesnt-require-you-to-link-4935</guid>
      <description>&lt;p&gt;Two implementations that both claim "we comply with RFC 5280" can reach different conclusions about the same certification path, and neither one is violating the standard. This happens because the RFC defines three separate mechanisms — the certificate profile, the path validation algorithm, and CRL checking — and only one of them has a strict mandatory floor. The other two leave explicit room for MAY.&lt;/p&gt;

&lt;p&gt;This piece separates those three layers using the RFC's own text, without assuming how any particular library or real-world verifier implements them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: Certificate Policies is data in the certificate, not a process
&lt;/h2&gt;

&lt;p&gt;Section 4.2.1.4 of RFC 5280 defines the Certificate Policies extension within the X.509v3 certificate profile, in the same block as Policy Mappings (4.2.1.5) and Policy Constraints (4.2.1.11). It's information that travels inside the certificate: a set of OIDs declaring under which policy it was issued.&lt;/p&gt;

&lt;p&gt;That extension, by itself, doesn't validate anything. It's data available for some other process to use or ignore. The document's own table of contents shows the structural separation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4.2.1.4. Certificate Policies ......................32
4.2.1.5. Policy Mappings ...........................35
4.2.1.11. Policy Constraints .......................43
...
6.1. Basic Path Validation .....................72
6.3. CRL Validation .............................90
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The certificate profile (Section 4) and path validation (Section 6) are different sections of the RFC. The fact that a certificate carries Certificate Policies doesn't imply the verifier will read them, let alone require them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: the path validation algorithm defines a minimum, not a maximum
&lt;/h2&gt;

&lt;p&gt;Section 6.1 describes the basic certification path validation algorithm. The RFC is explicit about its scope:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Thus, the path validation algorithm presented in Section 6.1 defines the minimum conditions for a path to be considered valid."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's a floor. Any implementation can extend that floor, and the RFC says so using the same word it uses to mark anything optional: MAY.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"An implementation MAY augment the algorithm presented in Section 6.1 to further limit the set of valid certification paths that begin with a particular trust anchor."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The examples the text itself gives: applying a path length restriction to a specific trust anchor, requiring a particular form of alternative name in the target certificate, or imposing requirements on application-specific extensions. And there's one specific case that connects directly to layer 1: a trust anchor can be limited to trusting only paths associated with a particular certificate policy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"...a trusted CA may only be trusted for a particular certificate policy. This restriction can be expressed through the inputs to the path validation procedure."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words: the Certificate Policies from layer 1 only matter if the algorithm from layer 2 receives them as input and decides to use them. An implementation can run the basic algorithm from Section 6.1, completely ignore those additional inputs, and still be RFC-compliant. The text is equally clear about self-signed trust anchors: they can carry a policy constraints extension stating that paths starting there should only be trusted for specific policies, but the basic algorithm "does not assume that this information is present" and "does not specify processing rules" for it. Whether to process it or ignore it is left to each implementation's discretion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: CRL checking is, literally, optional to implement as an algorithm
&lt;/h2&gt;

&lt;p&gt;Section 6.3 describes how to determine whether a certificate is revoked when the revocation mechanism is a CRL. Here the RFC doesn't say MAY — it says something even stronger:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Conforming implementations that support CRLs are not required to implement this algorithm, but they MUST be functionally equivalent to the external behavior resulting from this procedure when processing CRLs that are issued in conformance with this profile."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There's no need to implement the detailed step-by-step algorithm. It's enough to produce the same external result. This is a wide implementation freedom: two verifiers can reach the same revoked/not-revoked conclusion through completely different internal paths, and both are conformant.&lt;/p&gt;

&lt;p&gt;There's also an operational detail the RFC flags that a basic implementation might overlook: when the CA signs certificates and CRLs with different private keys, verifying revocation requires building and validating a second, separate, full certification path for the certificate that signs the CRL.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"CRL checking in turn requires a separate certification path to be constructed and validated for the CA's CRL signature validation certificate."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The RFC even adjusts the level of obligation depending on the scenario:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Applications that perform CRL checking MUST support certification path validation when certificates and CRLs are digitally signed with the same CA private key. These applications SHOULD support certification path validation when certificates and CRLs are digitally signed with different CA private keys."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With the same key, it's a MUST. With separate keys, it drops to SHOULD. An implementation that only covers the shared-key case isn't violating the RFC: it's covering the MUST and leaving out the SHOULD.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three layers together
&lt;/h2&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  A[Certificado X.509v3] --&amp;gt;|contiene| B[Certificate Policies OIDs&amp;lt;br/&amp;gt;seccion 4.2.1.4]
  A --&amp;gt; C[Algoritmo de path validation&amp;lt;br/&amp;gt;seccion 6.1 - piso minimo]
  B -.-&amp;gt;|input opcional MAY| C
  C --&amp;gt; D{Verificador soporta CRL?}
  D --&amp;gt;|si| E[CRL Validation&amp;lt;br/&amp;gt;seccion 6.3 - algoritmo no obligatorio]
  D --&amp;gt;|no| F[Ruta valida sin chequeo de revocacion]
  E --&amp;gt; G{Misma clave CA/CRL?}
  G --&amp;gt;|si, MUST| H[Path validation exigido]
  G --&amp;gt;|no, SHOULD| I[Path validation recomendado, no obligatorio]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The diagram shows where the leeway is: the dotted arrow between Certificate Policies and the Section 6.1 algorithm is an optional input, not an automatic link. A certificate can declare a policy, and that declaration might never end up influencing the validation result, because it depends on whether the verifier requests it as input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for interoperability
&lt;/h2&gt;

&lt;p&gt;Two verifiers can both accept exactly the same certification path as valid, even though one of them ignores the certificate's Certificate Policies and skips revocation checking entirely, while the other applies additional policy restrictions and full CRL checking with path validation for the CRL signing key. Neither one violates RFC 5280: the first covered the minimum from Section 6.1; the second covered additional MAYs that the RFC permits but doesn't require.&lt;/p&gt;

&lt;p&gt;This is an editorial reading, not a textual finding from the RFC itself: the document doesn't address legal interoperability or legally valid digital signatures. But the normative structure — a narrow mandatory floor, wide optional extensions — explains why "RFC 5280 compliant" is a necessary condition, not a sufficient one, for two verification systems to reach the same verdict on a signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits of this reading
&lt;/h2&gt;

&lt;p&gt;The excerpt of RFC 5280 available for this piece is truncated and doesn't cover the full detail of the initialization steps in Section 6.1, nor the step-by-step policy mapping algorithm. There's also no data here about what browsers, TLS libraries, or actual electronic signature engines do in practice: the RFC describes the standard, not the behavior of real software, and there's no evidence in this document about how many implementations do or don't activate each optional layer.&lt;/p&gt;

&lt;p&gt;The text also doesn't mention OCSP or other modern revocation mechanisms — everything said here about "the algorithm not being mandatory" applies specifically to CRL, Section 6.3, and can't be extended without additional evidence to other revocation protocols.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Original source:&lt;/strong&gt; &lt;a href="https://www.rfc-editor.org/rfc/rfc5280" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc5280&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/certificate-policies-path-validation-crl-rfc5280" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>pki</category>
      <category>firmadigital</category>
      <category>seguridadbackend</category>
    </item>
    <item>
      <title>Certificate Policies, path validation y CRL: tres capas que RFC 5280 no obliga a unir</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Fri, 18 Sep 2026 14:30:12 +0000</pubDate>
      <link>https://dev.to/jtorchia/certificate-policies-path-validation-y-crl-tres-capas-que-rfc-5280-no-obliga-a-unir-334</link>
      <guid>https://dev.to/jtorchia/certificate-policies-path-validation-y-crl-tres-capas-que-rfc-5280-no-obliga-a-unir-334</guid>
      <description>&lt;p&gt;Dos implementaciones que dicen "cumplimos RFC 5280" pueden llegar a resultados distintos frente a la misma ruta de certificación, y ninguna de las dos está incumpliendo el estándar. Esto pasa porque el RFC define tres mecanismos separados —perfil del certificado, algoritmo de validación de ruta y verificación de CRL— y solo uno de ellos tiene piso obligatorio estricto. Los otros dos dejan margen explícito de MAY.&lt;/p&gt;

&lt;p&gt;Esta nota separa esas tres capas usando el texto del RFC, sin asumir cómo las implementa cada librería o verificador real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capa 1: Certificate Policies es un dato en el certificado, no un proceso
&lt;/h2&gt;

&lt;p&gt;La sección 4.2.1.4 de RFC 5280 define la extensión Certificate Policies dentro del perfil del certificado X.509v3, en el mismo bloque donde están Policy Mappings (4.2.1.5) y Policy Constraints (4.2.1.11). Es información que viaja en el certificado: un conjunto de OIDs que declaran bajo qué política se emitió.&lt;/p&gt;

&lt;p&gt;Esa extensión, por sí sola, no valida nada. Es un dato disponible para que otro proceso lo use o lo ignore. El propio índice del documento muestra la separación estructural:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4.2.1.4. Certificate Policies ......................32
4.2.1.5. Policy Mappings ...........................35
4.2.1.11. Policy Constraints .......................43
...
6.1. Basic Path Validation .....................72
6.3. CRL Validation .............................90
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El perfil de certificado (sección 4) y la validación de ruta (sección 6) son secciones distintas del RFC. Que un certificado tenga Certificate Policies no implica que el verificador vaya a leerlas ni a exigirlas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capa 2: el algoritmo de path validation define un mínimo, no un máximo
&lt;/h2&gt;

&lt;p&gt;La sección 6.1 describe el algoritmo básico de validación de ruta de certificación. El RFC es explícito sobre su alcance:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Thus, the path validation algorithm presented in Section 6.1 defines the minimum conditions for a path to be considered valid."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Es un piso. Cualquier implementación puede ampliar ese piso, y el RFC lo dice con la misma palabra que usa para marcar lo opcional: MAY.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"An implementation MAY augment the algorithm presented in Section 6.1 to further limit the set of valid certification paths that begin with a particular trust anchor."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Los ejemplos que da el propio texto: aplicar una restricción de longitud de ruta a un trust anchor específico, exigir una forma particular de nombre alternativo en el certificado destino, o imponer requisitos sobre extensiones específicas de la aplicación. Y hay un caso puntual que conecta directo con la capa 1: un trust anchor puede limitarse a confiar en rutas asociadas a una política de certificado particular.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"...a trusted CA may only be trusted for a particular certificate policy. This restriction can be expressed through the inputs to the path validation procedure."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Es decir: las Certificate Policies de la capa 1 solo importan si el algoritmo de la capa 2 las recibe como input y decide usarlas. Una implementación puede correr el algoritmo básico de la sección 6.1, ignorar por completo esos inputs adicionales, y seguir siendo conforme al RFC. El texto es igual de claro sobre trust anchors autofirmados: pueden llevar una extensión de policy constraints indicando que las rutas que arrancan ahí solo deben confiarse para políticas específicas, pero el algoritmo básico "no asume que esa información esté presente" y "no especifica reglas de procesamiento" para ella. Procesarla o ignorarla queda a discreción de cada implementación.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capa 3: CRL checking es, literalmente, opcional de implementar como algoritmo
&lt;/h2&gt;

&lt;p&gt;La sección 6.3 describe cómo determinar si un certificado está revocado cuando el mecanismo de revocación es una CRL. Acá el RFC no dice MAY, dice algo más fuerte todavía:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Conforming implementations that support CRLs are not required to implement this algorithm, but they MUST be functionally equivalent to the external behavior resulting from this procedure when processing CRLs that are issued in conformance with this profile."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No hace falta implementar el algoritmo detallado paso a paso. Alcanza con producir el mismo resultado externo. Esto es una libertad de implementación amplia: dos verificadores pueden llegar a la misma conclusión de revocado/no-revocado por caminos internos completamente distintos, y ambos son conformes.&lt;/p&gt;

&lt;p&gt;Hay además un detalle operativo que el RFC marca y que una implementación básica puede pasar por alto: cuando la CA firma certificados y CRLs con claves privadas distintas, verificar la revocación exige construir y validar una segunda ruta de certificación completa, separada, para el certificado que firma la CRL.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"CRL checking in turn requires a separate certification path to be constructed and validated for the CA's CRL signature validation certificate."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;El RFC incluso ajusta el nivel de obligatoriedad según el escenario:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Applications that perform CRL checking MUST support certification path validation when certificates and CRLs are digitally signed with the same CA private key. These applications SHOULD support certification path validation when certificates and CRLs are digitally signed with different CA private keys."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Con la misma clave, es MUST. Con claves separadas, baja a SHOULD. Una implementación que solo cubre el caso de clave compartida no está violando el RFC: está cubriendo el MUST y dejando afuera el SHOULD.&lt;/p&gt;

&lt;h2&gt;
  
  
  Las tres capas en conjunto
&lt;/h2&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  A[Certificado X.509v3] --&amp;gt;|contiene| B[Certificate Policies OIDs&amp;lt;br/&amp;gt;seccion 4.2.1.4]
  A --&amp;gt; C[Algoritmo de path validation&amp;lt;br/&amp;gt;seccion 6.1 - piso minimo]
  B -.-&amp;gt;|input opcional MAY| C
  C --&amp;gt; D{Verificador soporta CRL?}
  D --&amp;gt;|si| E[CRL Validation&amp;lt;br/&amp;gt;seccion 6.3 - algoritmo no obligatorio]
  D --&amp;gt;|no| F[Ruta valida sin chequeo de revocacion]
  E --&amp;gt; G{Misma clave CA/CRL?}
  G --&amp;gt;|si, MUST| H[Path validation exigido]
  G --&amp;gt;|no, SHOULD| I[Path validation recomendado, no obligatorio]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;El diagrama muestra dónde está el margen: la flecha punteada entre Certificate Policies y el algoritmo de la sección 6.1 es un input opcional, no un enlace automático. Un certificado puede declarar una política y esa declaración nunca llegar a influir en el resultado de la validación, porque depende de que el verificador la pida como input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué esto importa para interoperabilidad
&lt;/h2&gt;

&lt;p&gt;Dos verificadores pueden aceptar como válida exactamente la misma ruta de certificación, aunque uno de ellos ignore las Certificate Policies del certificado y no chequee revocación, mientras el otro aplique restricciones de política adicionales y CRL checking completo con path validation para la clave de firma de CRL. Ninguno de los dos incumple RFC 5280: el primero cubrió el mínimo de la sección 6.1; el segundo, MAY adicionales que el RFC permite pero no exige.&lt;/p&gt;

&lt;p&gt;Esto es una lectura editorial, no un hallazgo textual del RFC: el documento no habla de interoperabilidad legal ni de firma digital con validez jurídica. Pero la estructura normativa —piso obligatorio angosto, extensiones opcionales anchas— explica por qué "conforme a RFC 5280" es una condición necesaria y no suficiente para que dos sistemas de verificación lleguen al mismo veredicto sobre una firma.&lt;/p&gt;

&lt;h2&gt;
  
  
  Límites de esta lectura
&lt;/h2&gt;

&lt;p&gt;El fragmento de RFC 5280 disponible para esta nota está truncado y no cubre el detalle completo de los pasos de inicialización de la sección 6.1 ni el algoritmo de policy mapping paso a paso. Tampoco hay acá datos sobre qué hacen en la práctica navegadores, librerías TLS o motores de firma electrónica concretos: el RFC describe el estándar, no el comportamiento de software real, y no hay evidencia en este documento sobre cuántas implementaciones activan o no cada capa opcional.&lt;/p&gt;

&lt;p&gt;El texto tampoco menciona OCSP ni otros mecanismos de revocación modernos — todo lo dicho sobre "no obligatoriedad del algoritmo" aplica específicamente a CRL, sección 6.3, y no se puede extender sin evidencia adicional a otros protocolos de revocación.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fuente original:&lt;/strong&gt; &lt;a href="https://www.rfc-editor.org/rfc/rfc5280" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc5280&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/certificate-policies-path-validation-crl-rfc5280" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>pki</category>
      <category>firmadigital</category>
    </item>
    <item>
      <title>Ollama v0.34.2 vs v0.34.1: a targeted MLX fix, not the same regression</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Fri, 18 Sep 2026 12:00:19 +0000</pubDate>
      <link>https://dev.to/jtorchia/ollama-v0342-vs-v0341-a-targeted-mlx-fix-not-the-same-regression-mkh</link>
      <guid>https://dev.to/jtorchia/ollama-v0342-vs-v0341-a-targeted-mlx-fix-not-the-same-regression-mkh</guid>
      <description>&lt;p&gt;Ollama published two releases back to back: v0.34.1 on September 14 and v0.34.2 on September 15. Both changelogs touch MLX and both mention memory. That invites reading them as an emergency fix for the same issue. The official text doesn't say that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in each version
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;v0.34.1&lt;/strong&gt; (September 14), per its official changelog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ollama create&lt;/code&gt; with MLX safetensors is no longer experimental. Creating GGUF models now requires llama.cpp tools for conversion and quantization.&lt;/li&gt;
&lt;li&gt;General improvement to MLX memory handling on Apple Silicon ("Improved MLX memory handling on Apple Silicon").&lt;/li&gt;
&lt;li&gt;Token repetition detection now requires 100 repeated tokens before triggering, to reduce false positives in cases like OCR.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/tags&lt;/code&gt; reports a measured improvement on large model libraries: from 3.1 seconds down to 294 ms cold, according to the tests included in the changelog itself.&lt;/li&gt;
&lt;li&gt;Deprecation of &lt;code&gt;typical_p&lt;/code&gt;: it can't be set on new models, but existing GGUF models keep it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;v0.34.2&lt;/strong&gt; (September 15), one day later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First-run setup when running &lt;code&gt;ollama&lt;/code&gt;, with the option to sign in or continue locally. That state is shared with the desktop app on macOS and Windows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ollama://apps&lt;/code&gt; link to open the Apps page of the desktop app directly on macOS and Windows.&lt;/li&gt;
&lt;li&gt;Specific fix: "Fixed excessive memory growth during long generations with MLX speculative decoding".&lt;/li&gt;
&lt;li&gt;llama.cpp update (no version detail given in the changelog).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why these aren't the same fix
&lt;/h2&gt;

&lt;p&gt;The friction point is the shared wording: "memory" and "MLX" show up in both entries. But the text of each changelog describes different mechanisms.&lt;/p&gt;

&lt;p&gt;v0.34.1 talks about "Improved MLX memory handling" in general terms, without specifying under what condition the problem occurred or which part of the pipeline it touched.&lt;/p&gt;

&lt;p&gt;v0.34.2 points to a precise case: memory growth during long generations when using MLX speculative decoding. That's a specific inference mechanism, not the general memory handling the previous version mentioned.&lt;/p&gt;

&lt;p&gt;Neither changelog says one fixes what the other broke. There's no text linking the two entries as the same regression, and there's no confirmation either that they're fully independent. The documents simply don't clarify it.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  A[v0.34.1 - Sep 14] --&amp;gt; B[Improved MLX memory handling, general]
  A --&amp;gt; C[MLX safetensors no longer experimental]
  D[v0.34.2 - Sep 15] --&amp;gt; E[Fix: memory in MLX speculative decoding, long generations]
  D --&amp;gt; F[First-run setup + ollama apps]
  B -.- G{No explicit textual link}
  E -.- G&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Who the v0.34.2 fix matters to
&lt;/h2&gt;

&lt;p&gt;The memory fix in v0.34.2 is scoped to one scenario: MLX speculative decoding on long generations. That runs on Apple Silicon with the MLX backend.&lt;/p&gt;

&lt;p&gt;If the use case is GGUF models on llama.cpp, that fix doesn't apply according to what the changelog says: the described problem is specific to MLX speculative decoding, not the GGUF backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can't be concluded from this evidence
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;There's no confirmation of whether the memory growth fixed in v0.34.2 was introduced by the v0.34.1 changes or already existed before that.&lt;/li&gt;
&lt;li&gt;The internal mechanism isn't explained: the changelog says it was "fixed," not why memory grew during speculative decoding.&lt;/li&gt;
&lt;li&gt;There's no detail on which llama.cpp version was bumped in v0.34.2 or what that bump brings.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;/api/tags&lt;/code&gt; numbers (3.1 s → 294 ms) come from the project's own changelog, with no described test methodology.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who needs to update today
&lt;/h2&gt;

&lt;p&gt;Based on the available information: those running MLX models on Apple Silicon with long generations and using speculative decoding have a concrete, documented fix in v0.34.2. For the rest of the changes — stable safetensors, the repetition threshold, &lt;code&gt;typical_p&lt;/code&gt; deprecation, the &lt;code&gt;/api/tags&lt;/code&gt; improvement — those were already in v0.34.1, and updating to v0.34.2 includes them by inheritance, not because v0.34.2 repeats or fixes them.&lt;/p&gt;

&lt;p&gt;Original source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ollama/ollama/releases/tag/v0.34.2" rel="noopener noreferrer"&gt;https://github.com/ollama/ollama/releases/tag/v0.34.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ollama/ollama/releases/tag/v0.34.1" rel="noopener noreferrer"&gt;https://github.com/ollama/ollama/releases/tag/v0.34.1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/ollama-v0-34-2-vs-v0-34-1-mlx-fix" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>ollama</category>
      <category>llmlocal</category>
      <category>applesilicon</category>
    </item>
    <item>
      <title>Ollama v0.34.2 vs v0.34.1: un fix puntual de MLX, no la misma regresión</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Fri, 18 Sep 2026 12:00:15 +0000</pubDate>
      <link>https://dev.to/jtorchia/ollama-v0342-vs-v0341-un-fix-puntual-de-mlx-no-la-misma-regresion-514b</link>
      <guid>https://dev.to/jtorchia/ollama-v0342-vs-v0341-un-fix-puntual-de-mlx-no-la-misma-regresion-514b</guid>
      <description>&lt;p&gt;Ollama publicó dos releases seguidos: v0.34.1 el 14 de septiembre y v0.34.2 el 15 de septiembre. Los dos changelogs tocan MLX y los dos mencionan memoria. Eso invita a leerlos como una corrección de emergencia sobre lo mismo. Los textos oficiales no dicen eso.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué cambió en cada versión
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;v0.34.1&lt;/strong&gt; (14 de septiembre), según su changelog oficial:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ollama create&lt;/code&gt; con MLX safetensors deja de ser experimental. La creación de modelos GGUF pasa a requerir herramientas de llama.cpp para la conversión y cuantización.&lt;/li&gt;
&lt;li&gt;Mejora general de manejo de memoria MLX en Apple Silicon ("Improved MLX memory handling on Apple Silicon").&lt;/li&gt;
&lt;li&gt;La detección de repetición de tokens ahora exige 100 tokens repetidos antes de activarse, para reducir falsos positivos en casos como OCR.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/tags&lt;/code&gt; reporta una mejora medida en librerías grandes de modelos: de 3.1 segundos a 294 ms en frío, según las pruebas incluidas en el propio changelog.&lt;/li&gt;
&lt;li&gt;Deprecación de &lt;code&gt;typical_p&lt;/code&gt;: no se puede setear en modelos nuevos, pero los modelos GGUF existentes lo conservan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;v0.34.2&lt;/strong&gt; (15 de septiembre), un día después:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup de primer uso al correr &lt;code&gt;ollama&lt;/code&gt;, con opción de iniciar sesión o seguir localmente. Ese estado se comparte con la app de escritorio en macOS y Windows.&lt;/li&gt;
&lt;li&gt;Enlace &lt;code&gt;ollama://apps&lt;/code&gt; para abrir directamente la página de Apps de la app de escritorio en macOS y Windows.&lt;/li&gt;
&lt;li&gt;Fix específico: "Fixed excessive memory growth during long generations with MLX speculative decoding".&lt;/li&gt;
&lt;li&gt;Actualización de llama.cpp (sin detalle de versión en el changelog).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Por qué no son el mismo fix
&lt;/h2&gt;

&lt;p&gt;El punto de fricción está en la palabra compartida: "memoria" y "MLX" aparecen en las dos entradas. Pero el texto de cada changelog describe mecanismos distintos.&lt;/p&gt;

&lt;p&gt;v0.34.1 habla de "Improved MLX memory handling" en términos generales, sin especificar bajo qué condición ocurría el problema ni qué parte del pipeline tocaba.&lt;/p&gt;

&lt;p&gt;v0.34.2 apunta a un caso preciso: crecimiento de memoria durante generaciones largas cuando se usa decodificación especulativa MLX (MLX speculative decoding). Es un mecanismo de inferencia específico, no el manejo de memoria general que mencionó la versión anterior.&lt;/p&gt;

&lt;p&gt;Ningún changelog dice que uno corrija lo que dejó roto el otro. No hay texto que vincule las dos entradas como la misma regresión, y tampoco hay confirmación de que sean completamente independientes. Los documentos simplemente no lo aclaran.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  A[v0.34.1 - 14 sep] --&amp;gt; B[Manejo de memoria MLX mejorado, general]
  A --&amp;gt; C[MLX safetensors ya no experimental]
  D[v0.34.2 - 15 sep] --&amp;gt; E[Fix: memoria en MLX speculative decoding, generaciones largas]
  D --&amp;gt; F[First-run setup + ollama apps]
  B -.- G{Sin vínculo textual explícito}
  E -.- G&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  A quién le importa el fix de v0.34.2
&lt;/h2&gt;

&lt;p&gt;El fix de memoria de v0.34.2 está acotado a un escenario: decodificación especulativa MLX en generaciones largas. Eso corre en Apple Silicon con el backend MLX.&lt;/p&gt;

&lt;p&gt;Si el uso es con modelos GGUF sobre llama.cpp, ese fix no aplica según lo que dice el changelog: el problema descripto es específico de MLX speculative decoding, no del backend GGUF.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué no se puede concluir con esta evidencia
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No hay confirmación de si el crecimiento de memoria que corrige v0.34.2 fue introducido por los cambios de v0.34.1 o si ya existía antes.&lt;/li&gt;
&lt;li&gt;No se explica el mecanismo interno: el changelog dice que se "fixed", no por qué crecía la memoria en decodificación especulativa.&lt;/li&gt;
&lt;li&gt;No hay detalle de qué versión de llama.cpp se actualizó en v0.34.2 ni qué trae ese bump.&lt;/li&gt;
&lt;li&gt;Los números de &lt;code&gt;/api/tags&lt;/code&gt; (3.1 s → 294 ms) vienen del propio changelog del proyecto, sin metodología de prueba descripta.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quién necesita actualizar hoy
&lt;/h2&gt;

&lt;p&gt;Con la información disponible: quienes corren modelos MLX en Apple Silicon con generaciones largas y usan decodificación especulativa tienen un fix concreto y documentado en v0.34.2. Para el resto de los cambios —safetensors estables, umbral de repetición, deprecación de &lt;code&gt;typical_p&lt;/code&gt;, mejora de &lt;code&gt;/api/tags&lt;/code&gt;— ya estaban en v0.34.1, y actualizar a v0.34.2 los incluye por arrastre, no porque v0.34.2 los repita o los corrija.&lt;/p&gt;

&lt;p&gt;Fuente original:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ollama/ollama/releases/tag/v0.34.2" rel="noopener noreferrer"&gt;https://github.com/ollama/ollama/releases/tag/v0.34.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ollama/ollama/releases/tag/v0.34.1" rel="noopener noreferrer"&gt;https://github.com/ollama/ollama/releases/tag/v0.34.1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/ollama-v0-34-2-vs-v0-34-1-fix-mlx" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>ollama</category>
      <category>llmlocal</category>
    </item>
    <item>
      <title>CUDA Rust: two native tracks, not a wrapper over C++</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Thu, 17 Sep 2026 12:00:19 +0000</pubDate>
      <link>https://dev.to/jtorchia/cuda-rust-two-native-tracks-not-a-wrapper-over-c-35kb</link>
      <guid>https://dev.to/jtorchia/cuda-rust-two-native-tracks-not-a-wrapper-over-c-35kb</guid>
      <description>&lt;p&gt;Nvidia published an announcement with an ambitious name on its developer blog on September 8, 2026: CUDA Rust. The question I'm interested in isn't whether Rust on GPU sounds good — it does, it always does — but what's actually underneath the announcement. Is it a native frontend that compiles to PTX, as the text claims, or does it end up being a layer over the usual CUDA C++ toolchain? And what about wgpu, rust-gpu and rust-cuda, which were already solving part of this problem?&lt;/p&gt;

&lt;h2&gt;
  
  
  Hypothesis, scope and status
&lt;/h2&gt;

&lt;p&gt;The hypothesis of this post is specific: &lt;strong&gt;"native" in Nvidia's blog means the kernel compiles from Rust down to PTX without going through CUDA C++ as an intermediary, but that doesn't imply the two announced projects replace the existing Rust-GPU ecosystem, nor that they're production-ready.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Status of this note: it's a reading of the primary source, not my own bench test. I didn't run the examples. What follows separates what the blog claims from what still needs verifying with code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the primary source says
&lt;/h2&gt;

&lt;p&gt;According to &lt;a href="https://developer.nvidia.com/blog/introducing-cuda-rust-two-tracks-for-writing-gpu-kernels" rel="noopener noreferrer"&gt;Nvidia's official blog&lt;/a&gt;, published September 8, 2026, there are two distinct projects, not one:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;cuda-oxide&lt;/strong&gt; (SIMT track). It's a custom codegen backend for &lt;code&gt;rustc&lt;/code&gt;. It intercepts compilation, routes functions marked with &lt;code&gt;#[kernel]&lt;/code&gt; through Rust MIR, the Pliron community IR framework, and from there to LLVM IR down to PTX. The rest of the program is handled by the standard Rust backend. It requires Linux, a GPU with compute capability 8.0+, CUDA toolkit 12.x or higher, clang with libclang headers, and a &lt;em&gt;pinned&lt;/em&gt; nightly toolchain (&lt;code&gt;nightly-2026-04-03&lt;/code&gt; in the blog's example).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;cutile-rs&lt;/strong&gt; (Tile track). This one works a level higher: instead of programming per thread, you program per tile. The &lt;code&gt;#[cutile::module]&lt;/code&gt; macro embeds the kernel's AST in the host binary and JIT-compiles it via CUDA Tile IR when the kernel is first needed. It runs on stable Rust 1.89+, with CUDA 13.3, no nightly and no custom LLVM. It's already published on crates.io, and the blog mentions it's used outside Nvidia in HuggingFace's Grout inference engine and in mistral.rs.&lt;/p&gt;

&lt;p&gt;The sentence that answers the first part of the reader's question is right there in the text: &lt;em&gt;"GPU kernels can be written in Rust, compiled natively to PTX, rather than a wrapper around code from somewhere else."&lt;/em&gt; That's a claim from the source, not a measurement of mine. Given that, the stated mechanism is native compilation to PTX via MIR/Pliron/LLVM (for cuda-oxide) or via CUDA Tile IR (for cutile-rs), not a bindings layer over already-compiled CUDA C++ binaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the blog doesn't settle about replacement vs. complement
&lt;/h2&gt;

&lt;p&gt;Here's the real limit of the evidence. The blog is explicit that it does &lt;strong&gt;not&lt;/strong&gt; present itself as a replacement:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Rust on GPUs is not new. There is good work in this space that predates ours and continues alongside it. [...] we have been working with the rust-cuda maintainers as both projects mature."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And it also names the goal of cross-language coexistence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"NVIDIA plans to support inter-language interoperability between CUDA Rust, CUDA C++, and CUDA Python so the choice of frontend does not lock developers out of other ecosystems."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two things remain unresolved with this single source:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;How mature the actual interoperability is.&lt;/strong&gt; The blog says "plans to support," future tense. There's no code example showing that interop working today. Treating it as available would be inflating the claim.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical relationship with wgpu.&lt;/strong&gt; The text compares cuda-oxide and cutile-rs against Rust-GPU, rust-cuda and CubeCL (via an appendix in the cuda-oxide book that I didn't read), but doesn't mention wgpu in the available fragment. wgpu targets multi-backend portability (Vulkan, Metal, DX12, WebGPU); CUDA Rust targets Nvidia's stack purely. These are different goals, so "replaces" probably doesn't apply there — but this is my own inference from what I know about wgpu, not something the source explicitly confirms.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Environment and commands to verify the hypothesis (protocol, not execution)
&lt;/h2&gt;

&lt;p&gt;This is a verification plan, not a result. I didn't run these commands.&lt;/p&gt;

&lt;p&gt;For cuda-oxide (SIMT track), the blog documents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Requires Linux, GPU compute capability 8.0+, CUDA toolkit 12.x+,&lt;/span&gt;
&lt;span class="c"&gt;# clang with libclang, pinned nightly toolchain&lt;/span&gt;
cargo +nightly-2026-04-03 &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--git&lt;/span&gt; https://github.com/NVlabs/cuda-oxide.git cargo-oxide

cargo oxide new vecadd_demo
&lt;span class="nb"&gt;cd &lt;/span&gt;vecadd_demo
cargo oxide doctor
cargo oxide run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example program does vector addition (1024 floats) and verifies the result with &lt;code&gt;PASSED: all 1024 elements correct&lt;/code&gt;. The technical detail that marks the difference from a wrapper: the &lt;code&gt;DisjointSlice&amp;lt;f32&amp;gt;&lt;/code&gt; type replaces &lt;code&gt;&amp;amp;mut [f32]&lt;/code&gt; for the output buffer, because Rust's borrow checker doesn't allow thousands of threads to share the same &lt;code&gt;&amp;amp;mut&lt;/code&gt;. &lt;code&gt;DisjointSlice&lt;/code&gt; splits that single borrow into exclusive per-thread fragments, checked at compile time. That's the kind of guarantee a thin wrapper over CUDA C++ couldn't provide, because the Rust compiler needs to see the type to reason about aliasing.&lt;/p&gt;

&lt;p&gt;For cutile-rs (Tile track), the blog documents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Requires GPU compute capability 8.0+, CUDA 13.3, stable Rust 1.89+, Linux&lt;/span&gt;
&lt;span class="c"&gt;# No nightly, no custom LLVM&lt;/span&gt;
cargo new vecadd_demo
&lt;span class="nb"&gt;cd &lt;/span&gt;vecadd_demo
cargo add cutile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the &lt;code&gt;hello_world&lt;/code&gt; example from the cutile-rs repo running via &lt;code&gt;cargo run -p cutile-examples --example hello_world&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Criterion for accepting or rejecting the hypothesis: if someone runs both examples and confirms that the final binary doesn't depend on &lt;code&gt;nvcc&lt;/code&gt; or a CUDA C++ compiler anywhere in the kernel's compilation path, the "native, not wrapper" hypothesis is empirically supported. If a hidden C++ dependency shows up in the build chain, the hypothesis falls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits of this note
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I didn't run &lt;code&gt;cargo oxide run&lt;/code&gt; or the cutile-rs example. Everything I describe about compiler behavior comes from the blog text, not from a run of my own.&lt;/li&gt;
&lt;li&gt;The fragment of the source I have is truncated at several points (marked &lt;code&gt;[fragment]&lt;/code&gt; in the original), so there may be nuances about cross-language interoperability or about the comparison appendix with Rust-GPU/CubeCL that I didn't get to read in full.&lt;/li&gt;
&lt;li&gt;Both projects explicitly declare themselves unfit for production: cuda-oxide is in early alpha, cutile-rs is "further along" but with incomplete coverage and APIs that will still move, according to the blog's own words.&lt;/li&gt;
&lt;li&gt;There's no performance benchmark in the source or in this note. Any speed comparison between these tracks and traditional CUDA C++ would need a separate experiment, with fixed hardware and versions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where this stands
&lt;/h2&gt;

&lt;p&gt;With the evidence I have, I don't take it for granted that "native compilation to PTX" equals "ready to replace CUDA C++ in a real system." The blog is careful not to promise that — it talks about early-stage projects, with heavy requirements (pinned nightly, system LLVM) on the track that most resembles traditional CUDA C++. If I had to decide today whether to adopt this in an inference system, I wouldn't do it without first running the two official examples and comparing the build pipeline against what rust-cuda or CubeCL already offer — both of which have been in the ecosystem longer and don't depend on a freshly published compiler backend preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Original source
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Nvidia Developer Blog — Introducing CUDA Rust: Two Tracks for Writing GPU Kernels: &lt;a href="https://developer.nvidia.com/blog/introducing-cuda-rust-two-tracks-for-writing-gpu-kernels" rel="noopener noreferrer"&gt;https://developer.nvidia.com/blog/introducing-cuda-rust-two-tracks-for-writing-gpu-kernels&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/cuda-rust-two-native-tracks-not-a-cpp-wrapper" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>rust</category>
      <category>cuda</category>
      <category>gpuprogramming</category>
    </item>
    <item>
      <title>CUDA Rust: dos tracks nativos, no un wrapper sobre C++</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Thu, 17 Sep 2026 12:00:14 +0000</pubDate>
      <link>https://dev.to/jtorchia/cuda-rust-dos-tracks-nativos-no-un-wrapper-sobre-c-d8n</link>
      <guid>https://dev.to/jtorchia/cuda-rust-dos-tracks-nativos-no-un-wrapper-sobre-c-d8n</guid>
      <description>&lt;p&gt;Nvidia publicó el 8 de septiembre de 2026 en su blog de desarrolladores un anuncio con nombre ambicioso: CUDA Rust. La pregunta que me interesa no es si Rust en GPU suena bien —suena bien, siempre suena bien— sino qué hay debajo del anuncio. ¿Es un frontend nativo que compila a PTX, como dice el texto, o termina siendo una capa sobre el toolchain de CUDA C++ de siempre? ¿Y qué pasa con wgpu, rust-gpu y rust-cuda, que ya venían resolviendo parte de este problema?&lt;/p&gt;

&lt;h2&gt;
  
  
  Hipótesis, alcance y estado
&lt;/h2&gt;

&lt;p&gt;La hipótesis de este post es específica: &lt;strong&gt;"nativo" en el blog de Nvidia significa que el kernel se compila desde Rust hasta PTX sin pasar por CUDA C++ como intermediario, pero eso no implica que los dos proyectos anunciados reemplacen el ecosistema Rust-GPU existente ni que estén listos para producción.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Estado de esta nota: es una lectura de la fuente primaria, no una prueba de banco propia. No corrí los ejemplos. Lo que sigue separa lo que el blog afirma de lo que falta verificar con código.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué dice la fuente primaria
&lt;/h2&gt;

&lt;p&gt;Según el &lt;a href="https://developer.nvidia.com/blog/introducing-cuda-rust-two-tracks-for-writing-gpu-kernels" rel="noopener noreferrer"&gt;blog oficial de Nvidia&lt;/a&gt;, publicado el 8 de septiembre de 2026, hay dos proyectos distintos, no uno:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;cuda-oxide&lt;/strong&gt; (track SIMT). Es un codegen backend custom para &lt;code&gt;rustc&lt;/code&gt;. Intercepta la compilación, enruta las funciones marcadas con &lt;code&gt;#[kernel]&lt;/code&gt; a través de Rust MIR, el framework de IR de la comunidad Pliron, y de ahí a LLVM IR hasta PTX. El resto del programa lo maneja el backend estándar de Rust. Requiere Linux, GPU con compute capability 8.0+, CUDA toolkit 12.x o superior, clang con headers de libclang, y un toolchain nightly &lt;em&gt;pinned&lt;/em&gt; (&lt;code&gt;nightly-2026-04-03&lt;/code&gt; en el ejemplo del blog).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;cutile-rs&lt;/strong&gt; (track Tile). Trabaja un nivel más arriba: en vez de programar por thread, programás por tile. El macro &lt;code&gt;#[cutile::module]&lt;/code&gt; embebe el AST del kernel en el binario del host y lo compila JIT vía CUDA Tile IR cuando el kernel se necesita por primera vez. Corre en Rust estable 1.89+, con CUDA 13.3, sin nightly y sin LLVM propio. Ya está publicado en crates.io y el blog menciona que se usa fuera de Nvidia en el motor de inferencia Grout de HuggingFace y en mistral.rs.&lt;/p&gt;

&lt;p&gt;La frase que responde la primera parte de la pregunta del lector está en el propio texto: &lt;em&gt;"GPU kernels can be written in Rust, compiled natively to PTX, rather than a wrapper around code from somewhere else."&lt;/em&gt; Es una afirmación de la fuente, no una medición mía. Con eso, el mecanismo declarado es compilación nativa a PTX vía MIR/Pliron/LLVM (para cuda-oxide) o vía CUDA Tile IR (para cutile-rs), no una capa de bindings sobre binarios de CUDA C++ ya compilados.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que el blog no resuelve sobre reemplazo vs. complemento
&lt;/h2&gt;

&lt;p&gt;Acá está el límite real de la evidencia. El blog es explícito en que &lt;strong&gt;no&lt;/strong&gt; se presenta como reemplazo:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Rust on GPUs is not new. There is good work in this space that predates ours and continues alongside it. [...] we have been working with the rust-cuda maintainers as both projects mature."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Y también nombra el objetivo de convivencia entre lenguajes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"NVIDIA plans to support inter-language interoperability between CUDA Rust, CUDA C++, and CUDA Python so the choice of frontend does not lock developers out of other ecosystems."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Dos cosas quedan sin resolver con esta única fuente:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Estado de madurez real de la interoperabilidad.&lt;/strong&gt; El blog dice "plans to support", tiempo futuro. No hay ejemplo de código mostrando esa interop funcionando hoy. Tratarlo como disponible sería inflar el claim.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relación técnica con wgpu.&lt;/strong&gt; El texto compara cuda-oxide y cutile-rs contra Rust-GPU, rust-cuda y CubeCL (via un apéndice del libro de cuda-oxide que no leí), pero no menciona wgpu en el fragmento disponible. wgpu apunta a portabilidad multi-backend (Vulkan, Metal, DX12, WebGPU); CUDA Rust apunta puramente al stack de Nvidia. Son objetivos distintos, así que "reemplaza" probablemente no aplica ahí — pero esto es una inferencia mía a partir de lo que sé de wgpu, no algo que la fuente confirme explícitamente.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Entorno y comandos para verificar la hipótesis (protocolo, no ejecución)
&lt;/h2&gt;

&lt;p&gt;Esto es un plan de verificación, no un resultado. No corrí estos comandos.&lt;/p&gt;

&lt;p&gt;Para cuda-oxide (track SIMT), el blog documenta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Requiere Linux, GPU compute capability 8.0+, CUDA toolkit 12.x+,&lt;/span&gt;
&lt;span class="c"&gt;# clang con libclang, toolchain nightly pinned&lt;/span&gt;
cargo +nightly-2026-04-03 &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--git&lt;/span&gt; https://github.com/NVlabs/cuda-oxide.git cargo-oxide

cargo oxide new vecadd_demo
&lt;span class="nb"&gt;cd &lt;/span&gt;vecadd_demo
cargo oxide doctor
cargo oxide run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El programa de ejemplo hace suma de vectores (1024 floats) y verifica el resultado con &lt;code&gt;PASSED: all 1024 elements correct&lt;/code&gt;. El detalle técnico que marca la diferencia frente a un wrapper: el tipo &lt;code&gt;DisjointSlice&amp;lt;f32&amp;gt;&lt;/code&gt; reemplaza &lt;code&gt;&amp;amp;mut [f32]&lt;/code&gt; para el buffer de salida, porque el borrow checker de Rust no permite que miles de threads compartan el mismo &lt;code&gt;&amp;amp;mut&lt;/code&gt;. &lt;code&gt;DisjointSlice&lt;/code&gt; divide ese borrow único en fragmentos exclusivos por thread, verificados en compile time. Esa es la clase de garantía que un wrapper delgado sobre CUDA C++ no podría dar, porque el compilador de Rust necesita ver el tipo para razonar sobre aliasing.&lt;/p&gt;

&lt;p&gt;Para cutile-rs (track Tile), el blog documenta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Requiere GPU compute capability 8.0+, CUDA 13.3, Rust estable 1.89+, Linux&lt;/span&gt;
&lt;span class="c"&gt;# Sin nightly, sin LLVM propio&lt;/span&gt;
cargo new vecadd_demo
&lt;span class="nb"&gt;cd &lt;/span&gt;vecadd_demo
cargo add cutile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con el ejemplo &lt;code&gt;hello_world&lt;/code&gt; del repo cutile-rs corriendo vía &lt;code&gt;cargo run -p cutile-examples --example hello_world&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Criterio para aceptar o descartar la hipótesis: si alguien corre ambos ejemplos y confirma que el binario final no depende de &lt;code&gt;nvcc&lt;/code&gt; ni de un compilador de CUDA C++ en el camino de compilación del kernel, la hipótesis de "nativo, no wrapper" queda soportada empíricamente. Si aparece una dependencia oculta de C++ en la cadena de build, la hipótesis cae.&lt;/p&gt;

&lt;h2&gt;
  
  
  Límites de esta nota
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No ejecuté &lt;code&gt;cargo oxide run&lt;/code&gt; ni el ejemplo de cutile-rs. Todo lo que describo sobre comportamiento del compilador viene del texto del blog, no de una corrida propia.&lt;/li&gt;
&lt;li&gt;El fragmento de la fuente que tengo está truncado en varios puntos (marcado &lt;code&gt;[fragment]&lt;/code&gt; en el original), así que puede haber matices sobre la interoperabilidad entre lenguajes o sobre el apéndice de comparación con Rust-GPU/CubeCL que no llegué a leer completos.&lt;/li&gt;
&lt;li&gt;Ambos proyectos se declaran explícitamente no aptos para producción: cuda-oxide está en alpha temprana, cutile-rs "está más avanzado" pero con cobertura incompleta y APIs que van a moverse, según palabras del propio blog.&lt;/li&gt;
&lt;li&gt;No hay benchmark de performance en la fuente ni en esta nota. Cualquier comparación de velocidad entre estos tracks y CUDA C++ tradicional necesitaría un experimento aparte, con hardware y versiones fijadas.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Postura
&lt;/h2&gt;

&lt;p&gt;Con la evidencia que tengo, no doy por sentado que "compilación nativa a PTX" equivalga a "listo para reemplazar CUDA C++ en un sistema real". El blog es cuidadoso en no prometer eso — habla de proyectos tempranos, con requisitos pesados (nightly pinned, LLVM del sistema) en el track que más se parece al CUDA C++ tradicional. Si tuviera que decidir hoy si adoptar esto en un sistema de inferencia, no lo haría sin antes correr los dos ejemplos oficiales y comparar el pipeline de build contra lo que ya usa rust-cuda o CubeCL, que llevan más tiempo en el ecosistema y no dependen de una preview de compiler backend recién publicada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fuente original
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Nvidia Developer Blog — Introducing CUDA Rust: Two Tracks for Writing GPU Kernels: &lt;a href="https://developer.nvidia.com/blog/introducing-cuda-rust-two-tracks-for-writing-gpu-kernels" rel="noopener noreferrer"&gt;https://developer.nvidia.com/blog/introducing-cuda-rust-two-tracks-for-writing-gpu-kernels&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/cuda-rust-nvidia-dos-tracks-gpu-kernels" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>rust</category>
      <category>cuda</category>
    </item>
    <item>
      <title>XGBoost Doesn't Beat scikit-learn, It Solves a Different Problem</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Wed, 16 Sep 2026 12:00:19 +0000</pubDate>
      <link>https://dev.to/jtorchia/xgboost-doesnt-beat-scikit-learn-it-solves-a-different-problem-221p</link>
      <guid>https://dev.to/jtorchia/xgboost-doesnt-beat-scikit-learn-it-solves-a-different-problem-221p</guid>
      <description>&lt;p&gt;You open a notebook, train a scikit-learn &lt;code&gt;RandomForestClassifier&lt;/code&gt; on a small tabular dataset, and the result is decent. Someone on the team says "try XGBoost, it'll do better." You swap two lines, run it again, and the number ticks up a bit. The question almost nobody asks at that point is &lt;em&gt;why&lt;/em&gt; it improved — and whether that improvement justifies the cost of having six new hyperparameters to tune instead of two.&lt;/p&gt;

&lt;p&gt;My take is this: XGBoost isn't "better" than scikit-learn in general. It's the right tool when you need to squeeze performance out of tabular data using boosting, at the cost of a bigger configuration surface. If you're not in that specific scenario, you're paying complexity for nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What XGBoost Is (and Isn't)
&lt;/h2&gt;

&lt;p&gt;XGBoost stands for &lt;em&gt;Extreme Gradient Boosting&lt;/em&gt;. It's an implementation of gradient boosting on decision trees, optimized for speed and for squeezing out every bit of signal the dataset has to give. The &lt;a href="https://xgboost.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt; describes it as a gradient boosting library optimized to be "efficient, flexible and portable" — not a new algorithm, but a particular implementation of an idea that already existed.&lt;/p&gt;

&lt;p&gt;The underlying idea, gradient boosting, is simple to explain even though it's not simple to implement well:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Train a weak model (typically a small tree).&lt;/li&gt;
&lt;li&gt;Measure the error that model makes.&lt;/li&gt;
&lt;li&gt;Train a second model focused on correcting that error.&lt;/li&gt;
&lt;li&gt;Repeat, stacking models, each one fixing what the previous one got wrong.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A scikit-learn &lt;code&gt;RandomForestClassifier&lt;/code&gt; does something different: it trains many trees in parallel, each on a different sample of the data, and averages their votes. There's no sequential error correction — there's voting. That design difference is exactly what explains why, on certain tabular datasets, boosting pulls ahead: each new tree is literally trained to patch the hole the previous one left, instead of just contributing another independent point of view.&lt;/p&gt;

&lt;p&gt;What the XGBoost docs &lt;strong&gt;don't&lt;/strong&gt; say is that this will always happen. They document the algorithm, the parameters, the API. They don't document "what percentage of datasets it wins on" — that depends on the dataset, and any generic figure floating around about that has no citable source.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reproducible Example: Same Dataset, Two Libraries
&lt;/h2&gt;

&lt;p&gt;To see the difference without inventing a use case, use the toy dataset &lt;code&gt;load_breast_cancer&lt;/code&gt; that ships with scikit-learn — 569 rows, 30 numeric features, binary classification. It's small on purpose: the point isn't to prove who "wins," it's to show the workflow and where the new hyperparameters show up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# comparacion_basica.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_breast_cancer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;train_test_split&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.ensemble&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RandomForestClassifier&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;xgboost&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;XGBClassifier&lt;/span&gt;

&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_breast_cancer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;return_X_y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;train_test_split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RandomForestClassifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;xgb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;XGBClassifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eval_metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;logloss&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;xgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RandomForest score:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;XGBoost score:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this in a Jupyter notebook and you'll get two accuracy numbers on the same split. I'm not going to invent here what number you'll get — it depends on library versions, the seed, the hardware. What you &lt;em&gt;can&lt;/em&gt; claim from this experiment is something else: &lt;strong&gt;both models ran on the same dataset, with the same split, and one of the two has a smaller API surface to tune.&lt;/strong&gt; &lt;code&gt;RandomForestClassifier&lt;/code&gt; has &lt;code&gt;n_estimators&lt;/code&gt;, &lt;code&gt;max_depth&lt;/code&gt;, &lt;code&gt;min_samples_split&lt;/code&gt; as its core knobs. &lt;code&gt;XGBClassifier&lt;/code&gt; adds &lt;code&gt;learning_rate&lt;/code&gt;, &lt;code&gt;subsample&lt;/code&gt;, &lt;code&gt;colsample_bytree&lt;/code&gt;, &lt;code&gt;gamma&lt;/code&gt;, &lt;code&gt;reg_alpha&lt;/code&gt;, &lt;code&gt;reg_lambda&lt;/code&gt; — and each one interacts with the others.&lt;/p&gt;

&lt;p&gt;That's the real cost of XGBoost. It's not that training is slower (for small datasets, you won't even notice) or harder to install. It's that the hyperparameter space is bigger, and a badly done search in that space can give you a worse result than a RandomForest with defaults.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  A[Tabular dataset] --&amp;gt; B{Do I need to squeeze every point of performance?}
  B --&amp;gt;|No, I want a quick baseline| C[scikit-learn RandomForest]
  B --&amp;gt;|Yes, and I have time to tune| D[XGBoost]
  D --&amp;gt; E{Did you tune learning_rate, subsample, reg_lambda?}
  E --&amp;gt;|No| F[Result probably worse than baseline]
  E --&amp;gt;|Yes, with cross-validation| G[Result potentially better]&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Where People Screw Up: The Copy-Pasted Recipe
&lt;/h2&gt;

&lt;p&gt;The common recipe going around is: "use XGBoost because it wins Kaggle competitions." It's true that XGBoost played a starring role in a lot of tabular data competitions — that's documented history. The problem is the logical leap that follows: it doesn't follow from that that it'll win on &lt;strong&gt;your&lt;/strong&gt; dataset, with &lt;strong&gt;your&lt;/strong&gt; features, at &lt;strong&gt;your&lt;/strong&gt; row count.&lt;/p&gt;

&lt;p&gt;The hidden cost of copying that recipe without thinking is twofold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Silent overfitting.&lt;/strong&gt; XGBoost with lots of estimators and no regularization (&lt;code&gt;reg_alpha&lt;/code&gt;, &lt;code&gt;reg_lambda&lt;/code&gt; at zero) memorizes the training dataset more easily than a RandomForest, because each new tree fits itself specifically to the residual error — including the noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tuning time nobody budgeted for.&lt;/strong&gt; If you're going to use XGBoost seriously, you need a hyperparameter search (grid search, random search, or something like Optuna) with cross-validation. That's compute time and human time that a RandomForest with reasonable defaults doesn't ask of you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The simplest counterexample: a tabular dataset with few rows (say, a few hundred) and high-dimensional noise. There, a RandomForest with its implicit bagging regularization can behave more stably than a badly-tuned XGBoost, which is going to learn to explain the noise instead of the signal. I don't have a public benchmark to cite for this specific case — it's an expected behavior pattern based on how each algorithm works, not a measurement I ran.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision Matrix: When to Look at XGBoost First
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;What to check first&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tabular data, need a quick baseline&lt;/td&gt;
&lt;td&gt;scikit-learn (RandomForest or GradientBoosting)&lt;/td&gt;
&lt;td&gt;Fewer hyperparameters, decent result with no tuning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Competition or problem where every performance point counts&lt;/td&gt;
&lt;td&gt;XGBoost, with a time budget for tuning&lt;/td&gt;
&lt;td&gt;This is where sequential boosting pays off, if paired with cross-validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dataset with lots of unencoded categorical columns&lt;/td&gt;
&lt;td&gt;Check native categorical support in the XGBoost version you're using&lt;/td&gt;
&lt;td&gt;The official docs detail support per version — don't assume "it just works" without checking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tight team time for tuning, short deadline&lt;/td&gt;
&lt;td&gt;scikit-learn first&lt;/td&gt;
&lt;td&gt;A badly-tuned XGBoost can perform worse than a RandomForest with defaults&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need to explain the model to a non-technical person&lt;/td&gt;
&lt;td&gt;Either one, but check interpretability tools (feature importance, SHAP) before choosing on performance alone&lt;/td&gt;
&lt;td&gt;Interpretability doesn't depend on the library, it depends on what you instrument on top&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table isn't a closed conclusion about which algorithm "wins." It's a starting point for deciding what to try first based on your actual constraint: time, data volume, need to explain the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Limits of This (and Why I Won't Close With a Number)
&lt;/h2&gt;

&lt;p&gt;Watch out for the claim I almost wrote before pulling it back: "XGBoost wins in most tabular data cases." That sentence has no citable source I can point to here, and it's exactly the kind of evidence-free claim that got flagged in an earlier post about scikit-learn. I'm not going to repeat it in different words.&lt;/p&gt;

&lt;p&gt;What I &lt;em&gt;can&lt;/em&gt; say at the level of certainty the evidence allows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The official XGBoost documentation explains the algorithm, the parameters, and feature support. It doesn't document performance comparisons against scikit-learn on general datasets — you have to run that yourself, with your data.&lt;/li&gt;
&lt;li&gt;The snippet experiment above is reproducible: anyone running it will get two comparable numbers, but those numbers will vary by library version and seed. It's not a fixed measurement you can cite as "XGBoost scored X% better."&lt;/li&gt;
&lt;li&gt;Without a cross-validation experiment with hyperparameter search on your specific dataset, any accuracy comparison between the two models is anecdotal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need a reproducible, measurable decision, the concrete next step is to run &lt;code&gt;GridSearchCV&lt;/code&gt; or &lt;code&gt;RandomizedSearchCV&lt;/code&gt; on both models, with the same cross-validation scheme, and compare the distribution of scores — not a single number from a single split.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does XGBoost work for non-tabular data, like images or text?&lt;/strong&gt;&lt;br&gt;
Not its strength. For images and text, neural network architectures (CNNs, transformers) tend to dominate the state of the art. XGBoost shines on data structured in rows and columns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a GPU to use XGBoost?&lt;/strong&gt;&lt;br&gt;
No. XGBoost runs fine on CPU for small and medium datasets. It has GPU support to speed up training on large datasets, documented on the official page, but it's not a requirement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are XGBoost and LightGBM the same thing?&lt;/strong&gt;&lt;br&gt;
Both implement gradient boosting on trees, but with different design decisions (for example, how they split trees). If you're evaluating which one to use, the same criteria from this matrix apply: it depends on the dataset and how much time you have to tune each one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use XGBoost directly with the scikit-learn API?&lt;/strong&gt;&lt;br&gt;
Yes. &lt;code&gt;XGBClassifier&lt;/code&gt; and &lt;code&gt;XGBRegressor&lt;/code&gt; implement scikit-learn's &lt;code&gt;fit&lt;/code&gt;/&lt;code&gt;predict&lt;/code&gt; interface, so they plug into &lt;code&gt;Pipeline&lt;/code&gt;, &lt;code&gt;GridSearchCV&lt;/code&gt; and the rest of the ecosystem without friction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which hyperparameter has the most impact in XGBoost?&lt;/strong&gt;&lt;br&gt;
According to the docs, &lt;code&gt;learning_rate&lt;/code&gt; and &lt;code&gt;n_estimators&lt;/code&gt; interact directly: a low &lt;code&gt;learning_rate&lt;/code&gt; needs more estimators to converge. &lt;code&gt;max_depth&lt;/code&gt; controls how much each individual tree can memorize. There's no single "magic" parameter — the interaction between them is the whole point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does XGBoost replace the need for feature engineering?&lt;/strong&gt;&lt;br&gt;
No. It's still a tree-based model: it benefits from well-built features just like scikit-learn does. No boosting algorithm fixes a poorly-prepared data problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Take
&lt;/h2&gt;

&lt;p&gt;If you're starting a tabular classification or regression problem and you don't have evidence that you need to squeeze every point of performance, start with scikit-learn. It's less surface area to break, fewer hyperparameters to explain in a code review, and a reliable baseline in minutes. Move to XGBoost when you have the tuning time budgeted and a concrete reason — a competition, a product requirement that justifies the effort — not because "that's what the Kaggle winners use."&lt;/p&gt;

&lt;p&gt;The question you need to ask yourself before switching libraries isn't "which one is better?" It's "do I have the time to tune six hyperparameters instead of two, and will that time pay off with the improvement I actually need?" If the answer is no, stick with the RandomForest and keep iterating somewhere else in the pipeline — probably the features, not the algorithm.&lt;/p&gt;

&lt;p&gt;If this got you thinking about how you structure the rest of your data stack, it might be worth checking out how &lt;a href="https://juanchi.dev/en/blog/depends-on-not-enough-service-healthy-compose" rel="noopener noreferrer"&gt;Docker Compose handles healthcheck and depends_on&lt;/a&gt; when training runs in a separate container from the service that serves the model, or how to &lt;a href="https://juanchi.dev/en/blog/actuator-env-sanitizer-naming-conventions" rel="noopener noreferrer"&gt;sanitize sensitive values in Actuator&lt;/a&gt; if your ML pipeline exposes metrics over HTTP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Original source:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;XGBoost Documentation — &lt;a href="https://xgboost.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;https://xgboost.readthedocs.io/en/stable/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/xgboost-vs-scikit-learn-different-problem" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>gradientboosting</category>
    </item>
    <item>
      <title>XGBoost no le gana a scikit-learn, resuelve otro problema</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Wed, 16 Sep 2026 12:00:14 +0000</pubDate>
      <link>https://dev.to/jtorchia/xgboost-no-le-gana-a-scikit-learn-resuelve-otro-problema-5404</link>
      <guid>https://dev.to/jtorchia/xgboost-no-le-gana-a-scikit-learn-resuelve-otro-problema-5404</guid>
      <description>&lt;p&gt;Abrís un notebook, entrenás un &lt;code&gt;RandomForestClassifier&lt;/code&gt; de scikit-learn sobre un dataset tabular chico, y el resultado es razonable. Alguien en el equipo te dice "probá con XGBoost, te va a andar mejor". Cambiás dos líneas, corrés de nuevo, y el número mejora un poco. La pregunta que casi nadie se hace en ese momento es &lt;em&gt;por qué&lt;/em&gt; mejoró — y si esa mejora justifica el costo de tener seis hiperparámetros nuevos que tunear en lugar de dos.&lt;/p&gt;

&lt;p&gt;Mi tesis es esta: XGBoost no es "mejor" que scikit-learn en general. Es la herramienta correcta cuando necesitás exprimir performance en datos tabulares con boosting, al costo de más superficie de configuración. Si no estás en ese escenario específico, estás pagando complejidad por nada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué es XGBoost (y qué no es)
&lt;/h2&gt;

&lt;p&gt;XGBoost significa &lt;em&gt;Extreme Gradient Boosting&lt;/em&gt;. Es una implementación de gradient boosting sobre árboles de decisión, optimizada para velocidad y para exprimir cada punto de señal que el dataset tiene para dar. La &lt;a href="https://xgboost.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;documentación oficial&lt;/a&gt; lo describe como una librería de boosting de gradiente optimizada para ser "eficiente, flexible y portable" — no como un algoritmo nuevo, sino como una implementación particular de una idea que ya existía.&lt;/p&gt;

&lt;p&gt;La idea de fondo, gradient boosting, es simple de explicar aunque no sea simple de implementar bien:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Entrenás un modelo débil (típicamente un árbol chico).&lt;/li&gt;
&lt;li&gt;Medís el error que ese modelo comete.&lt;/li&gt;
&lt;li&gt;Entrenás un segundo modelo que se enfoca en corregir ese error.&lt;/li&gt;
&lt;li&gt;Repetís, sumando modelos, cada uno corrigiendo lo que el anterior se equivocó.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Un &lt;code&gt;RandomForestClassifier&lt;/code&gt; de scikit-learn hace algo distinto: entrena muchos árboles en paralelo, cada uno sobre una muestra distinta de los datos, y promedia sus votos. No hay corrección secuencial de errores — hay votación. Esa diferencia de diseño es la que explica por qué, en ciertos datasets tabulares, boosting saca ventaja: cada árbol nuevo está literalmente entrenado para tapar el agujero que dejó el anterior, en lugar de simplemente aportar otro punto de vista independiente.&lt;/p&gt;

&lt;p&gt;Lo que la documentación de XGBoost &lt;strong&gt;no&lt;/strong&gt; dice es que esto vaya a pasar siempre. Documenta el algoritmo, los parámetros y la API. No documenta "en qué porcentaje de datasets gana" — eso depende del dataset, y cualquier cifra genérica que circule sobre eso no tiene fuente citable.&lt;/p&gt;

&lt;h2&gt;
  
  
  El ejemplo reproducible: mismo dataset, dos librerías
&lt;/h2&gt;

&lt;p&gt;Para ver la diferencia sin inventar un caso de uso, usá el dataset de juguete &lt;code&gt;load_breast_cancer&lt;/code&gt; que trae scikit-learn — 569 filas, 30 features numéricas, clasificación binaria. Es chico a propósito: no busca demostrar quién "gana", busca mostrar el flujo de trabajo y dónde aparecen los hiperparámetros nuevos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# comparacion_basica.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_breast_cancer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;train_test_split&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.ensemble&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RandomForestClassifier&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;xgboost&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;XGBClassifier&lt;/span&gt;

&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_breast_cancer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;return_X_y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;train_test_split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RandomForestClassifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;xgb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;XGBClassifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eval_metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;logloss&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;xgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RandomForest score:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;XGBoost score:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Corré esto en un notebook de Jupyter y vas a tener dos números de accuracy sobre el mismo split. No voy a inventar acá qué número te va a salir a vos — depende de la versión de las librerías, del seed, del hardware. Lo que sí podés afirmar con este experimento es algo distinto: &lt;strong&gt;ambos modelos corrieron sobre el mismo dataset, con el mismo split, y uno de los dos tiene una API más chica para tunear.&lt;/strong&gt; &lt;code&gt;RandomForestClassifier&lt;/code&gt; tiene &lt;code&gt;n_estimators&lt;/code&gt;, &lt;code&gt;max_depth&lt;/code&gt;, &lt;code&gt;min_samples_split&lt;/code&gt; como parámetros centrales. &lt;code&gt;XGBClassifier&lt;/code&gt; suma &lt;code&gt;learning_rate&lt;/code&gt;, &lt;code&gt;subsample&lt;/code&gt;, &lt;code&gt;colsample_bytree&lt;/code&gt;, &lt;code&gt;gamma&lt;/code&gt;, &lt;code&gt;reg_alpha&lt;/code&gt;, &lt;code&gt;reg_lambda&lt;/code&gt; — y cada uno interactúa con los demás.&lt;/p&gt;

&lt;p&gt;Eso es el costo real de XGBoost. No es que sea más lento entrenar (para datasets chicos, no lo vas a notar) ni que sea más difícil de instalar. Es que el espacio de hiperparámetros es más grande, y una búsqueda mal hecha en ese espacio te puede dar un resultado peor que el RandomForest con defaults.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  A[Dataset tabular] --&amp;gt; B{¿Necesito exprimir cada punto de performance?}
  B --&amp;gt;|No, quiero un baseline rápido| C[RandomForest de scikit-learn]
  B --&amp;gt;|Sí, y tengo tiempo para tunear| D[XGBoost]
  D --&amp;gt; E{¿Tuneaste learning_rate, subsample, reg_lambda?}
  E --&amp;gt;|No| F[Resultado probablemente peor que el baseline]
  E --&amp;gt;|Sí, con validación cruzada| G[Resultado potencialmente mejor]&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Dónde se equivoca la gente: la receta copiada sin criterio
&lt;/h2&gt;

&lt;p&gt;La receta común que circula es: "usá XGBoost porque gana las competencias de Kaggle". Es cierto que XGBoost tuvo un rol protagónico en muchas competencias de datos tabulares — eso es historia documentada. El problema es el salto lógico siguiente: de ahí no se sigue que gane en &lt;strong&gt;tu&lt;/strong&gt; dataset, con &lt;strong&gt;tus&lt;/strong&gt; features, en &lt;strong&gt;tu&lt;/strong&gt; volumen de filas.&lt;/p&gt;

&lt;p&gt;El costo oculto de copiar esa receta sin pensarlo es doble:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overfitting silencioso.&lt;/strong&gt; XGBoost con muchos estimadores y sin regularización (&lt;code&gt;reg_alpha&lt;/code&gt;, &lt;code&gt;reg_lambda&lt;/code&gt; en cero) memoriza el dataset de entrenamiento más fácil que un RandomForest, porque cada árbol nuevo se ajusta específicamente al error residual — incluido el ruido.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiempo de tuneo que nadie presupuestó.&lt;/strong&gt; Si vas a usar XGBoost en serio, necesitás una búsqueda de hiperparámetros (grid search, random search, o algo tipo Optuna) con validación cruzada. Eso es tiempo de cómputo y tiempo de persona que un RandomForest con defaults razonables no te pide.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El contraejemplo más simple: un dataset tabular con pocas filas (digamos, unos pocos cientos) y alta dimensionalidad de ruido. Ahí un RandomForest con su regularización implícita por bagging puede comportarse de forma más estable que un XGBoost mal tuneado, que va a aprender a explicar el ruido en lugar de la señal. No tengo un benchmark público para citar ese caso puntual — es un patrón de comportamiento esperado según cómo funciona cada algoritmo, no una medición que hice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Matriz de decisión: cuándo mirar XGBoost primero
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situación&lt;/th&gt;
&lt;th&gt;Qué mirar primero&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;Datos tabulares, necesitás un baseline rápido&lt;/td&gt;
&lt;td&gt;scikit-learn (RandomForest o GradientBoosting)&lt;/td&gt;
&lt;td&gt;Menos hiperparámetros, resultado razonable sin tuneo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Competencia o problema donde cada punto de performance cuenta&lt;/td&gt;
&lt;td&gt;XGBoost, con presupuesto de tiempo para tunear&lt;/td&gt;
&lt;td&gt;Ahí es donde el boosting secuencial rinde, si lo acompañás con validación cruzada&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dataset con muchas columnas categóricas sin encodear&lt;/td&gt;
&lt;td&gt;Revisar soporte nativo de categóricas en la versión de XGBoost que uses&lt;/td&gt;
&lt;td&gt;La documentación oficial detalla el soporte por versión — no asumas que "simplemente funciona" sin chequear&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poco tiempo de equipo para tuneo, plazo corto&lt;/td&gt;
&lt;td&gt;scikit-learn primero&lt;/td&gt;
&lt;td&gt;Un XGBoost mal tuneado puede rendir peor que un RandomForest con defaults&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Necesitás explicar el modelo a alguien no técnico&lt;/td&gt;
&lt;td&gt;Cualquiera de los dos, pero revisá herramientas de interpretabilidad (feature importance, SHAP) antes de elegir por performance sola&lt;/td&gt;
&lt;td&gt;La interpretabilidad no depende de la librería, depende de qué instrumentés encima&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Esta tabla no es una conclusión cerrada sobre cuál algoritmo "gana". Es un punto de partida para decidir qué probar primero según la restricción real que tengas: tiempo, volumen de datos, necesidad de explicar el modelo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los límites de esto (y por qué no cierro con un número)
&lt;/h2&gt;

&lt;p&gt;Ojo con la afirmación que casi escribí antes de sacarla: "XGBoost gana en la mayoría de los casos de datos tabulares". Esa frase no tiene una fuente citable que yo pueda poner acá, y es exactamente el tipo de claim sin evidencia que terminó bloqueado en un post anterior sobre scikit-learn. No la voy a repetir con otras palabras.&lt;/p&gt;

&lt;p&gt;Lo que sí puedo decir con el nivel de certeza que la evidencia permite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;La documentación oficial de XGBoost explica el algoritmo, los parámetros y el soporte de features. No documenta comparativas de performance contra scikit-learn en datasets generales — eso lo tenés que correr vos, con los datos.&lt;/li&gt;
&lt;li&gt;El experimento del snippet de arriba es reproducible: cualquiera que lo corra va a tener dos números comparables, pero esos números van a variar según versión de librería y seed. No es una medición fija que puedas citar como "XGBoost dio X% mejor".&lt;/li&gt;
&lt;li&gt;Sin un experimento de validación cruzada con búsqueda de hiperparámetros sobre el dataset específico que tengas, cualquier comparación de accuracy entre ambos modelos es anecdótica.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si necesitás una decisión reproducible y medible, el próximo paso concreto es correr &lt;code&gt;GridSearchCV&lt;/code&gt; o &lt;code&gt;RandomizedSearchCV&lt;/code&gt; sobre ambos modelos, con el mismo esquema de validación cruzada, y comparar la distribución de scores — no un solo número de un solo split.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿XGBoost sirve para datos que no son tabulares, como imágenes o texto?&lt;/strong&gt;&lt;br&gt;
No es su fuerte. Para imágenes y texto, arquitecturas de redes neuronales (CNNs, transformers) suelen dominar el estado del arte. XGBoost brilla en datos estructurados en filas y columnas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Necesito GPU para usar XGBoost?&lt;/strong&gt;&lt;br&gt;
No. XGBoost corre en CPU sin problema para datasets chicos y medianos. Tiene soporte de GPU para acelerar entrenamiento en datasets grandes, documentado en la página oficial, pero no es un requisito.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿XGBoost y LightGBM son lo mismo?&lt;/strong&gt;&lt;br&gt;
Ambos implementan gradient boosting sobre árboles, pero con decisiones de diseño distintas (por ejemplo, cómo hacen el split de los árboles). Si estás evaluando cuál usar, valen los mismos criterios de esta matriz: depende del dataset y del tiempo que tengas para tunear cada uno.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Puedo usar XGBoost directamente con la API de scikit-learn?&lt;/strong&gt;&lt;br&gt;
Sí. &lt;code&gt;XGBClassifier&lt;/code&gt; y &lt;code&gt;XGBRegressor&lt;/code&gt; implementan la interfaz &lt;code&gt;fit&lt;/code&gt;/&lt;code&gt;predict&lt;/code&gt; de scikit-learn, así que se integran con &lt;code&gt;Pipeline&lt;/code&gt;, &lt;code&gt;GridSearchCV&lt;/code&gt; y el resto del ecosistema sin fricción.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cuál es el hiperparámetro que más impacto tiene en XGBoost?&lt;/strong&gt;&lt;br&gt;
Según la documentación, &lt;code&gt;learning_rate&lt;/code&gt; y &lt;code&gt;n_estimators&lt;/code&gt; interactúan directamente: un &lt;code&gt;learning_rate&lt;/code&gt; bajo necesita más estimadores para converger. &lt;code&gt;max_depth&lt;/code&gt; controla cuánto puede memorizar cada árbol individual. No hay un único parámetro "mágico" — la interacción entre ellos es el punto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿XGBoost reemplaza la necesidad de feature engineering?&lt;/strong&gt;&lt;br&gt;
No. Sigue siendo un modelo sobre árboles: se beneficia de features bien construidas igual que scikit-learn. Ningún algoritmo de boosting resuelve un problema de datos mal preparados.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postura final
&lt;/h2&gt;

&lt;p&gt;Si estás arrancando un problema de clasificación o regresión tabular y no tenés evidencia de que necesitás exprimir cada punto de performance, arrancá con scikit-learn. Es menos superficie para romper, menos hiperparámetros para explicar en un code review, y un baseline confiable en minutos. Movete a XGBoost cuando tengas el tiempo de tuneo presupuestado y una razón concreta — una competencia, un requisito de producto que justifique el esfuerzo — no porque "es lo que usan los que ganan Kaggle".&lt;/p&gt;

&lt;p&gt;La pregunta que te tenés que hacer antes de cambiar de librería no es "¿cuál es mejor?". Es "¿tengo el tiempo para tunear seis hiperparámetros en lugar de dos, y ese tiempo me lo van a pagar con la mejora que necesito?". Si la respuesta es no, quedate con el RandomForest y seguí iterando en otra parte del pipeline — probablemente en las features, no en el algoritmo.&lt;/p&gt;

&lt;p&gt;Si esto te hizo pensar en cómo estructurás el resto del stack de datos, capaz te sirve repasar cómo &lt;a href="https://juanchi.dev/es/blog/docker-compose-healthcheck-depends-on-service-healthy" rel="noopener noreferrer"&gt;Docker Compose maneja el healthcheck y el depends_on&lt;/a&gt; cuando el entrenamiento corre en un contenedor separado del servicio que sirve el modelo, o cómo &lt;a href="https://juanchi.dev/es/blog/actuator-env-sanitizar-valores-sensibles" rel="noopener noreferrer"&gt;sanitizar valores sensibles en Actuator&lt;/a&gt; si el pipeline de ML expone métricas por HTTP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fuente original:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;XGBoost Documentation — &lt;a href="https://xgboost.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;https://xgboost.readthedocs.io/en/stable/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/xgboost-explicado-dataset-juguete-scikit-learn" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>machinelearning</category>
      <category>python</category>
    </item>
    <item>
      <title>The /actuator/env Sanitizer Doesn't Know Your Naming Conventions</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Fri, 11 Sep 2026 12:00:20 +0000</pubDate>
      <link>https://dev.to/jtorchia/the-actuatorenv-sanitizer-doesnt-know-your-naming-conventions-3dpl</link>
      <guid>https://dev.to/jtorchia/the-actuatorenv-sanitizer-doesnt-know-your-naming-conventions-3dpl</guid>
      <description>&lt;p&gt;I recently wrote about &lt;a href="https://juanchi.dev/en/blog/actuator-endpoints-spring-boot-allowlist-security" rel="noopener noreferrer"&gt;allowlisting actuator endpoints&lt;/a&gt;: what to expose and what not to. One question stayed open and kept nagging at me: if you decide to leave &lt;code&gt;/actuator/env&lt;/code&gt; enabled — because you need it for debugging in staging, because infra is asking for it — what actually guarantees you won't show a secret in plain text the first time someone hits it with &lt;code&gt;curl&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Short answer: nothing, if you blindly trust the default sanitizer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with actuator env show values
&lt;/h2&gt;

&lt;p&gt;Spring Boot ships with a sanitizer that automatically masks certain values before showing them in &lt;code&gt;/actuator/env&lt;/code&gt;. It works by property name: if the key contains &lt;code&gt;password&lt;/code&gt;, &lt;code&gt;secret&lt;/code&gt;, &lt;code&gt;key&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt; or &lt;code&gt;credentials&lt;/code&gt;, the value comes out as &lt;code&gt;******&lt;/code&gt;. It's a reasonable defense for the generic case.&lt;/p&gt;

&lt;p&gt;The problem is exactly that: it's generic. It covers the words some Spring developer imagined you'd use. It doesn't cover the ones your team actually uses.&lt;/p&gt;

&lt;p&gt;Think about how environment variables get named in a real project: &lt;code&gt;DB_PASS&lt;/code&gt; instead of &lt;code&gt;DB_PASSWORD&lt;/code&gt;, &lt;code&gt;API_AUTH&lt;/code&gt; instead of &lt;code&gt;API_TOKEN&lt;/code&gt;, &lt;code&gt;WEBHOOK_SIGNING&lt;/code&gt;, &lt;code&gt;PARTNER_SHARED_VALUE&lt;/code&gt;, &lt;code&gt;INTERNAL_CIPHER&lt;/code&gt;. None of those contain the keywords the sanitizer looks for. All of them end up in the &lt;code&gt;/actuator/env&lt;/code&gt; JSON response without a single asterisk.&lt;/p&gt;

&lt;p&gt;My tesis here: the default sanitizer covers the obvious names, but that's not where the real leaks happen. The real leaks happen through the naming convention someone improvised in a sprint under deadline pressure, and nobody circled back to add it to the pattern list. I've seen this exact gap in code review — a variable named &lt;code&gt;PARTNER_SHARED_VALUE&lt;/code&gt; sitting in a staging config, fully visible, because nobody thought a shared-secret-style value needed the word "secret" in it to deserve masking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the official source says — and what it doesn't
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.spring.io/spring-boot/reference/actuator/endpoints.html" rel="noopener noreferrer"&gt;official Spring Boot Actuator documentation&lt;/a&gt; confirms the behavior: the &lt;code&gt;/env&lt;/code&gt; endpoint applies sanitization to values before exposing them, and that behavior is configurable through the &lt;code&gt;SanitizingFunction&lt;/code&gt; interface, which replaced the keyword-based &lt;code&gt;Sanitizer&lt;/code&gt; mechanism in more recent versions of the framework.&lt;/p&gt;

&lt;p&gt;What the docs don't say — because it's not their job to say it — is what specific names you're going to use in your project. That's on you to audit. The docs give you the extension mechanism; the catalog of what to sanitize is the responsibility of whoever configures the project, not the framework.&lt;/p&gt;

&lt;p&gt;That gap between "mechanism available" and "correct configuration for this case" is exactly where most exposure incidents on default, unreviewed configurations slip through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where people get it wrong
&lt;/h2&gt;

&lt;p&gt;The common recipe I see repeated on forums and in inherited configs is: "turn on &lt;code&gt;management.endpoint.env.show-values=when-authorized&lt;/code&gt; and you're covered." That solves &lt;em&gt;who&lt;/em&gt; can see the values, not &lt;em&gt;what&lt;/em&gt; values show up unmasked to whoever has authorization. They're two different problems, and they get treated as one all the time.&lt;/p&gt;

&lt;p&gt;The hidden cost shows up when the endpoint stays accessible to an internal role — a monitoring service, an ops dashboard — and that role ends up seeing credentials nobody thought to mask because the variable name didn't match the default pattern.&lt;/p&gt;

&lt;p&gt;Typical counterexample: a project that uses Vault or AWS Secrets Manager for production, but leaves variables named something like &lt;code&gt;THIRD_PARTY_SHARED_SECRET_VALUE&lt;/code&gt; in &lt;code&gt;application-staging.yml&lt;/code&gt;. The default sanitizer doesn't reliably catch &lt;code&gt;SHARED_SECRET_VALUE&lt;/code&gt; as a unit if the matching logic is stricter than a plain substring check for "secret" — and in a lot of custom configs it ends up not covering variants with underscores or mixed case if someone overwrote the sanitizer without reviewing the inherited regex.&lt;/p&gt;

&lt;p&gt;Here's how to extend the sanitizer with your own pattern, using &lt;code&gt;SanitizingFunction&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Custom sanitizer configuration for /actuator/env&lt;/span&gt;
&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SanitizingFunction&lt;/span&gt; &lt;span class="nf"&gt;customSanitizingFunction&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Pattern covering the team's own naming conventions&lt;/span&gt;
    &lt;span class="nc"&gt;Pattern&lt;/span&gt; &lt;span class="n"&gt;patronCustom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pattern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"(?i).*(pass|auth|signing|shared|cipher).*"&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;nombre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSanitizableData&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getKey&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;patronCustom&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nombre&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"******"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// if it doesn't match, let the default sanitizer keep the chain going&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;};&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This bean gets added to the existing chain of sanitizers; it doesn't replace it. Spring Boot runs every registered &lt;code&gt;SanitizingFunction&lt;/code&gt; in order and applies masking if any of them decides it's warranted.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  A[Request a /actuator/env] --&amp;gt; B{Sanitizer default}
  B --&amp;gt;|nombre matchea password/secret/token| C[Enmascarado con ******]
  B --&amp;gt;|no matchea| D{SanitizingFunction custom}
  D --&amp;gt;|nombre matchea patron propio| C
  D --&amp;gt;|no matchea nada| E[Valor expuesto en texto plano]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;That last path, the one on the right, is the one you have to actively close. It doesn't close itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision matrix for /actuator/env
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;What to check first&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint exposed only on localhost/debug&lt;/td&gt;
&lt;td&gt;Confirm there's no tunneling or proxy exposing it outward&lt;/td&gt;
&lt;td&gt;Default sanitizer may be enough, but audit variable names anyway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint accessible in staging with monitoring roles&lt;/td&gt;
&lt;td&gt;What custom variables the team uses, not just Spring's&lt;/td&gt;
&lt;td&gt;Add a &lt;code&gt;SanitizingFunction&lt;/code&gt; with your own pattern before enabling access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Variables with unconventional names (&lt;code&gt;DB_PASS&lt;/code&gt;, &lt;code&gt;API_AUTH&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;List every key in &lt;code&gt;application.yml&lt;/code&gt; and &lt;code&gt;.env&lt;/code&gt; for each environment&lt;/td&gt;
&lt;td&gt;Extend the regex pattern, don't trust the default list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integrations with external providers (webhooks, partners)&lt;/td&gt;
&lt;td&gt;Names the partner defines, not the ones you define&lt;/td&gt;
&lt;td&gt;Custom sanitizer by provider prefix or suffix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deciding whether to disable the whole endpoint&lt;/td&gt;
&lt;td&gt;Whether anyone on the team regularly audits property names&lt;/td&gt;
&lt;td&gt;Disabling it is safer than an unmaintained sanitizer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the one I most want to point out: if nobody's periodically reviewing which property names show up in the code, a poorly maintained custom sanitizer gives a false sense of security — arguably worse than no sanitizer at all, because it looks like coverage. The allowlist criterion I laid out in the previous post is preferable to a sanitizer nobody updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confusing authorization with sanitization.&lt;/strong&gt; &lt;code&gt;show-values=when-authorized&lt;/code&gt; controls access, not content. They're independent settings that both need reviewing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copying the regex from a previous project without adapting it.&lt;/strong&gt; Naming conventions change between teams and even between projects within the same team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not testing the sanitizer against a negative case.&lt;/strong&gt; Common gap: nobody writes a test that verifies a "weird"-named variable actually comes out masked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thinking this is only a production problem.&lt;/strong&gt; Staging and shared dev environments also expose &lt;code&gt;/actuator/env&lt;/code&gt;, and that's often exactly where real third-party credentials live for integration testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming a Spring Boot version bump updates your pattern list for you.&lt;/strong&gt; It doesn't. The framework maintains its own default keywords; your team's naming conventions are always your own responsibility, version after version.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does Spring Boot's sanitizer mask all sensitive values by default?&lt;/strong&gt;&lt;br&gt;
No. It masks values whose property name contains specific words like password, secret, key, token or credentials. Any naming convention different from that stays uncovered unless explicitly configured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between &lt;code&gt;Sanitizer&lt;/code&gt; and &lt;code&gt;SanitizingFunction&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;SanitizingFunction&lt;/code&gt; is the recommended interface in recent Spring Boot versions for extending sanitization behavior programmatically, replacing the earlier approach based solely on a fixed keyword list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I have multiple &lt;code&gt;SanitizingFunction&lt;/code&gt; instances registered at once?&lt;/strong&gt;&lt;br&gt;
Yes. Spring Boot runs them in a chain; if any of them decides to mask a value, that value stays masked in the final response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is disabling &lt;code&gt;/actuator/env&lt;/code&gt; safer than sanitizing it?&lt;/strong&gt;&lt;br&gt;
Depends on the use case. If the team has no process to keep the custom sanitizer updated, disabling the endpoint or restricting it with a strict allowlist reduces risk with less ongoing maintenance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;show-values=when-authorized&lt;/code&gt; solve the exposed-secrets problem?&lt;/strong&gt;&lt;br&gt;
Not by itself. It controls who can see the values, not what values show up unmasked to those authorized users. They're two separate settings that need to be combined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I test that my custom sanitizer works before deploying?&lt;/strong&gt;&lt;br&gt;
With a unit test that invokes the &lt;code&gt;SanitizingFunction&lt;/code&gt; bean directly against real property names from the project, including cases with underscores, mixed case, and external provider prefixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I land
&lt;/h2&gt;

&lt;p&gt;Spring Boot's default sanitizer isn't a placebo: it covers the generic case reasonably well, per what the official reference itself documents. What I can't claim without production evidence is exactly how much it reduces risk in any specific project — that depends entirely on how far that project's naming conventions drift from the default list.&lt;/p&gt;

&lt;p&gt;What I can say with technical confidence: if nobody's audited the project's custom property names against the sanitizer's pattern list, there's an unclosed gap sitting there. That's not a remote possibility, it's a direct consequence of how the mechanism works — a name-matching filter only catches the names it was told to look for.&lt;/p&gt;

&lt;p&gt;My practical recommendation follows the same logic I used to think through endpoint allowlisting in the &lt;a href="https://juanchi.dev/en/blog/actuator-endpoints-spring-boot-allowlist-security" rel="noopener noreferrer"&gt;previous post about actuator&lt;/a&gt;: treat sanitization as a living list that gets reviewed every time a new integration gets added, not as a configuration you set once and forget. Today's regex won't cover the variable name someone's going to invent next sprint. The uncomfortable question worth asking your team right now: when was the last time anyone actually opened &lt;code&gt;application.yml&lt;/code&gt; and checked every key against the sanitizer, instead of assuming Spring already handled it?&lt;/p&gt;

&lt;p&gt;If you're into how I think about architecture decisions with this same "what does the tool cover versus what do I have to cover" lens, I've got related posts on &lt;a href="https://juanchi.dev/en/blog/stateless-jwt-vs-stateful-sessions-identity-systems" rel="noopener noreferrer"&gt;JWT vs stateful sessions&lt;/a&gt; and on &lt;a href="https://juanchi.dev/en/blog/java-champion-2026-real-criteria-why-it-matters" rel="noopener noreferrer"&gt;what the path to Java Champion actually means&lt;/a&gt; that touch the same tension from other angles.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Original source:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring Boot Actuator Docs: &lt;a href="https://docs.spring.io/spring-boot/reference/actuator/endpoints.html" rel="noopener noreferrer"&gt;https://docs.spring.io/spring-boot/reference/actuator/endpoints.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/actuator-env-sanitizer-naming-conventions" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>springboot</category>
      <category>java</category>
      <category>actuator</category>
    </item>
    <item>
      <title>El sanitizer de /actuator/env no conoce tus convenciones</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Fri, 11 Sep 2026 12:00:15 +0000</pubDate>
      <link>https://dev.to/jtorchia/el-sanitizer-de-actuatorenv-no-conoce-tus-convenciones-3oco</link>
      <guid>https://dev.to/jtorchia/el-sanitizer-de-actuatorenv-no-conoce-tus-convenciones-3oco</guid>
      <description>&lt;p&gt;Hace poco escribí sobre &lt;a href="https://juanchi.dev/es/blog/actuator-endpoints-spring-boot-seguridad-allowlist" rel="noopener noreferrer"&gt;allowlist en actuator endpoints&lt;/a&gt;: qué exponer y qué no. Quedó una pregunta abierta que me siguió picando: si decidís dejar &lt;code&gt;/actuator/env&lt;/code&gt; habilitado —porque lo necesitás para debug en staging, porque el equipo de infra lo pide— ¿qué te asegura que no vas a mostrar un secreto en texto plano la primera vez que alguien le pegue un &lt;code&gt;curl&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;La respuesta corta es: nada, si confiás ciegamente en el sanitizer default.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema con actuator env show values
&lt;/h2&gt;

&lt;p&gt;Spring Boot trae un sanitizer que enmascara automáticamente ciertos valores antes de mostrarlos en &lt;code&gt;/actuator/env&lt;/code&gt;. Funciona por nombre de propiedad: si la clave contiene &lt;code&gt;password&lt;/code&gt;, &lt;code&gt;secret&lt;/code&gt;, &lt;code&gt;key&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt; o &lt;code&gt;credentials&lt;/code&gt;, el valor sale como &lt;code&gt;******&lt;/code&gt;. Es una defensa razonable para el caso genérico.&lt;/p&gt;

&lt;p&gt;El problema es justamente eso: es genérica. Cubre las palabras que un desarrollador de Spring imaginó que ibas a usar. No cubre las que tu equipo realmente usa.&lt;/p&gt;

&lt;p&gt;Pensá en cómo se nombran las variables de entorno en un proyecto real: &lt;code&gt;DB_PASS&lt;/code&gt; en vez de &lt;code&gt;DB_PASSWORD&lt;/code&gt;, &lt;code&gt;API_AUTH&lt;/code&gt; en vez de &lt;code&gt;API_TOKEN&lt;/code&gt;, &lt;code&gt;WEBHOOK_SIGNING&lt;/code&gt;, &lt;code&gt;PARTNER_SHARED_VALUE&lt;/code&gt;, &lt;code&gt;INTERNAL_CIPHER&lt;/code&gt;. Ninguna de esas contiene las palabras clave que el sanitizer busca. Todas terminan en la respuesta JSON de &lt;code&gt;/actuator/env&lt;/code&gt; sin ningún asterisco.&lt;/p&gt;

&lt;p&gt;Mi tesis es esta: el sanitizer default no falla porque esté mal escrito, falla porque asume que todo el mundo nombra igual. Y en la práctica cada equipo tiene su propio dialecto de variables de entorno. Las fugas reales casi nunca pasan por &lt;code&gt;password&lt;/code&gt; mal escrito — pasan por la convención custom que alguien inventó en un sprint y que nadie agregó a la lista de patrones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué dice la fuente oficial y qué no dice
&lt;/h2&gt;

&lt;p&gt;La &lt;a href="https://docs.spring.io/spring-boot/reference/actuator/endpoints.html" rel="noopener noreferrer"&gt;documentación oficial de Spring Boot Actuator&lt;/a&gt; confirma el comportamiento: el endpoint &lt;code&gt;/env&lt;/code&gt; aplica sanitización sobre los valores antes de exponerlos, y ese comportamiento es configurable mediante la interfaz &lt;code&gt;SanitizingFunction&lt;/code&gt;, que reemplazó al mecanismo de &lt;code&gt;Sanitizer&lt;/code&gt; basado en keywords en versiones más recientes del framework.&lt;/p&gt;

&lt;p&gt;Lo que la doc no dice —porque no es su trabajo decirlo— es qué nombres específicos vas a usar en tu proyecto. Eso lo tenés que auditar vos. La doc te da el mecanismo de extensión; el catálogo de qué sanitizar es responsabilidad de quien configura el proyecto, no del framework.&lt;/p&gt;

&lt;p&gt;Esa diferencia entre "mecanismo disponible" y "configuración correcta para este caso puntual" es exactamente donde se cuelan la mayoría de los incidentes de exposición que se reportan en configuraciones default sin revisión.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dónde se equivoca la gente
&lt;/h2&gt;

&lt;p&gt;La receta común que veo repetida en foros y en configuraciones heredadas es: "activá &lt;code&gt;management.endpoint.env.show-values=when-authorized&lt;/code&gt; y ya estás cubierto". Eso resuelve &lt;em&gt;quién&lt;/em&gt; puede ver los valores, no &lt;em&gt;qué&lt;/em&gt; valores se muestran sin máscara a quien tiene autorización. Son dos problemas distintos y se los trata como uno solo todo el tiempo.&lt;/p&gt;

&lt;p&gt;El costo oculto aparece cuando el endpoint queda accesible para un rol interno —un servicio de monitoreo, un dashboard de operaciones— y ese rol termina viendo credenciales que nadie pensó en enmascarar porque el nombre de la variable no calzaba con el patrón default.&lt;/p&gt;

&lt;p&gt;Caso tipico que uso para explicar esto: un proyecto que usa Vault o AWS Secrets Manager para producción, pero en &lt;code&gt;application-staging.yml&lt;/code&gt; deja variables con nombres tipo &lt;code&gt;THIRD_PARTY_SHARED_SECRET_VALUE&lt;/code&gt;. El sanitizer default no necesariamente matchea &lt;code&gt;SHARED_SECRET_VALUE&lt;/code&gt; como unidad reconocible si el patrón de búsqueda es más rígido que una simple substring de "secret" — y en configuraciones custom heredadas, es común que las variantes con guiones bajos o mayúsculas mixtas queden afuera si alguien sobreescribió el regex sin revisarlo a fondo.&lt;/p&gt;

&lt;p&gt;Acá va la forma de extender el sanitizer con un patrón propio, usando &lt;code&gt;SanitizingFunction&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Configuracion custom del sanitizer para /actuator/env&lt;/span&gt;
&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SanitizingFunction&lt;/span&gt; &lt;span class="nf"&gt;customSanitizingFunction&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Patron que cubre convenciones propias del equipo&lt;/span&gt;
    &lt;span class="nc"&gt;Pattern&lt;/span&gt; &lt;span class="n"&gt;patronCustom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pattern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"(?i).*(pass|auth|signing|shared|cipher).*"&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;nombre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSanitizableData&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getKey&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;patronCustom&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nombre&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"******"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// si no matchea, dejamos que el sanitizer default siga la cadena&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;};&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este bean se agrega a la cadena existente de sanitizers; no la reemplaza. Spring Boot ejecuta todos los &lt;code&gt;SanitizingFunction&lt;/code&gt; registrados en orden y aplica el enmascarado si alguno de ellos decide que corresponde.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  A[Request a /actuator/env] --&amp;gt; B{Sanitizer default}
  B --&amp;gt;|nombre matchea password/secret/token| C[Enmascarado con ******]
  B --&amp;gt;|no matchea| D{SanitizingFunction custom}
  D --&amp;gt;|nombre matchea patron propio| C
  D --&amp;gt;|no matchea nada| E[Valor expuesto en texto plano]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Ese último camino, el de la derecha, es el que hay que cerrar activamente. No se cierra solo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Matriz de decisión para /actuator/env
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situación&lt;/th&gt;
&lt;th&gt;Qué mirar primero&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;Endpoint expuesto solo en localhost/debug&lt;/td&gt;
&lt;td&gt;Confirmar que no hay tunneling ni proxy hacia afuera&lt;/td&gt;
&lt;td&gt;Sanitizer default puede alcanzar, pero auditá nombres de variables igual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint accesible en staging con roles de monitoreo&lt;/td&gt;
&lt;td&gt;Qué variables custom usa tu equipo, no solo las de Spring&lt;/td&gt;
&lt;td&gt;Agregar &lt;code&gt;SanitizingFunction&lt;/code&gt; con el patrón propio antes de habilitar acceso&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Variables con nombres no convencionales (&lt;code&gt;DB_PASS&lt;/code&gt;, &lt;code&gt;API_AUTH&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Listar todas las claves de &lt;code&gt;application.yml&lt;/code&gt; y &lt;code&gt;.env&lt;/code&gt; de cada ambiente&lt;/td&gt;
&lt;td&gt;Extender el patrón regex, no confiar en la lista default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integraciones con proveedores externos (webhooks, partners)&lt;/td&gt;
&lt;td&gt;Nombres que el partner define, no los que definís vos&lt;/td&gt;
&lt;td&gt;Sanitizer custom por prefijo o sufijo del proveedor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Necesitás decidir si deshabilitar el endpoint entero&lt;/td&gt;
&lt;td&gt;Si nadie en el equipo audita nombres de propiedades regularmente&lt;/td&gt;
&lt;td&gt;Deshabilitarlo es más seguro que un sanitizer sin mantenimiento&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;La última fila es la que más me interesa señalar: si no hay alguien revisando periódicamente qué nombres de propiedad aparecen en el código, un sanitizer custom mal mantenido da una falsa sensación de seguridad — peor que no tener nada, porque genera confianza donde no debería haberla. Prefiero el criterio de allowlist que ya planteé en el post anterior antes que un sanitizer que nadie actualiza.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes y gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confundir autorización con sanitización.&lt;/strong&gt; &lt;code&gt;show-values=when-authorized&lt;/code&gt; controla acceso, no contenido. Son configuraciones independientes que hay que revisar las dos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copiar el regex de un proyecto anterior sin adaptarlo.&lt;/strong&gt; Las convenciones de nombres cambian entre equipos y entre proyectos dentro del mismo equipo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No testear el sanitizer con un caso negativo.&lt;/strong&gt; Falta común: nadie escribe un test que verifique que una variable con nombre "raro" efectivamente sale enmascarada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pensar que el problema es solo de producción.&lt;/strong&gt; Staging y entornos de desarrollo compartidos también exponen &lt;code&gt;/actuator/env&lt;/code&gt;, y ahí suelen vivir credenciales reales de servicios de terceros para pruebas de integración.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asumir que el framework te cubre las espaldas para siempre.&lt;/strong&gt; Spring Boot mantiene actualizados sus propios keywords default con cada release; la lista de convenciones custom de tu equipo no forma parte de esa actualización — ese mantenimiento es tuyo, siempre.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿El sanitizer de Spring Boot enmascara todos los valores sensibles por default?&lt;/strong&gt;&lt;br&gt;
No. Enmascara valores cuyo nombre de propiedad contiene palabras específicas como password, secret, key, token o credentials. Cualquier convención de nombres distinta a esa queda sin cubrir salvo que se configure explícitamente.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué diferencia hay entre &lt;code&gt;Sanitizer&lt;/code&gt; y &lt;code&gt;SanitizingFunction&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;SanitizingFunction&lt;/code&gt; es la interfaz recomendada en versiones recientes de Spring Boot para extender el comportamiento de sanitización de forma programática, reemplazando el enfoque anterior basado únicamente en una lista de keywords fija.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Puedo tener varios &lt;code&gt;SanitizingFunction&lt;/code&gt; registrados a la vez?&lt;/strong&gt;&lt;br&gt;
Sí. Spring Boot los ejecuta en cadena; si cualquiera de ellos decide enmascarar un valor, ese valor queda enmascarado en la respuesta final.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Deshabilitar &lt;code&gt;/actuator/env&lt;/code&gt; es más seguro que sanitizarlo?&lt;/strong&gt;&lt;br&gt;
Depende del caso de uso. Si tu equipo no tiene proceso para mantener actualizado el sanitizer custom, deshabilitar el endpoint o restringirlo con una allowlist estricta reduce el riesgo con menos mantenimiento continuo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿&lt;code&gt;show-values=when-authorized&lt;/code&gt; resuelve el problema de los secretos expuestos?&lt;/strong&gt;&lt;br&gt;
No por sí solo. Controla quién puede ver los valores, no qué valores se muestran sin máscara a esos usuarios autorizados. Son dos configuraciones distintas que hay que combinar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo pruebo que mi sanitizer custom funciona antes de deployar?&lt;/strong&gt;&lt;br&gt;
Con un test unitario que invoque el bean &lt;code&gt;SanitizingFunction&lt;/code&gt; directamente contra nombres de propiedad reales de tu proyecto, incluyendo casos con guiones bajos, mayúsculas mixtas y prefijos de proveedores externos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mi postura
&lt;/h2&gt;

&lt;p&gt;El sanitizer default de Spring Boot no es un placebo: cubre el caso genérico razonablemente bien, según lo que documenta la propia referencia oficial. Lo que no puedo sostener sin evidencia productiva es cuánto exactamente reduce el riesgo en un proyecto específico — eso depende enteramente de qué tan alejadas estén las convenciones de nombres de ese proyecto respecto de la lista default.&lt;/p&gt;

&lt;p&gt;Lo que sí puedo afirmar con criterio técnico: si nadie auditó los nombres de propiedad custom del proyecto contra la lista de patrones del sanitizer, hay una brecha sin cerrar. No es una posibilidad remota, es una consecuencia directa de cómo funciona el mecanismo.&lt;/p&gt;

&lt;p&gt;Mi recomendación práctica es la misma lógica que usé para pensar allowlist de endpoints en el &lt;a href="https://juanchi.dev/es/blog/actuator-endpoints-spring-boot-seguridad-allowlist" rel="noopener noreferrer"&gt;post anterior sobre actuator&lt;/a&gt;: tratá la sanitización como una lista viva que se revisa cada vez que se agrega una integración nueva, no como una configuración que se pone una vez y se olvida. El regex de hoy no cubre el nombre de variable que alguien va a inventar en el próximo sprint.&lt;/p&gt;

&lt;p&gt;Lo incómodo de todo esto es que no hay checklist que te salve si nadie mira. Podés tener el &lt;code&gt;SanitizingFunction&lt;/code&gt; más prolijo del mundo y seguir filtrando un secreto porque el pasante que armó la integración con el partner nuevo usó &lt;code&gt;PARTNER_KEY_RAW&lt;/code&gt; y a nadie se le ocurrió actualizar el regex esa semana. La pregunta que me hago antes de habilitar cualquier endpoint de actuator no es "¿tiene sanitizer?" sino "¿quién es responsable de actualizar esta lista cuando cambie algo?". Si no hay respuesta clara, prefiero apagarlo.&lt;/p&gt;

&lt;p&gt;Si te interesa cómo pienso decisiones de arquitectura con este mismo criterio de "qué cubre la herramienta versus qué tengo que cubrir yo", tengo posts relacionados sobre &lt;a href="https://juanchi.dev/es/blog/jwt-vs-sesiones-con-estado-identidad-digital-criterio" rel="noopener noreferrer"&gt;JWT vs sesiones con estado&lt;/a&gt; y sobre &lt;a href="https://juanchi.dev/es/blog/java-champion-2026-que-es-como-ser-reconocido" rel="noopener noreferrer"&gt;qué significa el camino a Java Champion&lt;/a&gt; que tocan la misma tensión desde otros ángulos.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Fuente original:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring Boot Actuator Docs: &lt;a href="https://docs.spring.io/spring-boot/reference/actuator/endpoints.html" rel="noopener noreferrer"&gt;https://docs.spring.io/spring-boot/reference/actuator/endpoints.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/actuator-env-sanitizar-valores-sensibles" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>springboot</category>
      <category>java</category>
    </item>
    <item>
      <title>depends_on Isn't Enough: service_healthy in Compose</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Wed, 09 Sep 2026 14:30:19 +0000</pubDate>
      <link>https://dev.to/jtorchia/dependson-isnt-enough-servicehealthy-in-compose-52g1</link>
      <guid>https://dev.to/jtorchia/dependson-isnt-enough-servicehealthy-in-compose-52g1</guid>
      <description>&lt;p&gt;I lost probably forty minutes on this one the first time, and I still see it happen to other people. You spin up &lt;code&gt;docker-compose up&lt;/code&gt; with a backend and a Postgres database. The backend has &lt;code&gt;depends_on: [db]&lt;/code&gt;. Everything starts, the backend throws &lt;code&gt;ECONNREFUSED&lt;/code&gt; two seconds in, the Postgres container is sitting there alive and well, and you start googling the error convinced it's a networking issue between services. It isn't. &lt;code&gt;depends_on&lt;/code&gt; on its own did exactly what it promises: it waited for the &lt;code&gt;db&lt;/code&gt; container to exist and be running. It never promised that Postgres inside it would be accepting connections.&lt;/p&gt;

&lt;p&gt;I wrote about this same misunderstanding recently from another angle — the gap between what a healthcheck tells Docker and what it tells an orchestrator like Kubernetes. This post goes one level deeper: into the &lt;code&gt;docker-compose.yml&lt;/code&gt; that anyone edits daily, no cluster, no readiness probes involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Compose healthcheck: what it solves and what it doesn't
&lt;/h2&gt;

&lt;p&gt;My thesis is simple: &lt;code&gt;depends_on&lt;/code&gt; without &lt;code&gt;condition&lt;/code&gt; is an illusion of order. It gives you container startup sequencing, not service availability sequencing. Only with &lt;code&gt;condition: service_healthy&lt;/code&gt; do you get something verifiable — Compose won't start the second service until the first one's &lt;code&gt;healthcheck&lt;/code&gt; reports &lt;code&gt;healthy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The source is the &lt;a href="https://docs.docker.com/compose/compose-file/05-services/#healthcheck" rel="noopener noreferrer"&gt;official Compose specification&lt;/a&gt;. It makes clear that &lt;code&gt;healthcheck&lt;/code&gt; defines a command Docker runs periodically inside the container, and that &lt;code&gt;depends_on&lt;/code&gt; accepts an object with &lt;code&gt;condition&lt;/code&gt; instead of just a list of service names. The valid conditions are &lt;code&gt;service_started&lt;/code&gt;, &lt;code&gt;service_healthy&lt;/code&gt;, and &lt;code&gt;service_completed_successfully&lt;/code&gt;. Without an explicit &lt;code&gt;condition&lt;/code&gt;, the default is &lt;code&gt;service_started&lt;/code&gt; — which is exactly the behavior that breaks expectations: container up, not necessarily the process inside responding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where people get it wrong
&lt;/h2&gt;

&lt;p&gt;The common recipe I see in repos and tutorials — and, if I'm honest, the one I copy-pasted myself before it bit me — is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It compiles, it starts, it "works" in the demo because Postgres usually boots fast on a laptop with an SSD. The hidden cost shows up in CI, on a slower machine, or when someone adds an &lt;code&gt;entrypoint.sh&lt;/code&gt; to the Postgres image that runs migrations before accepting connections. That's when the margin that "worked by luck" disappears and &lt;code&gt;api&lt;/code&gt; fails its first connection. It's not dramatic, it's just annoying: a flaky pipeline that passes eight times out of ten and makes you doubt your own code before you doubt the compose file.&lt;/p&gt;

&lt;p&gt;The counter-example that fixes this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;api&lt;/code&gt; doesn't start until &lt;code&gt;pg_isready&lt;/code&gt; returns success multiple times per &lt;code&gt;interval&lt;/code&gt; and &lt;code&gt;retries&lt;/code&gt;. That's the difference between "the container exists" and "the container is ready," and it's exactly the same concept as the readiness probe Kubernetes formalizes with &lt;code&gt;readinessProbe&lt;/code&gt; — but solved here at the local Compose level, no cluster needed.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  A[db starts] --&amp;gt; B{db healthcheck}
  B --&amp;gt;|starting/unhealthy| B
  B --&amp;gt;|healthy| C[api starts]
  C --&amp;gt; D{api healthcheck}
  D --&amp;gt;|healthy| E[stack ready]&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Decision matrix: when to condition and when not to
&lt;/h2&gt;

&lt;p&gt;Not every service needs this rigidity, and slapping &lt;code&gt;healthcheck&lt;/code&gt; on everything is its own kind of cargo cult. Here's the check I actually run before adding one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stateful service (DB, cache, broker) that another service connects to on boot&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;healthcheck&lt;/code&gt; + &lt;code&gt;condition: service_healthy&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Early connection failure is predictable and cheap to avoid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stateless service that only exposes HTTP and tolerates client-side retries&lt;/td&gt;
&lt;td&gt;Simple &lt;code&gt;depends_on&lt;/code&gt; or none&lt;/td&gt;
&lt;td&gt;The cost of waiting can be higher than the cost of retrying in the app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-shot migration job that runs and terminates&lt;/td&gt;
&lt;td&gt;&lt;code&gt;condition: service_completed_successfully&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What matters isn't "healthy," it's that it finished successfully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI environment with shared resources and slow startups&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;healthcheck&lt;/code&gt; with generous &lt;code&gt;retries&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A short timeout in CI produces false negatives you won't see locally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microservice that already handles reconnection with backoff in its own code&lt;/td&gt;
&lt;td&gt;Simple &lt;code&gt;depends_on&lt;/code&gt;, let the app retry&lt;/td&gt;
&lt;td&gt;Duplicating wait logic in Compose and in the code is redundant&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What you CAN'T conclude from this
&lt;/h2&gt;

&lt;p&gt;The Compose documentation doesn't tell you how long a &lt;code&gt;healthcheck&lt;/code&gt; with specific &lt;code&gt;interval&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;, and &lt;code&gt;retries&lt;/code&gt; values takes to converge in real production, because that depends on the image, the host, and the load — there's no universal number that fits every case. It also doesn't solve the problem of "service healthy but still can't handle real traffic" under load, which is different from "started up fine cold." And &lt;code&gt;depends_on&lt;/code&gt; with &lt;code&gt;condition&lt;/code&gt;, while it orders startup, isn't a continuous retry mechanism throughout the container's lifetime: if &lt;code&gt;db&lt;/code&gt; goes down after &lt;code&gt;api&lt;/code&gt; already started healthy, Compose doesn't go back to blocking anything. For that you need reconnection logic in the app's code, not in the &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Nor is it a substitute for Kubernetes's &lt;code&gt;readinessProbe&lt;/code&gt; and &lt;code&gt;livenessProbe&lt;/code&gt; if the final destination is a cluster: Compose solves the problem in local development or a staging &lt;code&gt;docker-compose up&lt;/code&gt;, but the production orchestration guarantee lives at another level, with other tools and other restart semantics. What I don't buy is treating a green &lt;code&gt;docker-compose up&lt;/code&gt; as proof that the same stack will behave in production — it proves ordering, nothing about load.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is &lt;code&gt;depends_on&lt;/code&gt; without &lt;code&gt;condition&lt;/code&gt; good for anything?&lt;/strong&gt;&lt;br&gt;
It's good for startup order and for making Compose stop containers in reverse order when you bring the stack down. It doesn't guarantee that the dependent service is ready to receive traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if the healthcheck never reaches &lt;code&gt;healthy&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
The service depending on it with &lt;code&gt;condition: service_healthy&lt;/code&gt; never starts, and Compose reports it as a dependency failure. That's preferable to a silent startup that fails at runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;service_healthy&lt;/code&gt; work with any image?&lt;/strong&gt;&lt;br&gt;
Only if the image defines a &lt;code&gt;healthcheck&lt;/code&gt; — either baked into the &lt;code&gt;Dockerfile&lt;/code&gt; or declared in the &lt;code&gt;docker-compose.yml&lt;/code&gt;. Without a defined healthcheck, Compose has no way to evaluate the condition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this replace Kubernetes readiness probes?&lt;/strong&gt;&lt;br&gt;
No. They're analogous concepts but they live at different layers. Compose's &lt;code&gt;healthcheck&lt;/code&gt; is for the world of local or simple self-hosted &lt;code&gt;docker-compose up&lt;/code&gt;; &lt;code&gt;readinessProbe&lt;/code&gt; is the equivalent piece when the destination is a Kubernetes cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many &lt;code&gt;retries&lt;/code&gt; or what &lt;code&gt;interval&lt;/code&gt; should I set?&lt;/strong&gt;&lt;br&gt;
There's no number that works for every case: it depends on the image and the environment. The sensible move is to start with conservative values, measure in your own CI pipeline or staging environment, and adjust based on what you observe there — not copy a value from a generic example.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is &lt;code&gt;condition: service_completed_successfully&lt;/code&gt; the same as &lt;code&gt;service_healthy&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;service_completed_successfully&lt;/code&gt; waits for the container to exit with a zero exit code, useful for one-shot migration jobs. &lt;code&gt;service_healthy&lt;/code&gt; waits for a healthcheck that keeps running while the container is alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;If a project's &lt;code&gt;docker-compose.yml&lt;/code&gt; has &lt;code&gt;depends_on&lt;/code&gt; as a plain list against a stateful service, there's a design flaw waiting for the worst possible moment to show up — probably in CI, with tighter resources than your dev laptop. The fix isn't complex: a well-defined &lt;code&gt;healthcheck&lt;/code&gt; and &lt;code&gt;condition: service_healthy&lt;/code&gt;. What I do ask is that you don't confuse this with a guarantee of continuous availability, nor with the equivalent of a production Kubernetes readiness probe. It's a guarantee of ordered startup in a local or simple staging environment, and that's how I treat it: useful, verifiable against the official source, and with limits worth knowing before you assume more than it delivers. The uncomfortable question worth asking your own repo today: does your compose file order containers, or does it order services actually being ready?&lt;/p&gt;

&lt;p&gt;If the stack is already using TanStack Query against Server Actions, it's worth checking how cache invalidation is handled in &lt;a href="https://juanchi.dev/en/blog/setquerydata-after-server-action-pattern" rel="noopener noreferrer"&gt;that post about setQueryData&lt;/a&gt;. And if the next step is Next.js with layered caching, the difference between &lt;code&gt;revalidatePath&lt;/code&gt; and &lt;code&gt;revalidateTag&lt;/code&gt; is covered in &lt;a href="https://juanchi.dev/en/blog/revalidatepath-vs-revalidatetag-nextjs-16" rel="noopener noreferrer"&gt;this other analysis&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Original source:&lt;/strong&gt; &lt;a href="https://docs.docker.com/compose/compose-file/05-services/#healthcheck" rel="noopener noreferrer"&gt;Docker Compose Spec — healthcheck&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/depends-on-not-enough-service-healthy-compose" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>docker</category>
      <category>devops</category>
      <category>dockercompose</category>
    </item>
    <item>
      <title>depends_on no basta: el service_healthy en Compose</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Wed, 09 Sep 2026 14:30:14 +0000</pubDate>
      <link>https://dev.to/jtorchia/dependson-no-basta-el-servicehealthy-en-compose-335b</link>
      <guid>https://dev.to/jtorchia/dependson-no-basta-el-servicehealthy-en-compose-335b</guid>
      <description>&lt;p&gt;Levantás un &lt;code&gt;docker-compose up&lt;/code&gt; con un backend y una base Postgres. El backend tiene &lt;code&gt;depends_on: [db]&lt;/code&gt;. Arranca todo, el backend tira &lt;code&gt;ECONNREFUSED&lt;/code&gt; a los dos segundos, el contenedor de Postgres sigue vivo, y googleás el error pensando que es un problema de red entre servicios. No lo es. Me pasó armando un stack chico para probar una migración: el contenedor de &lt;code&gt;db&lt;/code&gt; estaba "up" en el &lt;code&gt;docker ps&lt;/code&gt;, pero Postgres todavía estaba corriendo su inicialización interna y no aceptaba conexiones. &lt;code&gt;depends_on&lt;/code&gt; sin más hizo exactamente lo que promete: esperó a que el contenedor de &lt;code&gt;db&lt;/code&gt; existiera y estuviera corriendo. Nunca prometió que Postgres adentro estuviera aceptando conexiones.&lt;/p&gt;

&lt;p&gt;Ese malentendido es viejo y lo escribí hace poco desde otro ángulo — la diferencia entre lo que un healthcheck le dice a Docker y lo que le dice a un orquestador como Kubernetes. Este post baja un nivel más: al &lt;code&gt;docker-compose.yml&lt;/code&gt; que cualquiera edita a diario, sin cluster, sin readiness probes de Kubernetes de por medio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Compose healthcheck: qué resuelve y qué no
&lt;/h2&gt;

&lt;p&gt;Mi tesis es simple: &lt;code&gt;depends_on&lt;/code&gt; sin &lt;code&gt;condition&lt;/code&gt; es una ilusión de orden. Te da secuencia de arranque de contenedores, no secuencia de disponibilidad de servicio. Con &lt;code&gt;condition: service_healthy&lt;/code&gt; recién ahí tenés algo verificable — Compose no arranca el segundo servicio hasta que el &lt;code&gt;healthcheck&lt;/code&gt; del primero reporte &lt;code&gt;healthy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Lo incómodo es que esta confusión no es un error de configuración raro: es el comportamiento default de Compose, y por eso aparece en tantos repos que "andan bien" hasta que dejan de andar.&lt;/p&gt;

&lt;p&gt;La fuente es la &lt;a href="https://docs.docker.com/compose/compose-file/05-services/#healthcheck" rel="noopener noreferrer"&gt;especificación oficial de Compose&lt;/a&gt;. Ahí queda claro que &lt;code&gt;healthcheck&lt;/code&gt; define un comando que Docker ejecuta periódicamente dentro del contenedor, y que &lt;code&gt;depends_on&lt;/code&gt; acepta un objeto con &lt;code&gt;condition&lt;/code&gt; en vez de solo una lista de nombres de servicios. Las condiciones válidas son &lt;code&gt;service_started&lt;/code&gt;, &lt;code&gt;service_healthy&lt;/code&gt; y &lt;code&gt;service_completed_successfully&lt;/code&gt;. Sin &lt;code&gt;condition&lt;/code&gt; explícita, el default es &lt;code&gt;service_started&lt;/code&gt;, que es exactamente el comportamiento que rompe expectativas: contenedor arriba, no necesariamente el proceso adentro respondiendo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dónde se equivoca la gente
&lt;/h2&gt;

&lt;p&gt;La receta común que veo en repos y en tutoriales es esta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compila, arranca, "funciona" en la demo porque Postgres suele levantar rápido en una laptop con SSD. El costo oculto aparece en CI, en una máquina más lenta, o cuando alguien le agrega un &lt;code&gt;entrypoint.sh&lt;/code&gt; a la imagen de Postgres que corre migraciones antes de aceptar conexiones. Ahí el margen que "funcionaba por suerte" desaparece y el &lt;code&gt;api&lt;/code&gt; falla la primera conexión.&lt;/p&gt;

&lt;p&gt;El contraejemplo que corrige esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahora &lt;code&gt;api&lt;/code&gt; no arranca hasta que &lt;code&gt;pg_isready&lt;/code&gt; devuelva éxito varias veces según &lt;code&gt;interval&lt;/code&gt; y &lt;code&gt;retries&lt;/code&gt;. Es la diferencia entre "el contenedor existe" y "el contenedor está listo", y es exactamente el mismo concepto de readiness probe que Kubernetes formaliza con &lt;code&gt;readinessProbe&lt;/code&gt; — pero acá resuelto a nivel de Compose local, sin necesidad de un cluster.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  A[db arranca] --&amp;gt; B{healthcheck de db}
  B --&amp;gt;|starting/unhealthy| B
  B --&amp;gt;|healthy| C[api arranca]
  C --&amp;gt; D{healthcheck de api}
  D --&amp;gt;|healthy| E[stack listo]&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Matriz de decisión: cuándo condicionar y cuándo no
&lt;/h2&gt;

&lt;p&gt;No todo servicio necesita esta rigidez. Antes de agregar &lt;code&gt;healthcheck&lt;/code&gt; y &lt;code&gt;condition: service_healthy&lt;/code&gt; en cada línea, conviene mirar esto:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situación&lt;/th&gt;
&lt;th&gt;Qué hacer&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;Servicio con estado (DB, cache, broker) al que otro se conecta al boot&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;healthcheck&lt;/code&gt; + &lt;code&gt;condition: service_healthy&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;El fallo de conexión temprana es predecible y barato de evitar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Servicio sin estado que solo expone HTTP y tolera reintentos en el cliente&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;depends_on&lt;/code&gt; simple o ninguno&lt;/td&gt;
&lt;td&gt;El costo de esperar puede ser mayor que el de reintentar en la app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job de migración que corre una vez y termina&lt;/td&gt;
&lt;td&gt;&lt;code&gt;condition: service_completed_successfully&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No es "healthy" lo que importa, es que terminó bien&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entorno de CI con recursos compartidos y arranques lentos&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;healthcheck&lt;/code&gt; con &lt;code&gt;retries&lt;/code&gt; generosos&lt;/td&gt;
&lt;td&gt;Un timeout corto en CI genera falsos negativos que no ves en local&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microservicio que ya maneja reconexión con backoff en su propio código&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;depends_on&lt;/code&gt; simple, dejar que la app reintente&lt;/td&gt;
&lt;td&gt;Duplicar la lógica de espera en Compose y en el código es redundante&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Qué NO se puede concluir de esto
&lt;/h2&gt;

&lt;p&gt;La documentación de Compose no dice cuánto tarda en converger un &lt;code&gt;healthcheck&lt;/code&gt; con valores específicos de &lt;code&gt;interval&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt; y &lt;code&gt;retries&lt;/code&gt; en producción real, porque eso depende de la imagen, del host y de la carga — no hay un número universal que valga para todos los casos. Tampoco resuelve el problema de "servicio healthy pero todavía no puede atender tráfico real" bajo carga, que es distinto de "arrancó bien en frío". Y &lt;code&gt;depends_on&lt;/code&gt; con &lt;code&gt;condition&lt;/code&gt;, aunque ordena el arranque, no es un mecanismo de reintento continuo durante la vida del contenedor: si &lt;code&gt;db&lt;/code&gt; se cae después de que &lt;code&gt;api&lt;/code&gt; ya arrancó healthy, Compose no vuelve a bloquear nada. Para eso hace falta la lógica de reconexión en el código de la app, no en el &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Tampoco es un sustituto de &lt;code&gt;readinessProbe&lt;/code&gt; y &lt;code&gt;livenessProbe&lt;/code&gt; de Kubernetes si el destino final es un cluster: Compose resuelve el problema en desarrollo local o en un &lt;code&gt;docker-compose up&lt;/code&gt; de staging, pero la garantía de orquestación productiva vive en otro nivel, con otras herramientas y otras semánticas de reinicio.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿&lt;code&gt;depends_on&lt;/code&gt; sin &lt;code&gt;condition&lt;/code&gt; sirve para algo?&lt;/strong&gt;&lt;br&gt;
Sirve para el orden de arranque y para que Compose pare los contenedores en el orden inverso al bajar el stack. No sirve para garantizar que el servicio dependido esté listo para recibir tráfico.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué pasa si el healthcheck nunca llega a &lt;code&gt;healthy&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
El servicio que depende de él con &lt;code&gt;condition: service_healthy&lt;/code&gt; no arranca, y Compose lo reporta como fallo de dependencia. Prefiero eso mil veces a un arranque silencioso que falla en runtime y te hace perder media hora googleando un &lt;code&gt;ECONNREFUSED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿&lt;code&gt;service_healthy&lt;/code&gt; funciona con cualquier imagen?&lt;/strong&gt;&lt;br&gt;
Solo si la imagen define un &lt;code&gt;healthcheck&lt;/code&gt; — propio en el &lt;code&gt;Dockerfile&lt;/code&gt; o declarado en el &lt;code&gt;docker-compose.yml&lt;/code&gt;. Sin healthcheck definido, Compose no tiene manera de evaluar la condición.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Esto reemplaza los readiness probes de Kubernetes?&lt;/strong&gt;&lt;br&gt;
No. Son conceptos análogos pero viven en capas distintas. &lt;code&gt;healthcheck&lt;/code&gt; de Compose es para el mundo de &lt;code&gt;docker-compose up&lt;/code&gt; local o self-hosted simple; &lt;code&gt;readinessProbe&lt;/code&gt; es la pieza equivalente cuando el destino es un cluster de Kubernetes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cuántos &lt;code&gt;retries&lt;/code&gt; o qué &lt;code&gt;interval&lt;/code&gt; conviene poner?&lt;/strong&gt;&lt;br&gt;
No hay un número que sirva para todos los casos: depende de la imagen y el entorno. Lo prudente es arrancar con valores conservadores, medir en el propio pipeline de CI o entorno de staging, y ajustar según lo que se observe ahí — no copiar un valor de un ejemplo genérico.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿&lt;code&gt;condition: service_completed_successfully&lt;/code&gt; es lo mismo que &lt;code&gt;service_healthy&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;service_completed_successfully&lt;/code&gt; espera a que el contenedor termine con código de salida cero, útil para jobs de migración one-shot. &lt;code&gt;service_healthy&lt;/code&gt; espera a un healthcheck que sigue corriendo mientras el contenedor vive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postura final
&lt;/h2&gt;

&lt;p&gt;Si tu &lt;code&gt;docker-compose.yml&lt;/code&gt; tiene &lt;code&gt;depends_on&lt;/code&gt; como lista simple contra un servicio con estado, hay una falla de diseño esperando el momento menos oportuno para aparecer — probablemente en CI, con recursos más ajustados que en tu laptop. La corrección no es compleja: un &lt;code&gt;healthcheck&lt;/code&gt; bien definido y &lt;code&gt;condition: service_healthy&lt;/code&gt;. Lo que sí pido es que no lo confundas con una garantía de disponibilidad continua ni con el equivalente de un readiness probe de Kubernetes en producción. Es una garantía de arranque ordenado en un entorno local o de staging simple, y como tal hay que tratarla: útil, verificable contra la fuente oficial, y con límites que conviene conocer antes de asumir más de lo que da. Mi límite personal: si el proyecto ya tiene un cluster de verdad, dejo de pelear con Compose y muevo la garantía de readiness a donde corresponde.&lt;/p&gt;

&lt;p&gt;Si el stack ya usa TanStack Query contra Server Actions, vale la pena revisar cómo se maneja la invalidación de cache en &lt;a href="https://juanchi.dev/es/blog/tanstack-query-server-actions-setquerydata-invalidacion" rel="noopener noreferrer"&gt;ese post sobre setQueryData&lt;/a&gt;. Y si el próximo paso es Next.js con cache en capas, la diferencia entre &lt;code&gt;revalidatePath&lt;/code&gt; y &lt;code&gt;revalidateTag&lt;/code&gt; está detallada en &lt;a href="https://juanchi.dev/es/blog/revalidatepath-vs-revalidatetag-nextjs-cache" rel="noopener noreferrer"&gt;este otro análisis&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fuente original:&lt;/strong&gt; &lt;a href="https://docs.docker.com/compose/compose-file/05-services/#healthcheck" rel="noopener noreferrer"&gt;Docker Compose Spec — healthcheck&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/docker-compose-healthcheck-depends-on-service-healthy" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>docker</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
