<?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>Actuator Endpoints in Spring Boot: Allowlist, Don't Just Disable the Obvious Ones</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Sun, 23 Aug 2026 12:00:21 +0000</pubDate>
      <link>https://dev.to/jtorchia/actuator-endpoints-in-spring-boot-allowlist-dont-just-disable-the-obvious-ones-2kpf</link>
      <guid>https://dev.to/jtorchia/actuator-endpoints-in-spring-boot-allowlist-dont-just-disable-the-obvious-ones-2kpf</guid>
      <description>&lt;p&gt;A &lt;code&gt;curl&lt;/code&gt; to &lt;code&gt;/actuator/env&lt;/code&gt; on a Spring Boot backend with default configuration can return environment variables, system properties, and — in some versions and setups — datasource values. No credentials needed. No exploit needed. All it takes is nobody touching Actuator's security config after adding it to the &lt;code&gt;pom.xml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's what I want to pick apart today: what Actuator exposes by default, which endpoints are structurally risky, and why the "I'll disable the ones that scare me" recipe is worse than having no strategy at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem behind "actuator endpoints spring boot"
&lt;/h2&gt;

&lt;p&gt;When someone googles "actuator endpoints spring boot," they're usually in one of two moments: they're adding the starter for the first time and want to know what turns on, or they're staring at a pentest/audit report that flagged an endpoint as exposed and need to understand why.&lt;/p&gt;

&lt;p&gt;In both cases the underlying problem is the same: Actuator was built to give operational visibility — health checks, metrics, build info — but several of its endpoints return information that should never leave the internal network. The default configuration doesn't make that distinction. It distinguishes between "web-exposed" and "not," with criteria designed for development, not production.&lt;/p&gt;

&lt;p&gt;My take is simple and not subtle: Actuator with default configuration is attack surface that gets overlooked all the time, and the right way to close it isn't turning off the endpoints that "sound dangerous" by gut feeling. It's defining an explicit allowlist of what gets exposed, with everything else closed by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the official docs say (and what they don't)
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html" rel="noopener noreferrer"&gt;official Spring Boot Actuator documentation&lt;/a&gt; is clear on a point a lot of people don't read all the way to: since Spring Boot 2, only &lt;code&gt;/health&lt;/code&gt; is exposed over HTTP by default. The rest of the endpoints exist but aren't web-exposed until you turn them on with &lt;code&gt;management.endpoints.web.exposure.include&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That sounds reassuring. The problem shows up when a team, trying to solve an observability pain point, does what most tutorials show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Lo que copian de un tutorial sin pensarlo dos veces
&lt;/span&gt;&lt;span class="py"&gt;management.endpoints.web.exposure.include&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That asterisk exposes &lt;strong&gt;every&lt;/strong&gt; registered endpoint, including &lt;code&gt;env&lt;/code&gt;, &lt;code&gt;beans&lt;/code&gt;, &lt;code&gt;configprops&lt;/code&gt;, &lt;code&gt;heapdump&lt;/code&gt;, and &lt;code&gt;threaddump&lt;/code&gt;. The docs do warn about it, but in a section separate from the one showing how to enable endpoints — and copy-paste habits don't respect section boundaries.&lt;/p&gt;

&lt;p&gt;What the official docs &lt;strong&gt;don't&lt;/strong&gt; say — because it's not their job to say it — is which combination of endpoints represents real risk in a system with production data. That's architecture judgment, not framework configuration. That's the gap this post is trying to close.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where people get it wrong: the "I'll just disable the obvious ones" recipe
&lt;/h2&gt;

&lt;p&gt;The most common recipe I see — and it makes sense as a first pass — goes like this: someone scans the list of endpoints, spots the ones that sound dangerous by name (&lt;code&gt;env&lt;/code&gt;, &lt;code&gt;shutdown&lt;/code&gt;, &lt;code&gt;heapdump&lt;/code&gt;), and disables them one by one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Receta comun: deshabilitar lo que "suena" peligroso
&lt;/span&gt;&lt;span class="py"&gt;management.endpoint.env.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;management.endpoint.shutdown.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;management.endpoint.heapdump.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;management.endpoints.web.exposure.include&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hidden cost of this recipe is that it still starts from total exposure (&lt;code&gt;include=*&lt;/code&gt;) and subtracts from there. Every new endpoint Spring Boot adds in a future version, every dependency that registers its own Actuator endpoint (some third-party libraries do), stays exposed by default until someone finds out and adds it to the blacklist.&lt;/p&gt;

&lt;p&gt;The comparison I keep coming back to: it's the difference between a firewall that blocks known ports and one that only allows the ports you actually need. The first protects you from threats you already know about. The second protects you from the ones that don't exist yet either.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/env&lt;/code&gt; is the most-cited case because the damage is direct and easy to demonstrate: it returns the full &lt;code&gt;PropertySource&lt;/code&gt; tree, which in real configs includes database credentials, tokens for external services, and app secrets if &lt;code&gt;management.endpoint.env.keys-to-sanitize&lt;/code&gt; wasn't set (or whatever sanitization mechanism applies to your version). &lt;code&gt;/heapdump&lt;/code&gt; is arguably worse: a full memory dump can contain strings with active session tokens, which connects directly to &lt;a href="https://juanchi.dev/en/blog/stateless-jwt-vs-stateful-sessions-identity-systems" rel="noopener noreferrer"&gt;how to think about sessions and digital identity&lt;/a&gt; — if those sessions live in process memory, a leaked heapdump exposes them just as much as a token stolen through XSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The explicit allowlist: a decision matrix
&lt;/h2&gt;

&lt;p&gt;Instead of starting from "everything open, subtract what scares me," the alternative is to start from "everything closed, add what I can justify":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Allowlist explicita: arranca cerrado, se abre por necesidad
&lt;/span&gt;&lt;span class="py"&gt;management.endpoints.web.exposure.include&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;health,info&lt;/span&gt;
&lt;span class="py"&gt;management.endpoint.health.show-details&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;when-authorized&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On top of that minimal baseline, adding each extra endpoint becomes a case-by-case decision. Here's the matrix I use to evaluate each one before adding it to the allowlist:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Exposed by default&lt;/th&gt;
&lt;th&gt;Risk if leaked&lt;/th&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes, on Boot 2+&lt;/td&gt;
&lt;td&gt;Low (with &lt;code&gt;show-details&lt;/code&gt; restricted)&lt;/td&gt;
&lt;td&gt;Leave open, but no details for unauthenticated users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Useful for build version; check it doesn't include sensitive metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;env&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;High — can leak secrets and credentials&lt;/td&gt;
&lt;td&gt;Behind auth only, never public, with sanitization active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;metrics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Medium — can leak internal topology&lt;/td&gt;
&lt;td&gt;Restrict to internal network or ops auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;heapdump&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;High — full process memory&lt;/td&gt;
&lt;td&gt;Never exposed over the web; local/SSH access only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shutdown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No, and must be explicitly enabled&lt;/td&gt;
&lt;td&gt;Critical — kills the process&lt;/td&gt;
&lt;td&gt;Don't enable in production except behind a controlled orchestrator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loggers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Medium — allows changing log level at runtime&lt;/td&gt;
&lt;td&gt;Behind auth with an ops role&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every row in this table is a guideline, not an absolute rule: &lt;code&gt;metrics&lt;/code&gt; can be perfectly public on a system with no sensitive data in metric tags, and &lt;code&gt;health&lt;/code&gt; with full details can be fine if it only runs on the internal network. The point isn't to memorize the table. It's to ask yourself "what happens if someone unauthenticated sees this?" for every endpoint before it goes into &lt;code&gt;include&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protecting what you do expose with Spring Security
&lt;/h2&gt;

&lt;p&gt;Once the allowlist is defined, the second common mistake is assuming "being on the allowlist" means "being protected." Spring Security lets you split Actuator's path off from the rest of the app and apply its own rules:&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 tipica: reglas distintas para actuator vs resto de la app&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;SecurityFilterChain&lt;/span&gt; &lt;span class="nf"&gt;actuatorSecurity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpSecurity&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;securityMatcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EndpointRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toAnyEndpoint&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EndpointRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"health"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"info"&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;anyRequest&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;hasRole&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OPS"&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;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&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;&lt;code&gt;EndpointRequest.to(...)&lt;/code&gt; is the matcher Spring Boot provides specifically for this — it saves you from hand-mapping Actuator paths and having them break every time &lt;code&gt;management.endpoints.web.base-path&lt;/code&gt; changes. The combination matters: the allowlist defines &lt;strong&gt;what exists&lt;/strong&gt;, Spring Security defines &lt;strong&gt;who can see it&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A[Request a /actuator/algo] --&amp;gt; B{¿Esta en el include?}
  B --&amp;gt;|no| C[404, no existe]
  B --&amp;gt;|si| D{¿Pasa Spring Security?}
  D --&amp;gt;|no| E[401/403]
  D --&amp;gt;|si| F[Respuesta del endpoint]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;This matrix is architecture judgment, not a measured result from a specific system. I don't have real incident metrics to cite, and I'm not going to make them up: there's no public evidence of concrete cases in this post beyond the official Spring Boot documentation linked above. What that source does let you state with confidence is the documented default behavior (only &lt;code&gt;health&lt;/code&gt; exposed on Boot 2+, everything else needs explicit &lt;code&gt;include&lt;/code&gt;) and the exposure mechanism via &lt;code&gt;management.endpoints.web.exposure&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What you can't conclude without running your own experiment: the exact impact of exposing &lt;code&gt;env&lt;/code&gt; on a system with your specific secrets, how sanitization behaves in your particular Boot version (it's changed across versions, so check the changelog for the one you're running), or whether a WAF/proxy in front already mitigates part of the risk before the request even reaches the app. If the goal is a formal audit, the sensible move is running an exposed-endpoints scanner against a staging environment, not assuming theory is enough.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Does Actuator come enabled by default in a Spring Boot project?&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;spring-boot-starter-actuator&lt;/code&gt; dependency does register the endpoints once added, but default web exposure on Boot 2+ is limited to &lt;code&gt;/health&lt;/code&gt;. Everything else needs explicit &lt;code&gt;management.endpoints.web.exposure.include&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is &lt;code&gt;/env&lt;/code&gt; the most-cited endpoint in Actuator security discussions?&lt;/strong&gt;&lt;br&gt;
Because it returns the full tree of the process's property sources, and in real-world configs that includes variables with credentials or tokens if key sanitization wasn't turned on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it enough to just disable &lt;code&gt;env&lt;/code&gt; and &lt;code&gt;heapdump&lt;/code&gt; individually?&lt;/strong&gt;&lt;br&gt;
Not as a long-term strategy. Any new endpoint (from Boot or from a third-party dependency) stays exposed by default if the baseline is still &lt;code&gt;include=*&lt;/code&gt;. The allowlist flips that logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Spring Security mandatory to run Actuator in production?&lt;/strong&gt;&lt;br&gt;
Not at the framework level — Spring Boot doesn't force it. But without an authorization layer sitting in front of Actuator, anything listed in &lt;code&gt;include&lt;/code&gt; is reachable by anyone who knows the URL, credentials or no credentials. That's a design gap in the default setup, not a documented vulnerability, and &lt;code&gt;EndpointRequest&lt;/code&gt; exists precisely to close it without hand-rolling path matchers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is &lt;code&gt;health&lt;/code&gt; with full details safe to expose publicly?&lt;/strong&gt;&lt;br&gt;
Depends on what's in those details. &lt;code&gt;management.endpoint.health.show-details=when-authorized&lt;/code&gt; is the sensible choice when you can't guarantee only internal traffic reaches the endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I check which endpoints are exposed on an already-running environment?&lt;/strong&gt;&lt;br&gt;
A direct &lt;code&gt;curl&lt;/code&gt; to &lt;code&gt;/actuator&lt;/code&gt; (no sub-path) usually lists the active endpoints if &lt;code&gt;discovery&lt;/code&gt; is enabled — which by itself is information worth reviewing before it's exposed.&lt;/p&gt;

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

&lt;p&gt;If the criterion for deciding which Actuator endpoints to expose is "I'll disable the ones that sound dangerous," the system is going to end up exposed by the next endpoint Spring Boot adds, the next dependency that registers one, or the next dev who runs &lt;code&gt;include=*&lt;/code&gt; copying an old tutorial. An explicit allowlist plus Spring Security in front of it isn't the easiest option to start with, but it's the only one that doesn't depend on someone remembering to update a blacklist.&lt;/p&gt;

&lt;p&gt;The concrete next step, if this hits home for a real system: go check &lt;code&gt;management.endpoints.web.exposure.include&lt;/code&gt; right now, not after the next pentest. And while you're at it, if the system keeps sessions or tokens in memory, check how exposed &lt;code&gt;/heapdump&lt;/code&gt; is too — the connection with &lt;a href="https://juanchi.dev/en/blog/stateless-jwt-vs-stateful-sessions-identity-systems" rel="noopener noreferrer"&gt;stateless JWT vs stateful sessions&lt;/a&gt; isn't a coincidence: Actuator's exposure surface and the identity model you picked end up talking about the same thing — how easy it is to steal a session without stealing a password.&lt;/p&gt;

&lt;p&gt;This post is a configuration guide, not an audit of a specific system — if you want something closer to how I think about dev tools with deliberate limits, &lt;a href="https://juanchi.dev/en/blog/cline-vs-code-autonomous-ai-agent-deliberate-limits" rel="noopener noreferrer"&gt;the piece on Cline in VS Code&lt;/a&gt; follows the same logic of "full capability by default, explicit restriction after."&lt;/p&gt;

&lt;p&gt;Original source: &lt;a href="https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html" rel="noopener noreferrer"&gt;Spring Boot Actuator Docs&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/actuator-endpoints-spring-boot-allowlist-security" 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>Actuator endpoints en Spring Boot: allowlist, no deshabilitar lo obvio</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Sun, 23 Aug 2026 12:00:16 +0000</pubDate>
      <link>https://dev.to/jtorchia/actuator-endpoints-en-spring-boot-allowlist-no-deshabilitar-lo-obvio-4m0k</link>
      <guid>https://dev.to/jtorchia/actuator-endpoints-en-spring-boot-allowlist-no-deshabilitar-lo-obvio-4m0k</guid>
      <description>&lt;p&gt;Un &lt;code&gt;curl&lt;/code&gt; a &lt;code&gt;/actuator/env&lt;/code&gt; en un backend Spring Boot con configuración default puede devolver variables de entorno, propiedades del sistema y —en algunas versiones y configuraciones— valores de datasource. No hace falta credencial. No hace falta exploit. Hace falta que nadie haya tocado la configuración de seguridad de Actuator después de agregarlo al &lt;code&gt;pom.xml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Esto es lo que quiero desarmar: qué expone Actuator por defecto, qué endpoints son estructuralmente riesgosos, y por qué la receta de "deshabilito los que me dan miedo" es peor que no tener criterio ninguno.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema real detrás de "actuator endpoints spring boot"
&lt;/h2&gt;

&lt;p&gt;Cuando alguien busca "actuator endpoints spring boot" en general está en uno de dos momentos: está agregando el starter por primera vez y quiere saber qué prende, o está mirando un pentest/auditoría que marcó un endpoint como expuesto y necesita entender por qué.&lt;/p&gt;

&lt;p&gt;En los dos casos el problema de fondo es el mismo: Actuator nació para dar visibilidad operacional —health checks, métricas, info de build— pero varios de sus endpoints devuelven información que nunca debería salir de la red interna. La configuración por defecto no distingue eso. Distingue entre "web-exposed" y "no", con un criterio pensado para desarrollo, no para producción.&lt;/p&gt;

&lt;p&gt;Mi tesis es simple y no es sutil: Actuator con la configuración default es superficie de ataque que se pasa por alto todo el tiempo, y la forma correcta de cerrarla no es apagar los endpoints que "suenan peligrosos" a ojo. Es definir una allowlist explícita de lo que se expone, y todo lo demás queda cerrado por default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué dice la documentación oficial (y qué no dice)
&lt;/h2&gt;

&lt;p&gt;La &lt;a href="https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html" rel="noopener noreferrer"&gt;documentación oficial de Spring Boot Actuator&lt;/a&gt; es clara en un punto que mucha gente no lee hasta el final: desde Spring Boot 2, solo &lt;code&gt;/health&lt;/code&gt; está expuesto por HTTP por defecto. El resto de los endpoints existen pero no están expuestos vía web hasta que los habilitás con &lt;code&gt;management.endpoints.web.exposure.include&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Eso suena tranquilizador. El problema aparece cuando un equipo, buscando resolver un dolor de observabilidad, hace lo que la mayoría de los tutoriales muestran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Lo que copian de un tutorial sin pensarlo dos veces
&lt;/span&gt;&lt;span class="py"&gt;management.endpoints.web.exposure.include&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ese asterisco expone &lt;strong&gt;todos&lt;/strong&gt; los endpoints registrados, incluidos &lt;code&gt;env&lt;/code&gt;, &lt;code&gt;beans&lt;/code&gt;, &lt;code&gt;configprops&lt;/code&gt;, &lt;code&gt;heapdump&lt;/code&gt; y &lt;code&gt;threaddump&lt;/code&gt;. La documentación lo advierte, pero en una sección separada de la que muestra cómo habilitar endpoints — y el patrón de copiar-pegar no distingue secciones.&lt;/p&gt;

&lt;p&gt;Lo que la documentación oficial &lt;strong&gt;no dice&lt;/strong&gt; —porque no es su trabajo decirlo— es qué combinación de endpoints representa riesgo real en un sistema con datos de producción. Eso es criterio de arquitectura, no configuración de framework. Ahí es donde entra la decisión que defiendo en este post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dónde se equivoca la gente: la receta de "deshabilito los obvios"
&lt;/h2&gt;

&lt;p&gt;La receta más común que veo —y que tiene sentido en un primer approach— es esta: alguien revisa la lista de endpoints, identifica los que suenan peligrosos por nombre (&lt;code&gt;env&lt;/code&gt;, &lt;code&gt;shutdown&lt;/code&gt;, &lt;code&gt;heapdump&lt;/code&gt;) y los deshabilita puntualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Receta comun: deshabilitar lo que "suena" peligroso
&lt;/span&gt;&lt;span class="py"&gt;management.endpoint.env.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;management.endpoint.shutdown.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;management.endpoint.heapdump.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;management.endpoints.web.exposure.include&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El costo oculto de esta receta es que sigue partiendo de una exposición total (&lt;code&gt;include=*&lt;/code&gt;) y resta desde ahí. Cada endpoint nuevo que Spring Boot agregue en una versión futura, cada dependencia que registre su propio endpoint de Actuator (algunas librerías de terceros lo hacen), queda expuesto por default hasta que alguien se entere y lo agregue a la lista negra.&lt;/p&gt;

&lt;p&gt;El contraejemplo que suelo usar para explicar esto: es la diferencia entre un firewall que bloquea puertos conocidos y uno que permite solo los puertos que necesitás. El primero te protege de las amenazas que ya conocés. El segundo te protege también de las que todavía no existen.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/env&lt;/code&gt; es el caso más citado porque el daño es directo y fácil de demostrar: devuelve el árbol completo de &lt;code&gt;PropertySource&lt;/code&gt;, que en configuraciones reales incluye credenciales de base de datos, tokens de servicios externos y secrets de aplicación si no se usó &lt;code&gt;management.endpoint.env.keys-to-sanitize&lt;/code&gt; (o el mecanismo de sanitización correspondiente a la versión). Pero &lt;code&gt;/heapdump&lt;/code&gt; es igual o más grave: un volcado de memoria completo puede contener strings con tokens de sesión activos, algo que conecta directo con &lt;a href="https://juanchi.dev/es/blog/jwt-vs-sesiones-con-estado-identidad-digital-criterio" rel="noopener noreferrer"&gt;cómo pensar sesiones e identidad digital&lt;/a&gt; — si esas sesiones viven en memoria del proceso, un heapdump filtrado las expone tanto como un token robado por XSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  La allowlist explícita: matriz de decisión
&lt;/h2&gt;

&lt;p&gt;En vez de partir de "todo abierto, resto lo que asusta", la alternativa es partir de "todo cerrado, agrego lo que necesito justificar":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Allowlist explicita: arranca cerrado, se abre por necesidad
&lt;/span&gt;&lt;span class="py"&gt;management.endpoints.web.exposure.include&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;health,info&lt;/span&gt;
&lt;span class="py"&gt;management.endpoint.health.show-details&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;when-authorized&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sobre esa base mínima, la decisión de agregar cada endpoint adicional se toma caso por caso. Esta es la matriz de criterio que uso para evaluar cada uno antes de sumarlo a la allowlist:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Expone por defecto&lt;/th&gt;
&lt;th&gt;Riesgo si se filtra&lt;/th&gt;
&lt;th&gt;Criterio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sí, en Boot 2+&lt;/td&gt;
&lt;td&gt;Bajo (con &lt;code&gt;show-details&lt;/code&gt; restringido)&lt;/td&gt;
&lt;td&gt;Dejarlo abierto, pero sin detalles a usuarios no autenticados&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Bajo&lt;/td&gt;
&lt;td&gt;Útil para versión de build; revisar que no incluya metadata sensible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;env&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Alto — puede filtrar secrets y credenciales&lt;/td&gt;
&lt;td&gt;Solo detrás de auth, nunca público, con sanitización activa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;metrics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Medio — puede filtrar topología interna&lt;/td&gt;
&lt;td&gt;Restringir a red interna o auth de operaciones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;heapdump&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Alto — memoria completa del proceso&lt;/td&gt;
&lt;td&gt;Nunca expuesto vía web; solo acceso local/SSH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shutdown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No, y requiere habilitarlo explícitamente&lt;/td&gt;
&lt;td&gt;Crítico — apaga el proceso&lt;/td&gt;
&lt;td&gt;No habilitarlo en producción salvo orquestador controlado&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loggers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Medio — permite cambiar nivel de log en runtime&lt;/td&gt;
&lt;td&gt;Detrás de auth con rol de operaciones&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cada fila de esta tabla es un criterio, no una regla absoluta: &lt;code&gt;metrics&lt;/code&gt; puede ser perfectamente público en un sistema sin datos sensibles en las etiquetas de métricas, y &lt;code&gt;health&lt;/code&gt; con detalles completos puede ser aceptable si corre solo en red interna. El punto no es memorizar la tabla. Es hacerte la pregunta "¿qué pasa si esto lo ve alguien sin autenticar?" para cada endpoint antes de sumarlo al &lt;code&gt;include&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protegiendo lo que sí exponés con Spring Security
&lt;/h2&gt;

&lt;p&gt;Una vez que la allowlist está definida, el segundo error común es asumir que "estar en la allowlist" es lo mismo que "estar protegido". Spring Security permite separar el path de Actuator del resto de la aplicación y aplicarle reglas propias:&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 tipica: reglas distintas para actuator vs resto de la app&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;SecurityFilterChain&lt;/span&gt; &lt;span class="nf"&gt;actuatorSecurity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpSecurity&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;securityMatcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EndpointRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toAnyEndpoint&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EndpointRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"health"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"info"&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;anyRequest&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;hasRole&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OPS"&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;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&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;&lt;code&gt;EndpointRequest.to(...)&lt;/code&gt; es el matcher que Spring Boot provee específicamente para esto — evita tener que mapear paths de Actuator a mano y romperlos cada vez que cambia &lt;code&gt;management.endpoints.web.base-path&lt;/code&gt;. La combinación importa: la allowlist define &lt;strong&gt;qué existe&lt;/strong&gt;, Spring Security define &lt;strong&gt;quién puede verlo&lt;/strong&gt;. Sin esa segunda capa, cualquier endpoint que esté en el &lt;code&gt;include&lt;/code&gt; queda accesible a quien conozca la URL — no porque el framework lo obligue, sino porque nadie puso una capa de autorización delante.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A[Request a /actuator/algo] --&amp;gt; B{¿Esta en el include?}
  B --&amp;gt;|no| C[404, no existe]
  B --&amp;gt;|si| D{¿Pasa Spring Security?}
  D --&amp;gt;|no| E[401/403]
  D --&amp;gt;|si| F[Respuesta del endpoint]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Límites de esta guía
&lt;/h2&gt;

&lt;p&gt;Esta matriz es criterio de arquitectura, no un resultado medido en un sistema específico. No tengo métricas de incidentes reales para citar, y no las voy a inventar: no hay evidencia pública de casos concretos en este post, más allá de la documentación oficial de Spring Boot enlazada arriba. Lo que sí se puede afirmar con esa fuente es el comportamiento default documentado (solo &lt;code&gt;health&lt;/code&gt; expuesto en Boot 2+, el resto requiere &lt;code&gt;include&lt;/code&gt; explícito) y el mecanismo de exposición vía &lt;code&gt;management.endpoints.web.exposure&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Lo que no se puede concluir sin un experimento propio: el impacto exacto de exponer &lt;code&gt;env&lt;/code&gt; en un sistema con secrets particulares, el comportamiento de sanitización en cada versión puntual de Boot (cambió entre versiones, así que conviene revisar el changelog de la versión en uso), o si un WAF/proxy delante ya mitiga parte del riesgo antes de que la request llegue a la aplicación. Si el objetivo es una auditoría formal, la recomendación prudente es correr un scanner de endpoints expuestos contra un ambiente de staging, no asumir que la teoría alcanza.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;¿Actuator viene habilitado por defecto en un proyecto Spring Boot?&lt;/strong&gt;&lt;br&gt;
El starter &lt;code&gt;spring-boot-starter-actuator&lt;/code&gt; sí registra los endpoints al agregarlo, pero la exposición web por defecto en Boot 2+ se limita a &lt;code&gt;/health&lt;/code&gt;. El resto necesita &lt;code&gt;management.endpoints.web.exposure.include&lt;/code&gt; explícito.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Por qué &lt;code&gt;/env&lt;/code&gt; es el endpoint más citado en discusiones de seguridad de Actuator?&lt;/strong&gt;&lt;br&gt;
Porque devuelve el árbol completo de fuentes de propiedades del proceso, y en configuraciones reales eso incluye variables con credenciales o tokens si no se activó la sanitización de claves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Alcanza con deshabilitar &lt;code&gt;env&lt;/code&gt; y &lt;code&gt;heapdump&lt;/code&gt; puntualmente?&lt;/strong&gt;&lt;br&gt;
No como estrategia de largo plazo. Cualquier endpoint nuevo (de Boot o de una dependencia de terceros) queda expuesto por default si la base sigue siendo &lt;code&gt;include=*&lt;/code&gt;. La allowlist invierte esa lógica.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Spring Security es obligatorio para usar Actuator en producción?&lt;/strong&gt;&lt;br&gt;
A nivel framework, no: Actuator arranca sin ninguna dependencia de Spring Security. Pero esa libertad tiene un costo directo: si un endpoint queda en el &lt;code&gt;include&lt;/code&gt; y no hay ninguna capa de autorización delante, cualquiera que conozca la URL lo puede pegar sin credencial. No es una posibilidad remota, es el comportamiento por diseño cuando no se agrega nada más. &lt;code&gt;EndpointRequest&lt;/code&gt; existe justamente para simplificar esa integración, no para cumplir un requisito formal del framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿&lt;code&gt;health&lt;/code&gt; con detalles completos es seguro de exponer públicamente?&lt;/strong&gt;&lt;br&gt;
Depende del contenido de esos detalles. &lt;code&gt;management.endpoint.health.show-details=when-authorized&lt;/code&gt; es la opción prudente cuando no se puede garantizar que solo tráfico interno llegue al endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo verifico qué endpoints están expuestos en un ambiente ya corriendo?&lt;/strong&gt;&lt;br&gt;
Un &lt;code&gt;curl&lt;/code&gt; directo a &lt;code&gt;/actuator&lt;/code&gt; (sin sub-path) suele listar los endpoints activos si &lt;code&gt;discovery&lt;/code&gt; está habilitado, lo cual en sí mismo es información a revisar antes de exponerla.&lt;/p&gt;

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

&lt;p&gt;Si el criterio para decidir qué endpoints de Actuator exponer es "deshabilito los que suenan peligrosos", el sistema va a quedar expuesto ante el próximo endpoint que Spring Boot agregue, la próxima dependencia que registre uno, o el próximo dev que ejecute &lt;code&gt;include=*&lt;/code&gt; copiando un tutorial viejo. Allowlist explícita más Spring Security detrás no es la opción más cómoda para arrancar, pero es la única que no depende de que alguien se acuerde de actualizar una lista negra.&lt;/p&gt;

&lt;p&gt;Lo incómodo de esto es que no requiere ningún exploit sofisticado: requiere que nadie haya vuelto a mirar la config después del día en que se agregó el starter. Esa es la parte que más me interesa señalar, no la lista de endpoints.&lt;/p&gt;

&lt;p&gt;El próximo paso concreto, si esto resuena con un sistema real: revisar &lt;code&gt;management.endpoints.web.exposure.include&lt;/code&gt; ahora mismo, no después del próximo pentest. Y de paso, si el sistema maneja sesiones o tokens en memoria, revisar también qué tan expuesto queda &lt;code&gt;/heapdump&lt;/code&gt; — la conexión con &lt;a href="https://juanchi.dev/es/blog/jwt-vs-sesiones-con-estado-identidad-digital-criterio" rel="noopener noreferrer"&gt;JWT sin estado vs sesiones con estado&lt;/a&gt; no es casual: la superficie de exposición de Actuator y el modelo de identidad elegido terminan hablando de lo mismo, qué tan fácil es robar una sesión sin robar una contraseña.&lt;/p&gt;

&lt;p&gt;Esta nota es guía de configuración, no auditoría de un sistema puntual — si buscás algo más cercano a cómo pienso herramientas de desarrollo con límites deliberados, &lt;a href="https://juanchi.dev/es/blog/cline-vscode-agente-ia-codigo-autonomo-configuracion" rel="noopener noreferrer"&gt;la nota sobre Cline en VS Code&lt;/a&gt; sigue la misma lógica de "capacidad total por defecto, restricción explícita después".&lt;/p&gt;

&lt;p&gt;Fuente original: &lt;a href="https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html" rel="noopener noreferrer"&gt;Spring Boot Actuator Docs&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/actuator-endpoints-spring-boot-seguridad-allowlist" 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>TigerFS Isn't a Filesystem, It's a Promise of Determinism</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Sat, 22 Aug 2026 12:00:21 +0000</pubDate>
      <link>https://dev.to/jtorchia/tigerfs-isnt-a-filesystem-its-a-promise-of-determinism-2j4l</link>
      <guid>https://dev.to/jtorchia/tigerfs-isnt-a-filesystem-its-a-promise-of-determinism-2j4l</guid>
      <description>&lt;p&gt;A traditional filesystem promises you the file will be there when you need it. It doesn't promise that two runs of the same program, with the same input, will hit disk in exactly the same order, with the same bytes at the same offsets. For most systems I've worked on, that gap never mattered — logs land, backups run, nobody audits byte order. But the first time I debugged a race condition that only showed up on one specific disk controller, I understood why someone would want to kill that variability entirely. For a financial database that needs to reproduce a state byte-for-byte for audits or simulation testing, that gap is the whole problem.&lt;/p&gt;

&lt;p&gt;That's where TigerFS comes in: the storage layer used by TigerBeetle, the financial accounting database written in Zig. It's not a general-purpose filesystem. It's a piece of engineering built to solve a very specific friction — and that narrow scope is exactly what makes it interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What problem TigerFS solves in an embedded database
&lt;/h2&gt;

&lt;p&gt;The concrete pain is this: when you embed the storage engine directly inside your process (skipping a general-purpose filesystem like ext4 or XFS), you lose all the POSIX guarantees you take for granted without thinking — write ordering, fsync atomicity, behavior during a mid-operation crash. A traditional filesystem gives you those guarantees, but with nuances that shift between kernels, between mount configurations, between versions. I've hit that exact shift firsthand: the same fsync call behaving differently between an ext4 mount with &lt;code&gt;data=ordered&lt;/code&gt; and one with &lt;code&gt;data=writeback&lt;/code&gt; was enough to turn a "should never happen" bug into a Tuesday. For normal debugging, that margin of variation doesn't matter much. For a system that needs deterministic simulation — the same run, the same bug, reproducible a thousand times — that variation is noise drowning out the signal.&lt;/p&gt;

&lt;p&gt;TigerBeetle solves this with a particular approach: in its test suite, it replaces the real filesystem with a full I/O simulation that can inject disk failures, reorder writes, and force corruption in a controlled, repeatable way. TigerFS is the piece that makes simulated behavior and real behavior converge on the same guarantees, without the kernel throwing uncontrolled variables into the mix.&lt;/p&gt;

&lt;p&gt;If you already read the post about &lt;a href="https://juanchi.dev/en/blog/noroboto-lying-fonts-rust-mitigation-technical-analysis" rel="noopener noreferrer"&gt;Noroboto and its hype-free technical reading&lt;/a&gt;, the logic is similar: there's a low-level, specific problem that the usual generic tool doesn't solve — you need something built to spec.&lt;/p&gt;

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

&lt;p&gt;TigerBeetle's GitHub repo is the primary source for all of this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/tigerbeetle/tigerbeetle" rel="noopener noreferrer"&gt;https://github.com/tigerbeetle/tigerbeetle&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What the repo documents clearly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TigerBeetle is written in Zig, not Rust — I'm calling this out explicitly because it's a common mistake to assume Rust given the low-level systems ecosystem where this kind of design usually shows up.&lt;/li&gt;
&lt;li&gt;The project states determinism as a core design principle: the same sequence of operations produces the same state, always.&lt;/li&gt;
&lt;li&gt;It uses a simulation-based testing technique (sometimes called "deterministic simulation testing") where disk and network I/O get swapped for a simulated version that lets you reproduce the exact same failure scenario.&lt;/li&gt;
&lt;li&gt;The design targets a bounded use case: double-entry financial accounting, with a focus on durability and strict consistency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What the repo &lt;strong&gt;doesn't&lt;/strong&gt; say, and what's worth not making up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There's no public benchmark comparing TigerFS against ext4 or XFS on throughput or latency.&lt;/li&gt;
&lt;li&gt;There's no documentation claiming TigerFS is meant to replace a general-purpose filesystem.&lt;/li&gt;
&lt;li&gt;There's no public evidence that this design scales as a generic solution outside TigerBeetle's context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction between what the source claims and what you could enthusiastically infer is exactly where poorly-grounded hype tends to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where people get this kind of design wrong
&lt;/h2&gt;

&lt;p&gt;The common recipe when someone reads about a system like this is: "hey, this total-determinism thing sounds better than what I've got, let me apply it to my project." The hidden cost shows up fast.&lt;/p&gt;

&lt;p&gt;A deterministic filesystem like the one TigerFS's design describes assumes a very particular context: an embedded storage engine, with full control over the on-disk data layout, with no need to interoperate with other applications also writing to that same filesystem. That's exactly what a single-purpose financial database needs. It's not what a typical backend needs — one that serves static files, logs to disk, and shares the filesystem with fifteen other processes.&lt;/p&gt;

&lt;p&gt;The clearest counterexample: if your system needs broad POSIX compatibility — third-party tools, standard backups, mounting across different environments — building or adopting something with this philosophy solves a problem you don't have and creates one you didn't have before: maintaining a piece of non-standard infrastructure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A[Necesito storage embebido] --&amp;gt; B{¿Necesito reproducir estado exacto ante fallas?}
  B --&amp;gt;|sí, es crítico| C[Evaluar diseño determinístico dedicado]
  B --&amp;gt;|no, o es nice-to-have| D[Filesystem estándar + testing convencional]
  C --&amp;gt; E[Costo: mantenimiento de pieza no estándar]
  D --&amp;gt; F[Costo: menos control sobre orden exacto de I/O]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decision matrix: when to look at this kind of design
&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;Worth investigating a TigerFS-style approach?&lt;/th&gt;
&lt;th&gt;What to check first&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single-purpose embedded database engine (financial, accounting)&lt;/td&gt;
&lt;td&gt;Yes, worth evaluating&lt;/td&gt;
&lt;td&gt;Whether the domain demands byte-for-byte failure reproduction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical backend with Postgres/MySQL behind it&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;The database engine already solves this problem, not the filesystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System that needs testing with simulated disk failures&lt;/td&gt;
&lt;td&gt;Worth studying the technique, not necessarily the full filesystem&lt;/td&gt;
&lt;td&gt;Whether you can simulate at the application layer instead of replacing the filesystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project with POSIX interoperability pressure (backups, external tools)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;The cost of losing standard compatibility usually outweighs the benefit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Academic research or low-level systems exploration&lt;/td&gt;
&lt;td&gt;Yes, as technical reading&lt;/td&gt;
&lt;td&gt;Read the source code and tests, not just the marketing around the concept&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This matrix isn't a closed formula. It's a sensible starting point so you don't buy into the design without checking whether the problem it solves is the problem you actually have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits: what can't be concluded from this evidence
&lt;/h2&gt;

&lt;p&gt;None of these claims are backed by the public repo, so I'm not going to hold them as if they were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I can't claim TigerFS is faster or slower than a traditional filesystem — there's no public benchmark measuring that.&lt;/li&gt;
&lt;li&gt;I can't claim this approach applies outside TigerBeetle's specific context without evidence of an equivalent proven use case.&lt;/li&gt;
&lt;li&gt;I can't claim it's "the future of storage" — it's an engineering piece scoped to one domain, not a general industry trend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If someone wants to validate the deterministic behavior in practice, the right path is to run the repo's test suite locally with Docker, review the failure simulation logs, and compare the reproduced behavior against what's documented — not infer it from a tweet with a screenshotted graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  My take
&lt;/h2&gt;

&lt;p&gt;My thesis: TigerFS isn't competing with ext4, and framing it that way is the fastest way to misjudge it. Its value comes precisely from refusing to be generic. It's a piece built for a real, bounded friction: when you need a bug to reproduce exactly the same way a thousand times, a general-purpose filesystem with its kernel and configuration variations becomes the enemy, not the solution.&lt;/p&gt;

&lt;p&gt;What I don't buy is the instinct to grab this kind of design because it sounds more "correct" than what you're running. Determinism at the filesystem layer is a tool for a specific domain, not a maturity badge.&lt;/p&gt;

&lt;p&gt;If you're building a system with strong consistency requirements similar to financial accounting, it's worth studying the approach in detail before dismissing it as "niche systems stuff." If you're building anything else, stick with the standard filesystem and handle determinism at the application's testing layer — it's cheaper, and you'll be able to maintain it without depending on a piece of infrastructure that few people understand.&lt;/p&gt;

&lt;p&gt;The same logic of "pick the right tool for the right problem, without buying into the hype" applies when you're deciding between &lt;a href="https://juanchi.dev/en/blog/stateless-jwt-vs-stateful-sessions-identity-systems" rel="noopener noreferrer"&gt;stateless JWT and stateful sessions&lt;/a&gt;, or when you're evaluating whether you need &lt;a href="https://juanchi.dev/en/blog/server-actions-solves-mutation-not-cache" rel="noopener noreferrer"&gt;Server Actions to solve mutation without solving cache&lt;/a&gt;. The pattern repeats: the question is never "is this better in the abstract?", it's "is my problem the problem this thing solves?"&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Is TigerFS a filesystem you can mount like ext4 or XFS?&lt;/strong&gt;&lt;br&gt;
There's no public evidence it's meant for use as a mountable, general-purpose filesystem on any system. It's a storage layer designed for TigerBeetle's specific context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is TigerBeetle written in Rust?&lt;/strong&gt;&lt;br&gt;
No. TigerBeetle is written in Zig. Worth clarifying because the low-level systems ecosystem tends to automatically get associated with Rust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does "determinism" mean in this context?&lt;/strong&gt;&lt;br&gt;
That the same sequence of operations, run under the same conditions, produces exactly the same final state — with no variation introduced by OS I/O ordering or the scheduler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this approach work for traditional SQL databases?&lt;/strong&gt;&lt;br&gt;
There's no public evidence of that. The design addresses one specific use case (double-entry financial accounting) and isn't documented as a generic solution for SQL engines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you test deterministic behavior without access to production?&lt;/strong&gt;&lt;br&gt;
By running the project's test suite locally with Docker and reviewing the failure simulation scenarios the repo documents. That gives you reproducible proof without needing production data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it worth adopting this philosophy on a small project?&lt;/strong&gt;&lt;br&gt;
Generally, no. The maintenance cost of a non-standard piece of infrastructure usually outweighs the benefit if your domain doesn't demand exact reproducibility under disk failures.&lt;/p&gt;

&lt;p&gt;Original source: &lt;a href="https://github.com/tigerbeetle/tigerbeetle" rel="noopener noreferrer"&gt;https://github.com/tigerbeetle/tigerbeetle&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/tigerfs-not-a-filesystem-promise-of-determinism" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>rust</category>
      <category>sistemasdistribuidos</category>
      <category>tigerfs</category>
    </item>
    <item>
      <title>TigerFS no es un filesystem, es una promesa de determinismo</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Sat, 22 Aug 2026 12:00:16 +0000</pubDate>
      <link>https://dev.to/jtorchia/tigerfs-no-es-un-filesystem-es-una-promesa-de-determinismo-2c35</link>
      <guid>https://dev.to/jtorchia/tigerfs-no-es-un-filesystem-es-una-promesa-de-determinismo-2c35</guid>
      <description>&lt;p&gt;Un filesystem tradicional te promete que el archivo va a estar ahí cuando lo necesites. No te promete que dos corridas del mismo programa, con el mismo input, vayan a tocar el disco exactamente en el mismo orden y con los mismos bytes en los mismos offsets. En la mayoría de los proyectos que pasaron por mis manos —backends que sirven HTTP, workers que procesan colas— esa garantía nunca hizo falta: con logs y un buen &lt;code&gt;strace&lt;/code&gt; alcanzaba para debuggear. Pero para una base de datos financiera que necesita reproducir un estado byte a byte para auditoría o para testing con simulación, esa garantía deja de ser un lujo y se vuelve el problema entero.&lt;/p&gt;

&lt;p&gt;Ahí entra TigerFS: la capa de almacenamiento que usa TigerBeetle, la base de datos de contabilidad financiera escrita en Zig. No es un filesystem de propósito general. Es una pieza de ingeniería construida para resolver una fricción muy específica — y ese recorte es justo lo que la hace interesante.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué problema resuelve TigerFS en una base embebida
&lt;/h2&gt;

&lt;p&gt;El dolor concreto es este: cuando embebés el motor de almacenamiento dentro de tu proceso (sin pasar por un filesystem de propósito general como ext4 o XFS), perdés todas las garantías POSIX que asumís sin pensar — orden de escritura, atomicidad de fsync, comportamiento ante un crash a mitad de operación. Un filesystem tradicional te da esas garantías con matices que varían entre kernel, entre configuración de montaje, entre versión. Para debug normal, ese margen de variación no importa. Para un sistema que necesita simulación determinística — la misma ejecución, el mismo bug, reproducible mil veces — esa variación es ruido que tapa la señal.&lt;/p&gt;

&lt;p&gt;TigerBeetle resuelve esto con un enfoque particular: en su suite de tests, reemplaza el filesystem real por una simulación completa de I/O que puede inyectar fallas de disco, reordenar escrituras y forzar corrupción de forma controlada y repetible. TigerFS es la pieza que hace que ese comportamiento simulado y el comportamiento real converjan en las mismas garantías, sin que el kernel meta variables no controladas en el medio.&lt;/p&gt;

&lt;p&gt;Si ya leíste el post sobre &lt;a href="https://juanchi.dev/es/blog/noroboto-lying-fonts-mitigacion-rust-lectura-tecnica" rel="noopener noreferrer"&gt;Noroboto y su lectura técnica sin hype&lt;/a&gt;, la lógica es parecida: hay un problema de bajo nivel, específico, que no se resuelve con la herramienta genérica de siempre — hace falta una pieza construida a medida.&lt;/p&gt;

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

&lt;p&gt;El repositorio de TigerBeetle en GitHub es la fuente primaria de todo esto:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/tigerbeetle/tigerbeetle" rel="noopener noreferrer"&gt;https://github.com/tigerbeetle/tigerbeetle&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Lo que el repo documenta con claridad:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TigerBeetle está escrito en Zig, no en Rust — corrijo esto explícitamente porque es un error común asumir Rust por el ecosistema de sistemas de bajo nivel donde suele aparecer este tipo de diseño.&lt;/li&gt;
&lt;li&gt;El proyecto declara determinismo como principio de diseño central: la misma secuencia de operaciones produce el mismo estado, siempre.&lt;/li&gt;
&lt;li&gt;Usa una técnica de testing por simulación (a veces llamada "deterministic simulation testing") donde el I/O de disco y de red se reemplaza por una versión simulada que permite reproducir exactamente el mismo escenario de falla.&lt;/li&gt;
&lt;li&gt;El diseño apunta a un caso de uso acotado: contabilidad financiera de doble entrada, con foco en durabilidad y consistencia estricta.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lo que el repo &lt;strong&gt;no&lt;/strong&gt; dice, y que conviene no inventar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No hay un benchmark público comparando TigerFS contra ext4 o XFS en términos de throughput o latencia.&lt;/li&gt;
&lt;li&gt;No hay documentación afirmando que TigerFS esté pensado para reemplazar un filesystem de uso general.&lt;/li&gt;
&lt;li&gt;No hay evidencia pública de que este diseño escale como solución genérica fuera del contexto de TigerBeetle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esa distinción entre lo que la fuente afirma y lo que uno podría inferir con entusiasmo es exactamente donde suele empezar el hype mal fundado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dónde se equivoca la gente con este tipo de diseño
&lt;/h2&gt;

&lt;p&gt;La receta común cuando alguien lee sobre un sistema así es: "che, esto de determinismo total suena mejor que lo que tengo, lo aplico en mi proyecto". El costo oculto aparece rápido.&lt;/p&gt;

&lt;p&gt;Un filesystem determinístico como el que describe el diseño de TigerFS asume un contexto muy particular: un motor de storage embebido, con control total sobre el layout de datos en disco, sin necesidad de interoperar con otras aplicaciones que también escriben en ese mismo filesystem. Eso es exactamente lo que necesita una base de datos financiera de propósito único. No es lo que necesita un backend típico que sirve archivos estáticos, loguea a disco y comparte el filesystem con quince procesos más — el tipo de setup con el que me crucé más de una vez en proyectos donde alguien quiso meter "la solución elegante" donde sobraba.&lt;/p&gt;

&lt;p&gt;El contraejemplo más claro: si tu sistema necesita compatibilidad POSIX amplia — herramientas de terceros, backups estándar, montaje en distintos entornos — construir o adoptar algo con esta filosofía te resuelve un problema que no tenés y te crea uno que antes no tenías: mantenimiento de una pieza de infraestructura no estándar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A[Necesito storage embebido] --&amp;gt; B{¿Necesito reproducir estado exacto ante fallas?}
  B --&amp;gt;|sí, es crítico| C[Evaluar diseño determinístico dedicado]
  B --&amp;gt;|no, o es nice-to-have| D[Filesystem estándar + testing convencional]
  C --&amp;gt; E[Costo: mantenimiento de pieza no estándar]
  D --&amp;gt; F[Costo: menos control sobre orden exacto de I/O]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Matriz de decisión: cuándo mirar hacia este tipo de diseño
&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;¿Vale la pena investigar un enfoque tipo TigerFS?&lt;/th&gt;
&lt;th&gt;Qué mirar primero&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Motor de base de datos embebido de propósito único (financiero, contable)&lt;/td&gt;
&lt;td&gt;Sí, tiene sentido evaluarlo&lt;/td&gt;
&lt;td&gt;Si el dominio exige reproducir fallas byte a byte&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend típico con Postgres/MySQL detrás&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;El problema ya lo resuelve el motor de la base, no el filesystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sistema que necesita testing con simulación de fallas de disco&lt;/td&gt;
&lt;td&gt;Vale la pena estudiar la técnica, no necesariamente el filesystem completo&lt;/td&gt;
&lt;td&gt;Si podés simular a nivel de capa de aplicación en vez de reemplazar el filesystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proyecto con presión de interoperabilidad POSIX (backups, herramientas externas)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;El costo de perder compatibilidad estándar suele superar el beneficio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Investigación académica o exploración de sistemas de bajo nivel&lt;/td&gt;
&lt;td&gt;Sí, como lectura técnica&lt;/td&gt;
&lt;td&gt;Leer el código fuente y los tests, no solo el marketing del concepto&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Esta matriz no es una fórmula cerrada. Es un punto de partida prudente para no comprar el diseño sin evaluar si el problema que resuelve es el problema que tenés.&lt;/p&gt;

&lt;h2&gt;
  
  
  Límites: qué no se puede concluir con esta evidencia
&lt;/h2&gt;

&lt;p&gt;Ninguno de estos claims está respaldado por el repo público, así que no los voy a sostener como si lo estuvieran:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No puedo afirmar que TigerFS sea más rápido o más lento que un filesystem tradicional — no hay benchmark público que lo mida.&lt;/li&gt;
&lt;li&gt;No puedo afirmar que este enfoque sea aplicable fuera del contexto específico de TigerBeetle sin evidencia de un caso de uso equivalente probado.&lt;/li&gt;
&lt;li&gt;No puedo afirmar que sea "el futuro del storage" — es una pieza de ingeniería acotada a un dominio, no una tendencia general de la industria.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si alguien quiere validar el comportamiento determinístico en la práctica, el camino correcto es correr localmente la suite de tests del repo con Docker, revisar los logs de simulación de fallas, y comparar el comportamiento reproducido contra lo documentado — no inferirlo de un tuit con captura de gráfico.&lt;/p&gt;

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

&lt;p&gt;Mi tesis es simple: TigerFS es un diseño elegante precisamente porque no intenta ser genérico. No es el próximo ext4, y decirlo no le resta valor — al contrario. Es una pieza construida para una fricción real y acotada: cuando necesitás que un bug se pueda reproducir exactamente igual mil veces, un filesystem de propósito general con sus variaciones de kernel y configuración se convierte en el enemigo, no en la solución.&lt;/p&gt;

&lt;p&gt;Lo incómodo, para el que quiere aplicar esto en su proyecto: en la enorme mayoría de los casos que vi de cerca —backend con Postgres, colas, cron jobs, el combo de siempre— esa garantía de determinismo total sobra. No porque no sea valiosa en abstracto, sino porque tu problema real no es reproducir estado byte a byte, es que el deploy de un viernes rompió algo y necesitás un log decente, no un filesystem nuevo.&lt;/p&gt;

&lt;p&gt;Si estás armando un sistema con requerimientos de consistencia fuerte parecidos a los de contabilidad financiera, vale la pena estudiar el enfoque en detalle antes de descartarlo por "sistemas de nicho". Si estás armando cualquier otra cosa, quedate con el filesystem estándar y resolvé el determinismo en la capa de testing de la aplicación — es más barato y lo vas a poder mantener sin depender de una pieza de infraestructura que pocos entienden.&lt;/p&gt;

&lt;p&gt;La misma lógica de "elegir la herramienta correcta para el problema correcto, sin comprar el hype" aplica cuando decidís entre &lt;a href="https://juanchi.dev/es/blog/jwt-vs-sesiones-con-estado-identidad-digital-criterio" rel="noopener noreferrer"&gt;JWT sin estado y sesiones con estado&lt;/a&gt;, o cuando evaluás si necesitás &lt;a href="https://juanchi.dev/es/blog/tanstack-query-nextjs-app-router-server-actions" rel="noopener noreferrer"&gt;Server Actions para resolver mutación sin resolver cache&lt;/a&gt;. El patrón se repite: la pregunta nunca es "¿es esto mejor en abstracto?", es "¿mi problema es el problema que esto resuelve?".&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;¿TigerFS es un filesystem que se puede montar como ext4 o XFS?&lt;/strong&gt;&lt;br&gt;
No hay evidencia pública de que esté pensado para uso como filesystem de propósito general montable en cualquier sistema. Es una capa de almacenamiento diseñada para el contexto específico de TigerBeetle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿TigerBeetle está escrito en Rust?&lt;/strong&gt;&lt;br&gt;
No. TigerBeetle está escrito en Zig. Vale la pena aclararlo porque el ecosistema de sistemas de bajo nivel suele asociarse automáticamente con Rust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué significa "determinismo" en este contexto?&lt;/strong&gt;&lt;br&gt;
Que la misma secuencia de operaciones, ejecutada con las mismas condiciones, produce exactamente el mismo estado final — sin variación introducida por el orden de I/O del sistema operativo o el scheduler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Sirve este enfoque para bases de datos SQL tradicionales?&lt;/strong&gt;&lt;br&gt;
No hay evidencia pública de eso. El diseño responde a un caso de uso puntual (contabilidad financiera de doble entrada) y no está documentado como solución genérica para motores SQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo se prueba el comportamiento determinístico sin acceso a producción?&lt;/strong&gt;&lt;br&gt;
Corriendo localmente la suite de tests del proyecto con Docker y revisando los escenarios de simulación de fallas que documenta el repo. Eso da una prueba reproducible sin necesitar datos productivos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Vale la pena adoptar esta filosofía en un proyecto chico?&lt;/strong&gt;&lt;br&gt;
En general no. El costo de mantenimiento de una pieza de infraestructura no estándar suele superar el beneficio si el dominio no exige reproducibilidad exacta ante fallas de disco.&lt;/p&gt;

&lt;p&gt;Fuente original: &lt;a href="https://github.com/tigerbeetle/tigerbeetle" rel="noopener noreferrer"&gt;https://github.com/tigerbeetle/tigerbeetle&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/tigerfs-filesystem-bases-datos-embebidas" 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>sistemasdistribuidos</category>
    </item>
    <item>
      <title>The AI demo failed, but the database remembered half of it</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Fri, 21 Aug 2026 14:40:26 +0000</pubDate>
      <link>https://dev.to/jtorchia/the-ai-demo-failed-but-the-database-remembered-half-of-it-3m4a</link>
      <guid>https://dev.to/jtorchia/the-ai-demo-failed-but-the-database-remembered-half-of-it-3m4a</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The error message said the operation had failed. PostgreSQL told a different&lt;br&gt;
story.&lt;/p&gt;

&lt;p&gt;I was reading Formbricks' AI example-response generator when I found a sequence&lt;br&gt;
of writes that looked individually reasonable: create a tag, create a display,&lt;br&gt;
create a response, evaluate quotas, link the tag, repeat. But each completed&lt;br&gt;
piece could commit before the next one began.&lt;/p&gt;

&lt;p&gt;So I made the third response fail.&lt;/p&gt;

&lt;p&gt;The action rejected, exactly as the UI would expect. Underneath it, the first&lt;br&gt;
two responses were still there. So were three displays, a newly created tag and&lt;br&gt;
two tag links.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Record&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;th&gt;Observed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Response&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Display&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;newly-created Tag&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TagsOnResponses&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third display had been written before response creation failed. The error&lt;br&gt;
was real, but so was half the dataset.&lt;/p&gt;

&lt;p&gt;Then came the part that made this more than a cleanup problem. Formbricks only&lt;br&gt;
allows example generation while a survey has zero responses. After the&lt;br&gt;
existing rate-limit window, retrying the operation hit that guard. The failed&lt;br&gt;
attempt had left two responses behind, so its own debris prevented recovery.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The operation reported failure, changed the database and then used that&lt;br&gt;
change as the reason the user could not try again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the invariant I set out to restore: one request to generate example&lt;br&gt;
responses must have one persistence outcome. Either the complete synthetic&lt;br&gt;
dataset commits, or none of that attempt remains.&lt;/p&gt;
&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/formbricks/formbricks" rel="noopener noreferrer"&gt;Formbricks&lt;/a&gt; is an open-source&lt;br&gt;
experience-management platform for building surveys and analyzing responses.&lt;br&gt;
Its Survey Summary can generate example responses so a team can explore the&lt;br&gt;
analytics experience before collecting real data.&lt;/p&gt;

&lt;p&gt;That sounds like a small demo feature. Its persistence path is not small.&lt;/p&gt;

&lt;p&gt;One generation creates a generated-response tag, one Display and one Response&lt;br&gt;
for each synthetic submission, response timestamps, quota-evaluation links,&lt;br&gt;
tag links and additional impression-only Displays. The production path&lt;br&gt;
generates 20 example responses.&lt;/p&gt;

&lt;p&gt;From a person's perspective, that is one button press. Before this patch, the&lt;br&gt;
database saw a collection of separately committed operations.&lt;/p&gt;

&lt;p&gt;The distinction matters because a user-level operation does not become atomic&lt;br&gt;
just because every helper has a transaction somewhere inside it. If helper A&lt;br&gt;
commits, helper B commits and helper C rolls back, the database has faithfully&lt;br&gt;
protected three different operations. The user only asked for one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;
&lt;h3&gt;
  
  
  A deterministic failure, not an incident story
&lt;/h3&gt;

&lt;p&gt;I wanted a reproduction I could run on demand. I replaced the external model&lt;br&gt;
boundary with a deterministic four-response dataset and injected a failure&lt;br&gt;
while response 3 was being created. Four responses keep the baseline small&lt;br&gt;
enough to inspect; the final validation separately exercises the&lt;br&gt;
production-sized batch of 20.&lt;/p&gt;

&lt;p&gt;Before the fix, response 1 and response 2 committed. Their tag links committed.&lt;br&gt;
The Display belonging to response 3 also committed because it was created&lt;br&gt;
before the injected failure. The enclosing action rejected, but there was no&lt;br&gt;
enclosing database transaction capable of undoing the earlier work.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/JuanTorchia/formbricks/blob/d5c5f381c994bafa2efdb7662e5468e6c887843d/apps/web/integration/ai-example-response-atomicity.test.ts" rel="noopener noreferrer"&gt;baseline characterization&lt;br&gt;
test&lt;/a&gt;&lt;br&gt;
records that behavior against the unpatched path.&lt;/p&gt;

&lt;p&gt;This proves a mechanism, not its production frequency. I do not know how many&lt;br&gt;
users have encountered a mid-batch failure, how often it happens or what it has&lt;br&gt;
cost. I did not find this through a production incident report. The defensible&lt;br&gt;
claim is narrower: under a deterministic failure, the operation left the&lt;br&gt;
measured partial state above and made a later retry fail the zero-response&lt;br&gt;
guard.&lt;/p&gt;
&lt;h3&gt;
  
  
  The obvious transaction was still wrong
&lt;/h3&gt;

&lt;p&gt;My first fix was the fix most of us would sketch immediately: start one outer&lt;br&gt;
Prisma transaction before persistence and pass its client into the writes.&lt;/p&gt;

&lt;p&gt;It solved the first rollback test. Then I constrained Prisma to one database&lt;br&gt;
connection.&lt;/p&gt;

&lt;p&gt;The test expired after about five seconds.&lt;/p&gt;

&lt;p&gt;The outer transaction owned the only connection, but organization, workspace,&lt;br&gt;
survey and quota services still performed reads through the global Prisma&lt;br&gt;
client. Those reads asked the pool for another connection. There was no other&lt;br&gt;
connection. The transaction waited on code inside itself until it expired.&lt;/p&gt;

&lt;p&gt;Increasing the timeout would only make the deadlock-shaped wait longer. The&lt;br&gt;
problem was not that the transaction needed more patience. The problem was&lt;br&gt;
that I had drawn a boundary in one function while the call graph quietly&lt;br&gt;
crossed it.&lt;/p&gt;

&lt;p&gt;That one-connection test changed the implementation. Stable survey, quota and&lt;br&gt;
workspace-to-organization context is now loaded once through the transaction&lt;br&gt;
client. Mutable quota counts and every write also stay on that client. Existing&lt;br&gt;
callers keep their cached, global path; the generated-response path opts into a&lt;br&gt;
narrow persistence context tied to its caller-owned transaction.&lt;/p&gt;

&lt;p&gt;It left me with a rule I trust more than the green test I had before:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A transaction boundary is only real if every database operation inside it&lt;br&gt;
uses the same transaction client.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Keep the model outside, then distrust the old snapshot
&lt;/h3&gt;

&lt;p&gt;Putting the model call inside the transaction would hold a database connection&lt;br&gt;
and lock while waiting on an external service. That makes atomicity expensive&lt;br&gt;
in exactly the wrong place, so generation remains outside.&lt;/p&gt;

&lt;p&gt;But that creates a race. Two collaborators can both observe zero responses,&lt;br&gt;
start model generation and return with valid datasets. A real respondent can&lt;br&gt;
also submit while the model is running. The survey may be archived. Ownership&lt;br&gt;
may no longer match the snapshot used to start the action.&lt;/p&gt;

&lt;p&gt;The transaction therefore acquires the survey lock &lt;em&gt;after&lt;/em&gt; generation and&lt;br&gt;
revalidates the state that authorizes persistence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Survey still exists and is not archived;&lt;/li&gt;
&lt;li&gt;it still belongs to the expected Workspace;&lt;/li&gt;
&lt;li&gt;the Workspace still belongs to the expected Organization;&lt;/li&gt;
&lt;li&gt;no Response arrived while the model was running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only then does it load the stable persistence context and write the synthetic&lt;br&gt;
batch.&lt;/p&gt;
&lt;h3&gt;
  
  
  The strongest lock was not the safest lock
&lt;/h3&gt;

&lt;p&gt;My first instinct was &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;. It serializes competing&lt;br&gt;
generators, but it can also conflict with the &lt;code&gt;FOR KEY SHARE&lt;/code&gt; lock PostgreSQL&lt;br&gt;
uses when a normal Response or Display insert validates its foreign key to the&lt;br&gt;
Survey.&lt;/p&gt;

&lt;p&gt;The example generator should protect its own batch. It should not make a real&lt;br&gt;
respondent wait just because a synthetic demo is being persisted.&lt;/p&gt;

&lt;p&gt;The final design uses &lt;code&gt;FOR NO KEY UPDATE&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$queryRaw&lt;/span&gt;&lt;span class="s2"&gt;`
      SELECT id
      FROM "Survey"
      WHERE id = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;survey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
      FOR NO KEY UPDATE
    `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Revalidate archive state, ownership and zero Responses.&lt;/span&gt;
    &lt;span class="c1"&gt;// Load stable Survey and quota context through tx.&lt;/span&gt;
    &lt;span class="c1"&gt;// Persist every synthetic entity through tx.&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That lock still serializes competing generators and Survey updates, while&lt;br&gt;
remaining compatible with the foreign-key lock used by normal inserts.&lt;/p&gt;

&lt;p&gt;I tested the distinction directly: pause example persistence at response 3,&lt;br&gt;
insert a real Response from a second connection, and verify that the real&lt;br&gt;
insert completes before the example transaction is released.&lt;/p&gt;

&lt;p&gt;The winning lock was not the one with the most intimidating name. It was the&lt;br&gt;
weakest lock that protected the invariant without placing the demo ahead of a&lt;br&gt;
real person.&lt;/p&gt;
&lt;h3&gt;
  
  
  Strict failure where atomicity needs it
&lt;/h3&gt;

&lt;p&gt;Quota evaluation introduced another boundary. For normal response intake,&lt;br&gt;
Formbricks historically treats some quota database errors as best-effort: log&lt;br&gt;
the problem and continue accepting the response. Changing that globally would&lt;br&gt;
turn this bug fix into an unrelated compatibility decision.&lt;/p&gt;

&lt;p&gt;For an atomic generated batch, however, swallowing a quota-link failure would&lt;br&gt;
commit another kind of partial dataset.&lt;/p&gt;

&lt;p&gt;The dedicated example-response context therefore selects strict propagation.&lt;br&gt;
A real PostgreSQL &lt;code&gt;P2003&lt;/code&gt; foreign-key error during quota-link creation is&lt;br&gt;
re-thrown and aborts the outer transaction. Existing response callers preserve&lt;br&gt;
their original API and best-effort behavior.&lt;/p&gt;

&lt;p&gt;That asymmetry is intentional. Reliability work is not making every path&lt;br&gt;
stricter. It is deciding which failures each path is allowed to survive.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Upstream issue: &lt;a href="https://github.com/formbricks/formbricks/issues/8722" rel="noopener noreferrer"&gt;https://github.com/formbricks/formbricks/issues/8722&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pull request in my fork: &lt;a href="https://github.com/JuanTorchia/formbricks/pull/1" rel="noopener noreferrer"&gt;https://github.com/JuanTorchia/formbricks/pull/1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Immutable final commit: &lt;a href="https://github.com/JuanTorchia/formbricks/commit/7c183d42d6dc8f33aaaea9b297ab5c296794526a" rel="noopener noreferrer"&gt;https://github.com/JuanTorchia/formbricks/commit/7c183d42d6dc8f33aaaea9b297ab5c296794526a&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PostgreSQL evidence package: &lt;a href="https://gist.github.com/JuanTorchia/c3fc63122fba62a4b52e389c402aa2bb/c798152c61f1a1c20e046473518ad66f9160b3d9" rel="noopener noreferrer"&gt;https://gist.github.com/JuanTorchia/c3fc63122fba62a4b52e389c402aa2bb/c798152c61f1a1c20e046473518ad66f9160b3d9&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model request stays outside the transaction. Inside it, the patch locks and&lt;br&gt;
revalidates the Survey, loads stable evaluation context through &lt;code&gt;tx&lt;/code&gt;, creates&lt;br&gt;
all 20 responses and their related records through that same client, bulk&lt;br&gt;
inserts tag links and adds the remaining impression-only Displays before&lt;br&gt;
commit.&lt;/p&gt;

&lt;p&gt;The candidate patch also adds runtime guards so the atomic context cannot be&lt;br&gt;
used without its transaction, across surveys, with quotas from another survey&lt;br&gt;
or for contact-linked responses. A type that looks correct at one call site is&lt;br&gt;
not enough protection for shared persistence code.&lt;/p&gt;
&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;
&lt;h3&gt;
  
  
  One invariant, tested from its failure edges
&lt;/h3&gt;

&lt;p&gt;The completed regression matrix is broader than Ã¢Â€Âœthe happy path still worksÃ¢Â€Â:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Verified result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;failure while creating response 3&lt;/td&gt;
&lt;td&gt;zero synthetic rows; later retry succeeds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;quota-link foreign-key failure (&lt;code&gt;P2003&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;complete rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transaction expiration (&lt;code&gt;P2028&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;complete rollback; later retry succeeds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;two generators race&lt;/td&gt;
&lt;td&gt;one complete dataset; one domain rejection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;real Response arrives during model generation&lt;/td&gt;
&lt;td&gt;generated batch rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;real Response arrives during persistence&lt;/td&gt;
&lt;td&gt;real insert is not blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Survey archived during generation&lt;/td&gt;
&lt;td&gt;generated batch rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;generated Tag already exists&lt;/td&gt;
&lt;td&gt;preexisting Tag survives rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prisma pool has one connection&lt;/td&gt;
&lt;td&gt;complete response and quota path succeeds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The real PostgreSQL harness covers the production schema, ownership checks,&lt;br&gt;
Display creation, v1 Response persistence, quota lookup and evaluation,&lt;br&gt;
quota-link writes and bulk tag assignment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Focused compatibility run: 5 files passed, 158 tests passed
PostgreSQL harness:         2 files passed,   8 tests passed
One-connection full path:  1 file passed,    2 tests passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PostgreSQL results were recorded before a later type-only follow-up. That&lt;br&gt;
follow-up changed how the same quota-evaluation payload is constructed so&lt;br&gt;
TypeScript preserves its discriminated union; it did not change the runtime&lt;br&gt;
values or transaction behavior. I attempted a PostgreSQL rerun, but my local&lt;br&gt;
database service was offline, so I am stating the gap instead of pretending the&lt;br&gt;
rerun happened.&lt;/p&gt;

&lt;p&gt;On the final commit, the fork's hosted Formbricks web build, official unit&lt;br&gt;
tests, linters and Helm validation passed. The hosted E2E job stopped in one&lt;br&gt;
second without executing test steps in the fork environment, and SonarQube&lt;br&gt;
could not authenticate without its repository secret. I will not compress that&lt;br&gt;
mixed result into Ã¢Â€ÂœCI is green.Ã¢Â€Â&lt;/p&gt;

&lt;h3&gt;
  
  
  What this fix guarantees
&lt;/h3&gt;

&lt;p&gt;For the tested failure modes, a generated dataset commits completely or rolls&lt;br&gt;
back completely. A failed transaction no longer leaves synthetic Responses&lt;br&gt;
that create the persistent zero-response retry block. Competing persistence is&lt;br&gt;
serialized per Survey, state is revalidated after generation, quota capacity&lt;br&gt;
ordering is preserved and normal response insertion is not blocked by the&lt;br&gt;
Survey lock.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it deliberately does not guarantee
&lt;/h3&gt;

&lt;p&gt;The external model call is outside the database transaction. Two callers may&lt;br&gt;
still pay for duplicate model work before persistence serialization lets one&lt;br&gt;
win and rejects the other. This is not exactly-once generation.&lt;/p&gt;

&lt;p&gt;The explicit 10-second transaction timeout was validated locally. It is not a&lt;br&gt;
universal promise for every remote or heavily loaded PostgreSQL deployment.&lt;/p&gt;

&lt;p&gt;This work also does not cover a process crash outside PostgreSQL transaction&lt;br&gt;
semantics, establish production frequency or prove that a lease, queue or&lt;br&gt;
workflow engine is needed. Those are different claims requiring different&lt;br&gt;
evidence.&lt;/p&gt;

&lt;p&gt;I would rather leave a limitation visible than make the patch sound larger&lt;br&gt;
than it is. Atomicity is already a strong promise. It does not need borrowed&lt;br&gt;
certainty.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the open-source contribution stands
&lt;/h3&gt;

&lt;p&gt;Formbricks currently says community code contributions are accepted only in&lt;br&gt;
rare exceptions. I reported the reproducible bug and asked maintainers whether&lt;br&gt;
they wanted the prepared fix and where they would prefer the PostgreSQL&lt;br&gt;
regression to live.&lt;/p&gt;

&lt;p&gt;As of August 21, 2026, &lt;a href="https://github.com/formbricks/formbricks/issues/8722" rel="noopener noreferrer"&gt;the upstream issue remains&lt;br&gt;
open&lt;/a&gt;, labeled as a bug,&lt;br&gt;
without an assignee or a maintainer response. Another contributor asked to&lt;br&gt;
work on it; I clarified respectfully that a tested candidate already exists.&lt;br&gt;
The pull request linked above lives in my fork. It is not merged or accepted&lt;br&gt;
upstream.&lt;/p&gt;

&lt;p&gt;A contest deadline does not create a right to someone else's review queue. The&lt;br&gt;
patch and its evidence can stand in public without turning persistence into&lt;br&gt;
pressure.&lt;/p&gt;

&lt;h3&gt;
  
  
  What stayed with me
&lt;/h3&gt;

&lt;p&gt;The first rollback test went green quickly. If I had stopped there, I would&lt;br&gt;
have proposed a transaction that could starve its own connection pool. If I&lt;br&gt;
had chosen the strongest row lock without testing a real insert beside it, I&lt;br&gt;
might have protected synthetic data by delaying an actual respondent.&lt;/p&gt;

&lt;p&gt;The difficult part was never typing &lt;code&gt;$transaction&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It was discovering where the operation really begins and ends. It was tracing&lt;br&gt;
every quiet database read that crossed that boundary. It was accepting that an&lt;br&gt;
external model belongs outside the lock, then distrusting every piece of state&lt;br&gt;
that might have changed while it ran. It was choosing a lock for the work that&lt;br&gt;
needed protection without making unrelated work pay for it.&lt;/p&gt;

&lt;p&gt;There is a small debugging exercise here that travels well beyond Formbricks.&lt;br&gt;
Find a feature your product presents as one action, especially one that writes&lt;br&gt;
in a loop. Fail it in the middle. Then ignore the error message and ask the&lt;br&gt;
database what actually happened.&lt;/p&gt;

&lt;p&gt;If those two answers disagree, the bug is not merely a missing rollback. It is&lt;br&gt;
a boundary nobody finished drawing.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>postgres</category>
    </item>
    <item>
      <title>What It Means to Be a Java Champion in 2026: The Real Criteria Behind the Recognition and Why I Care</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 18 Aug 2026 14:30:36 +0000</pubDate>
      <link>https://dev.to/jtorchia/what-it-means-to-be-a-java-champion-in-2026-the-real-criteria-behind-the-recognition-and-why-i-care-ble</link>
      <guid>https://dev.to/jtorchia/what-it-means-to-be-a-java-champion-in-2026-the-real-criteria-behind-the-recognition-and-why-i-care-ble</guid>
      <description>&lt;h1&gt;
  
  
  What It Means to Be a Java Champion in 2026: The Real Criteria Behind the Recognition and Why I Care
&lt;/h1&gt;

&lt;p&gt;Why do the most respected technical recognition programs in the industry still have almost invisible Latin American representation, decades after the region started producing world-class software? I've been chewing on that question for a while now, not as a complaint but as a technical question at the community level: what kinds of contributions actually count, who gets to see them, and why does the language you write in seem to decide whether you exist on that map at all.&lt;/p&gt;

&lt;p&gt;My thesis is concrete: &lt;strong&gt;Java Champion isn't an academic title or a corporate badge — it's recognition of community contribution. And in 2026, quality technical content in non-English languages is an undervalued, completely legitimate contribution that the program's own public criteria already support, even if nobody says it out loud.&lt;/strong&gt; Writing this as a declared goal isn't arrogance; it's transparency. And building toward it in public, in my own voice, in Spanish, is part of the argument itself — not decoration around it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Java Champion Is and What the Official Source Actually Says
&lt;/h2&gt;

&lt;p&gt;The Java Champions program has existed for years and is publicly documented by Oracle at &lt;a href="https://developer.oracle.com/javachampions/" rel="noopener noreferrer"&gt;developer.oracle.com/javachampions&lt;/a&gt;. The list of recognized people is public. The general criteria are too.&lt;/p&gt;

&lt;p&gt;First thing worth understanding: &lt;strong&gt;Oracle doesn't pick Java Champions directly.&lt;/strong&gt; The process runs on peer nomination inside the existing Champions community. Oracle administers the program and has the final call, but the momentum starts from inside — someone already recognized proposes someone new based on contributions they've actually seen.&lt;/p&gt;

&lt;p&gt;That changes how you have to think about the goal. There's no exam to pass. There's no form to fill out. The path is building visible, consistent work until someone inside that circle notices it and puts your name forward. The question that matters isn't "how do I apply?" — it's "what kind of work makes me visible to the right people?"&lt;/p&gt;

&lt;p&gt;According to the program's public information, the contributions that count include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quality technical content&lt;/strong&gt;: articles, blogs, tutorials, videos, podcasts — anything that educates the community.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conference participation&lt;/strong&gt;: talks at JUGs (Java User Groups), JavaOne, Devoxx, and regional events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open source contributions&lt;/strong&gt;: visible work on relevant projects in the Java ecosystem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community leadership&lt;/strong&gt;: organizing groups, mentoring, building spaces where other people learn.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What the official source does &lt;strong&gt;not&lt;/strong&gt; say explicitly: the relative weight of each category, how long the process usually takes, or whether there's some minimum follower count or reach threshold. That's not publicly documented, and it would be irresponsible to make it up. What you &lt;em&gt;can&lt;/em&gt; infer from the public Champions list is that the dominant profile is someone with years of sustained contribution — not a spike of visibility that fades in a year.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Most Common Misunderstanding: Confusing Certification With Recognition
&lt;/h2&gt;

&lt;p&gt;There's a mix-up in the community that's worth naming directly: &lt;strong&gt;Java Champion is not a certification&lt;/strong&gt;. It's not the OCPJP or the OCPJEA. You don't earn it by studying at night and sitting for an exam at a Pearson VUE center — the way I did with the CCNA back in 2009, laptop running so hot I had to park a fan next to it so Packet Tracer wouldn't freeze mid-lab.&lt;/p&gt;

&lt;p&gt;Certifications measure technical knowledge at a single point in time. The Java Champions program measures &lt;strong&gt;accumulated impact on the community&lt;/strong&gt;. Those are different metrics, and the second one has no shortcuts — no cramming your way into a reputation.&lt;/p&gt;

&lt;p&gt;The mistake I keep seeing: technically sharp devs who assume community recognition will show up on its own because the code is good. It doesn't work that way. Code nobody sees doesn't move anything. A contribution needs surface area — it needs to be explained, published, presented, argued about in public. That's the part most people skip because it feels less "technical" than the code itself.&lt;/p&gt;

&lt;p&gt;The opposite mistake: content creators who build an audience with zero technical depth. A viral "10 Java tips" post full of conceptual errors doesn't build the kind of reputation this program cares about. The criterion isn't raw reach — it's &lt;strong&gt;genuine technical contribution with enough reach for the ecosystem to actually notice it&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Latin American Gap and Why Spanish Matters More Than It Looks
&lt;/h2&gt;

&lt;p&gt;Look at the public Java Champions list in 2026 and Latin American representation is thin — remarkably thin, relative to the size of the region's developer community. Brazil has a handful of names, partly because there's an active JUG culture and consolidated events like TDC. The rest of the Spanish-speaking region barely registers.&lt;/p&gt;

&lt;p&gt;There are a few hypotheses for that. The easiest one to dismiss is technical competence — the region has world-class architects and senior devs, full stop. The more plausible hypotheses are structural:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Language visibility&lt;/strong&gt;: the international technical ecosystem runs mostly in English. Contributions in Spanish get less cross-community reach even when the depth is equal or greater.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No nomination nodes nearby&lt;/strong&gt;: if you don't have Champions close by who can propose and vouch for your work, the chain never starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A content culture skewed toward consumption&lt;/strong&gt;: in the region there's more tradition of consuming than publishing. Plenty of mid-to-senior devs read content in English and produce nothing in any language.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My stance on this is clear: &lt;strong&gt;quality technical content in Spanish is a legitimate contribution to the global Java ecosystem&lt;/strong&gt;, not a lesser version of contributing in English. It solves a real access problem for an enormous community of professionals who learn and work in Spanish. A precise article on Spring Boot 3, OpenTelemetry, or systems architecture, written with actual depth, reaches a segment of the community that English content simply doesn't touch. That has value. That value should be recognized — and right now it mostly isn't.&lt;/p&gt;

&lt;p&gt;Is it today? Probably less than it should be. Can that change? Yes, and the way to change it isn't complaining about it — it's building the corpus, staying consistent, connecting with the Spanish-speaking JUG community, and making the work visible enough that it can't be ignored.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest Checklist: What Actually Builds a Real Candidacy in 2026
&lt;/h2&gt;

&lt;p&gt;I don't have access to the internal nomination process or whatever undocumented criteria exist behind it. What I can do is put together a checklist based on the program's public evidence and the patterns you can actually observe in existing Champions' profiles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Sustained technical content
   - Blog or channel with regular publications (not sporadic viral posts)
   - Verifiable technical depth: real code, decision criteria, honest trade-offs
   - Coverage of the Java ecosystem: JVM, frameworks, patterns, tooling

✅ Participation in structured Java community
   - JUG membership or leadership (there are active JUGs in Argentina, Mexico, Colombia, Peru)
   - Technical talks at meetups or conferences
   - Public interaction with other ecosystem members

✅ Observable open source contributions
   - Merged PRs in relevant Java projects
   - Issues with genuine technical analysis behind them
   - Own projects with demonstrable adoption or utility

✅ Network within the program
   - Connection with existing Java Champions (not empty networking — connection built on real work)
   - Visibility in spaces where Champions actually show up

❌ What probably does NOT cut it alone
   - Oracle certifications (necessary for your career, irrelevant to this specific recognition)
   - A big audience with no technical depth behind it
   - Private contributions with zero public surface area
   - One year of intense activity with no track record before it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The honest thing to say here: I don't know how long this takes or what the minimum threshold looks like in any of these dimensions, because the official source doesn't specify it, and inventing numbers would be irresponsible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I'm Declaring This in Public as a Goal
&lt;/h2&gt;

&lt;p&gt;There's something uncomfortable about declaring a recognition goal in public. It sounds like ego, like LinkedIn posturing, like optimizing for the title before doing the work. I get that reaction. I'm doing it anyway, for a reason that's about the community, not about me.&lt;/p&gt;

&lt;p&gt;The content on this blog — posts about &lt;a href="https://juanchi.dev/en/blog/why-i-stopped-using-useeffect-sync-state-react-19" rel="noopener noreferrer"&gt;useEffect and state synchronization&lt;/a&gt;, about &lt;a href="https://juanchi.dev/en/blog/prisma-server-actions-nextjs-16-n1-composition-patterns" rel="noopener noreferrer"&gt;Prisma and Server Actions in Next.js&lt;/a&gt;, about &lt;a href="https://juanchi.dev/en/blog/spring-boot-startup-time-2026-graalvm-native-aot-cds" rel="noopener noreferrer"&gt;Spring Boot startup time in 2026&lt;/a&gt;, about &lt;a href="https://juanchi.dev/en/blog/opentelemetry-spring-boot-logs-vs-traces-diagnosis" rel="noopener noreferrer"&gt;OpenTelemetry and the difference between logs and traces&lt;/a&gt; — doesn't exist to pad a resume. It exists because there's a real gap in quality technical content in Spanish, and closing part of that gap is the actual work, title or no title.&lt;/p&gt;

&lt;p&gt;Declaring Java Champion as a goal doesn't change what I write. What it does is make explicit why that content matters beyond any single post: it's part of a corpus, a contribution sustained over time, an argument I'm building post by post. Being transparent about the goal is also what builds a genuine audience — it shows the reasoning behind editorial decisions. Why I write about Java and not just Next.js. Why every post aims for real depth instead of quick answers. Why connecting with JUGs matters to me instead of just shouting into the void.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://juanchi.dev/en/blog/needle-gemini-tool-calling-26m-parameters-technical-read" rel="noopener noreferrer"&gt;analysis of Needle and tool calling in small models&lt;/a&gt; I wrote last week is an example of what I mean: it's not content for devs who want a quick answer. It's content for devs who want to understand the technical reasoning behind a decision. That's the kind of contribution that actually points somewhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between Java Champion and Oracle ACE?&lt;/strong&gt;&lt;br&gt;
Oracle ACE is a separate Oracle recognition program, with different criteria and a different process. Java Champion is specifically oriented toward the Java ecosystem and its technical community. You can be an Oracle ACE without being a Java Champion, and vice versa. Both carry value, but they target slightly different profiles and contributions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there a cost or application form for Java Champion?&lt;/strong&gt;&lt;br&gt;
No cost. And there's no open application form — the process runs on nomination by existing Champions. According to the program's public information at &lt;a href="https://developer.oracle.com/javachampions/" rel="noopener noreferrer"&gt;developer.oracle.com/javachampions&lt;/a&gt;, Oracle evaluates nominations but doesn't generate them unilaterally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does content in Spanish count as a contribution to the Java ecosystem?&lt;/strong&gt;&lt;br&gt;
Based on the program's public criteria, yes — quality technical content is a valid contribution regardless of language. What I can't say with certainty is how much weight it actually carries during nomination, because that detail isn't documented anywhere. My position is that it should count, and making that case requires demonstrating real technical quality, not just publication volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are there Spanish-speaking Java Champions?&lt;/strong&gt;&lt;br&gt;
Yes, though the representation is thin relative to the size of the community. Brazil has more historical presence in the program, partly thanks to JUG activity like SouJava. The Spanish-speaking space has real room to grow here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's a JUG and how do I connect with one?&lt;/strong&gt;&lt;br&gt;
JUG stands for Java User Group — community groups organized by region or city. There are active JUGs in Argentina, Mexico, Colombia, and other countries. The &lt;a href="https://developer.oracle.com/java/jug/" rel="noopener noreferrer"&gt;official JUG list&lt;/a&gt; is on Oracle's site. Participating in a JUG is one of the most direct ways to plug into the region's structured Java community.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long does it take to reach this recognition?&lt;/strong&gt;&lt;br&gt;
I don't know, and I wouldn't pretend to without evidence. Existing Champions' profiles show sustained contributions over years, not visibility sprints. What I can say: starting to build tomorrow beats waiting for some imaginary perfect moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing: The Argument Built One Post at a Time
&lt;/h2&gt;

&lt;p&gt;Java Champion in 2026 isn't a title you chase directly. It's the byproduct of work that matters regardless of whether it ever leads to that specific recognition. That distinction matters to me: if the only value of the content were the title, it wouldn't be worth building in the first place. The real value is the contribution itself.&lt;/p&gt;

&lt;p&gt;What I do believe, without hedging: &lt;strong&gt;technical content in Spanish is an unresolved gap in the Java ecosystem.&lt;/strong&gt; There are hundreds of thousands of devs across Latin America learning Java, working with Spring Boot, deploying to production, making complex technical calls — and they have very little quality content in their own language backing those decisions with real depth. Closing that gap is the work. Whatever recognition eventually comes from it is a consequence, not the point.&lt;/p&gt;

&lt;p&gt;The practical next step, if any of this landed for you: go find the JUG in your city or region. Not to pad a resume line — to find the network that makes the work you're already doing visible to someone.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Java Champions Program — Oracle: &lt;a href="https://developer.oracle.com/javachampions/" rel="noopener noreferrer"&gt;https://developer.oracle.com/javachampions/&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/java-champion-2026-real-criteria-why-it-matters" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>opensource</category>
      <category>springboot</category>
      <category>java</category>
    </item>
    <item>
      <title>Qué significa ser Java Champion en 2026: el criterio real detrás del reconocimiento y por qué me importa</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 18 Aug 2026 14:30:31 +0000</pubDate>
      <link>https://dev.to/jtorchia/que-significa-ser-java-champion-en-2026-el-criterio-real-detras-del-reconocimiento-y-por-que-me-g9b</link>
      <guid>https://dev.to/jtorchia/que-significa-ser-java-champion-en-2026-el-criterio-real-detras-del-reconocimiento-y-por-que-me-g9b</guid>
      <description>&lt;p&gt;ated me hago esa pregunta hace rato — dejame reescribirla bien:&lt;/p&gt;

&lt;h1&gt;
  
  
  Qué significa ser Java Champion en 2026: el criterio real detrás del reconocimiento y por qué me importa
&lt;/h1&gt;

&lt;p&gt;¿Por qué los programas de reconocimiento técnico más respetados de la industria siguen teniendo una representación latinoamericana casi invisible, décadas después de que la región empezó a producir software de clase mundial? Hace un tiempo que me hago esa pregunta. No como queja, sino como pregunta técnica de comunidad: ¿qué tipo de contribuciones cuenta, quién las ve, y por qué el idioma en el que las escribís pareciera determinar si existís o no en ese mapa?&lt;/p&gt;

&lt;p&gt;Mi tesis es concreta: &lt;strong&gt;Java Champion no es un título académico ni corporativo — es reconocimiento de contribución a la comunidad. Y en 2026, el contenido técnico de calidad en idiomas no-ingleses es una contribución subvalorada y completamente legítima.&lt;/strong&gt; Escribir esto como objetivo declarado no es arrogancia; es transparencia. Y construir hacia ese objetivo en público, en español rioplatense, es parte del argumento.&lt;/p&gt;




&lt;h2&gt;
  
  
  Qué es Java Champion y qué dice la fuente oficial
&lt;/h2&gt;

&lt;p&gt;El programa Java Champions existe desde hace años y está documentado públicamente por Oracle en &lt;a href="https://developer.oracle.com/javachampions/" rel="noopener noreferrer"&gt;developer.oracle.com/javachampions&lt;/a&gt;. La lista de reconocidos es pública. Los criterios generales también.&lt;/p&gt;

&lt;p&gt;Lo primero que hay que entender: &lt;strong&gt;Oracle no elige a los Java Champions directamente.&lt;/strong&gt; El proceso funciona por nominación entre pares dentro de la comunidad existente de Champions. Oracle administra el programa y tiene la palabra final, pero el impulso viene de adentro — alguien ya reconocido propone a alguien nuevo basándose en contribuciones observadas.&lt;/p&gt;

&lt;p&gt;Eso cambia cómo hay que pensar el objetivo. No se trata de pasar un examen. No hay un formulario de postulación que completar. El camino es construir contribuciones visibles y consistentes hasta que alguien dentro del círculo te vea y te proponga. La pregunta que importa entonces no es "¿cómo aplico?", sino "¿qué tipo de trabajo me hace visible para las personas correctas?".&lt;/p&gt;

&lt;p&gt;Según la información pública del programa, las contribuciones que se valoran incluyen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Contenido técnico de calidad&lt;/strong&gt;: artículos, blogs, tutoriales, videos, podcasts — creación que educa a la comunidad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Participación en conferencias&lt;/strong&gt;: presentaciones en JUGs (Java User Groups), JavaOne, Devoxx, y eventos regionales.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contribuciones a open source&lt;/strong&gt;: trabajo visible en proyectos relevantes del ecosistema Java.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Liderazgo en comunidad&lt;/strong&gt;: organización de grupos, mentoreo, construcción de espacios donde otros aprenden.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lo que la fuente oficial &lt;strong&gt;no dice&lt;/strong&gt; de forma explícita: qué peso relativo tiene cada una, cuánto tiempo requiere el proceso, o si hay un umbral mínimo de seguidores o alcance. Eso no está documentado públicamente y sería imprudente inventarlo. Lo que sí se puede inferir de la lista pública de Champions es que el perfil dominante es alguien con años de contribución sostenida, no picos de visibilidad aislados.&lt;/p&gt;




&lt;h2&gt;
  
  
  El malentendido más común: confundir certificación con reconocimiento
&lt;/h2&gt;

&lt;p&gt;Hay una confusión frecuente en la comunidad que merece nombrarse directamente: &lt;strong&gt;Java Champion no es una certificación&lt;/strong&gt;. No es el OCPJP ni el OCPJEA. No la ganás estudiando de noche y rindiéndola en un centro Pearson VUE — como sí hice con el CCNA en 2009, con la laptop calentándose tanto que tenía que poner un ventilador al lado para que no se trabe el Packet Tracer.&lt;/p&gt;

&lt;p&gt;Las certificaciones miden conocimiento técnico en un punto del tiempo. El programa Java Champion mide &lt;strong&gt;impacto acumulado en la comunidad&lt;/strong&gt;. Son métricas distintas, y la segunda no tiene atajos.&lt;/p&gt;

&lt;p&gt;El error que veo seguido: devs muy competentes técnicamente que asumen que el reconocimiento de comunidad va a llegar solo porque el código es bueno. No funciona así. El código que nadie ve no mueve el indicador. Una contribución técnica notable necesita superficie — necesita ser explicada, publicada, presentada, discutida. Esa es la parte que muchos saltean, y te lo digo porque yo mismo tardé años en entenderla: escribía código sólido en trabajos anteriores que nadie fuera del equipo vio jamás, y ese trabajo, técnicamente bueno, no existió para nadie más que para el changelog interno.&lt;/p&gt;

&lt;p&gt;El error inverso también existe: content creators que construyen audiencia sin profundidad técnica. Un post viral sobre "los 10 tips de Java" con errores conceptuales no construye el tipo de reputación que importa para este programa. El criterio no es alcance puro; es &lt;strong&gt;contribución técnica genuina con alcance suficiente para que el ecosistema lo note&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  La brecha latinoamericana y por qué el español importa más de lo que parece
&lt;/h2&gt;

&lt;p&gt;Si mirás la lista pública de Java Champions en 2026, la representación de América Latina es notablemente escasa en relación al tamaño de la comunidad de desarrolladores de la región. Brasil tiene algunos nombres — en parte porque hay una cultura de JUG activa y eventos técnicos consolidados como TDC. El resto de la región hispana aparece muy poco.&lt;/p&gt;

&lt;p&gt;Hay varias hipótesis para eso. La más fácil de descartar es la competencia técnica — la región tiene arquitectos y devs senior de primer nivel. Las hipótesis más probables son estructurales:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Visibilidad de idioma&lt;/strong&gt;: el ecosistema técnico internacional opera mayoritariamente en inglés. Contribuciones en español tienen menos alcance cross-community aunque tengan igual o mayor profundidad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ausencia de nodos de nominación&lt;/strong&gt;: si no tenés Champions cerca que puedan proponer y respaldar tu trabajo, la cadena no se activa.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cultura de contenido técnico&lt;/strong&gt;: en la región hay más tradición de aprender que de publicar. Muchos devs mid-senior consumen contenido en inglés y no producen en ningún idioma.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Mi postura sobre esto es clara: &lt;strong&gt;el contenido técnico de calidad en español es una contribución legítima al ecosistema Java global&lt;/strong&gt;, no una versión degradada de contribuir en inglés. Es resolver un problema de acceso real para una comunidad enorme de profesionales que aprende y trabaja en español. Un artículo técnico preciso sobre Spring Boot 3, OpenTelemetry o arquitectura de sistemas, escrito en español rioplatense con profundidad real, llega a un segmento de la comunidad que el contenido en inglés no llega. Eso tiene valor. Lo incómodo es que ese valor todavía no se traduce en reconocimiento formal, y no tengo evidencia pública de que eso vaya a cambiar solo porque yo lo diga.&lt;/p&gt;

&lt;p&gt;¿Puede cambiar de todos modos? Creo que sí, y la única forma que conozco es construir el corpus de contenido, hacerlo consistente, conectarlo con la comunidad de JUGs hispanohablantes, y hacer visible el trabajo. No hay un shortcut ahí — es trabajo acumulativo, sin garantía.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checklist honesto: qué construye una candidatura real en 2026
&lt;/h2&gt;

&lt;p&gt;No tengo acceso al proceso interno de nominación ni a los criterios no documentados. Lo que sí puedo hacer es armar una checklist basada en la evidencia pública del programa y los patrones observables en los perfiles de Champions existentes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Contenido técnico sostenido
   - Blog o canal con publicaciones regulares (no virales esporádicas)
   - Profundidad técnica verificable: código real, criterios de decisión, trade-offs honestos
   - Cobertura del ecosistema Java: JVM, frameworks, patrones, tooling

✅ Participación en comunidad Java estructurada
   - JUG membership o liderazgo (hay JUGs activos en Argentina, México, Colombia, Perú)
   - Presentaciones técnicas en meetups o conferencias
   - Interacción pública con otros miembros del ecosistema

✅ Contribuciones open source observables
   - PRs mergeados en proyectos Java relevantes
   - Issues con análisis técnico genuino
   - Proyectos propios con adopción o utilidad demostrable

✅ Red dentro del programa
   - Conexión con Java Champions existentes (no networking vacío: conexión basada en trabajo real)
   - Visibilidad en espacios donde Champions participan

❌ Lo que probablemente NO alcanza solo
   - Certificaciones Oracle (necesarias para la carrera, no para este reconocimiento)
   - Audiencia grande sin profundidad técnica
   - Contribuciones privadas sin superficie pública
   - Un año de actividad intensa sin historial previo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lo honesto es decir que no sé cuánto tiempo toma ni cuál es el umbral mínimo en ninguna de esas dimensiones — porque la fuente oficial no lo especifica y sería irresponsable inventarlo. El límite de este checklist es justamente eso: es inferencia de patrones públicos, no una fórmula garantizada.&lt;/p&gt;




&lt;h2&gt;
  
  
  Por qué lo declaro en público como objetivo
&lt;/h2&gt;

&lt;p&gt;Hay algo incómodo en declarar un objetivo de reconocimiento en público. Suena a ego, a vanidad de LinkedIn, a optimizar para el título antes que para el trabajo. Lo entiendo, y esa incomodidad no se va del todo aunque lo escriba con cuidado. Aun así lo hago, por una razón técnica de comunidad, no por vanidad.&lt;/p&gt;

&lt;p&gt;El contenido de este blog — los posts sobre &lt;a href="https://juanchi.dev/es/blog/useeffect-sincronizar-estado-alternativa-react-19" rel="noopener noreferrer"&gt;useEffect y sincronización de estado&lt;/a&gt;, sobre &lt;a href="https://juanchi.dev/es/blog/prisma-server-actions-nextjs-16-n1-produccion" rel="noopener noreferrer"&gt;Prisma y Server Actions en Next.js&lt;/a&gt;, sobre &lt;a href="https://juanchi.dev/es/blog/spring-boot-startup-time-2026-graalvm-native-aot-cds" rel="noopener noreferrer"&gt;Spring Boot y startup time en 2026&lt;/a&gt;, sobre &lt;a href="https://juanchi.dev/es/blog/opentelemetry-spring-boot-logs-vs-traces-diagnostico" rel="noopener noreferrer"&gt;OpenTelemetry y la diferencia entre logs y traces&lt;/a&gt; — no existe para construir un CV. Existe porque hay una brecha real de contenido técnico de calidad en español, y llenarla es el trabajo, con o sin título.&lt;/p&gt;

&lt;p&gt;Declarar Java Champion como objetivo no cambia el tipo de contenido que produzco. Sí hace explícito por qué ese contenido importa más allá de un post individual: es parte de un corpus, de una contribución sostenida, de un argumento que se construye en el tiempo. Por qué escribo sobre Java y no solo sobre Next.js. Por qué cada post apunta a profundidad técnica real y no a volumen. Por qué me importa conectar con JUGs y no solo publicar en el vacío, sin feedback de nadie que conozca el terreno.&lt;/p&gt;

&lt;p&gt;El &lt;a href="https://juanchi.dev/es/blog/show-needle-distilled-gemini-tool-calling-modelo-pequeno-analisis" rel="noopener noreferrer"&gt;análisis de Needle y tool calling en modelos pequeños&lt;/a&gt; que escribí la semana pasada es un ejemplo de lo que quiero decir: no es contenido para devs que quieren respuestas rápidas. Es contenido para devs que quieren entender el criterio técnico detrás de una decisión. Ese es el tipo de contribución que apunta en la dirección correcta — aunque no tenga forma de medir hoy si efectivamente suma.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;¿Cuál es la diferencia entre Java Champion y Oracle ACE?&lt;/strong&gt;&lt;br&gt;
Oracle ACE es otro programa de reconocimiento de Oracle, con criterios y proceso distintos. Java Champion está específicamente orientado al ecosistema Java y la comunidad técnica asociada. Podés ser Oracle ACE sin ser Java Champion y viceversa. Ambos tienen valor, pero apuntan a perfiles y contribuciones ligeramente distintos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Hay algún costo o formulario de postulación para Java Champion?&lt;/strong&gt;&lt;br&gt;
No hay costo. Y no hay un formulario de postulación abierto: el proceso es por nominación de Champions existentes. Según la información pública del programa en &lt;a href="https://developer.oracle.com/javachampions/" rel="noopener noreferrer"&gt;developer.oracle.com/javachampions&lt;/a&gt;, Oracle evalúa las nominaciones pero no las genera de forma unilateral.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿El contenido en español cuenta como contribución al ecosistema Java?&lt;/strong&gt;&lt;br&gt;
Basándome en los criterios públicos del programa, sí — el contenido técnico de calidad es una contribución válida independientemente del idioma. Lo que no sé con certeza es cuánto peso tiene en la práctica del proceso de nominación, porque ese detalle no está documentado. Mi postura es que debería contar, y que construir ese argumento requiere demostrar calidad técnica real, no solo volumen de publicaciones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Hay Java Champions hispanohablantes?&lt;/strong&gt;&lt;br&gt;
Sí, aunque la representación es escasa en relación al tamaño de la comunidad. Brasil tiene más presencia histórica en el programa, en parte por la actividad de JUGs como SouJava. El espacio hispanohablante tiene margen significativo de crecimiento.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué es un JUG y cómo conectarse con uno?&lt;/strong&gt;&lt;br&gt;
JUG significa Java User Group — grupos de la comunidad Java organizados por región o ciudad. Hay JUGs activos en Argentina, México, Colombia y otros países. La &lt;a href="https://developer.oracle.com/java/jug/" rel="noopener noreferrer"&gt;lista oficial de JUGs&lt;/a&gt; está en el sitio de Oracle. Participar en un JUG es una de las formas más directas de conectarse con la comunidad Java estructurada de la región.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿En cuánto tiempo se puede alcanzar el reconocimiento?&lt;/strong&gt;&lt;br&gt;
No lo sé, y no lo afirmaría sin evidencia. Los perfiles de Champions existentes muestran contribuciones sostenidas durante años — no sprints de visibilidad. Lo que sí puedo decir: empezar mañana a construir es mejor que esperar al momento perfecto.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cierre: el argumento que se construye publicación por publicación
&lt;/h2&gt;

&lt;p&gt;Java Champion en 2026 no es un título que se persigue directamente clickeando algún botón. Es la consecuencia de un trabajo que tiene que importar aunque nunca llegue ese reconocimiento específico. Si el único valor del contenido fuera el título, no valdría la pena escribirlo un domingo a la noche en vez de estar haciendo cualquier otra cosa.&lt;/p&gt;

&lt;p&gt;Lo que sostengo con postura clara: &lt;strong&gt;el contenido técnico en español es una brecha no resuelta del ecosistema Java&lt;/strong&gt;, y llenarla no es caridad ni relleno de portfolio — es trabajo técnico real con destinatario real. Hay cientos de miles de devs en Latinoamérica que aprenden Java, trabajan con Spring Boot, despliegan en producción y toman decisiones técnicas complejas, y tienen poco contenido de calidad en su idioma que respalde esas decisiones con profundidad real. El reconocimiento eventual es consecuencia, no objetivo primario — y si nunca llega, la brecha sigue mereciendo que alguien la llene.&lt;/p&gt;

&lt;p&gt;El próximo paso práctico si esto resuena con vos: conectate con el JUG de tu ciudad o región. No para sumar una línea al CV — para encontrar la red que hace visible el trabajo que ya estás haciendo. Y si no hay JUG activo cerca, esa ausencia es en sí misma un dato sobre por qué la brecha existe.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Java Champions Program — Oracle: &lt;a href="https://developer.oracle.com/javachampions/" rel="noopener noreferrer"&gt;https://developer.oracle.com/javachampions/&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/java-champion-2026-que-es-como-ser-reconocido" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>opensource</category>
      <category>springboot</category>
    </item>
    <item>
      <title>Stateless JWT vs stateful sessions: the framework I use to choose in identity systems</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 18 Aug 2026 12:00:33 +0000</pubDate>
      <link>https://dev.to/jtorchia/stateless-jwt-vs-stateful-sessions-the-framework-i-use-to-choose-in-identity-systems-47ib</link>
      <guid>https://dev.to/jtorchia/stateless-jwt-vs-stateful-sessions-the-framework-i-use-to-choose-in-identity-systems-47ib</guid>
      <description>&lt;h1&gt;
  
  
  Stateless JWT vs stateful sessions: the framework I use to choose in identity systems
&lt;/h1&gt;

&lt;p&gt;I was reviewing the token validation architecture of an identity backend when I found something that genuinely bothered me: the system was issuing JWTs with 24-hour expiration and there was zero revocation mechanism. If a token got compromised, the only remedy was waiting for it to expire. Twenty-four hours of window for an attacker holding a valid credential.&lt;/p&gt;

&lt;p&gt;I asked why. The answer was the usual one: "JWT is stateless, it scales better, doesn't need a database." That line isn't wrong on its face — but nobody in that conversation could tell me what would happen the day a token actually leaked. That gap between the slogan and the incident response plan is the part that bothered me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My thesis&lt;/strong&gt;: stateless JWT is premature optimization in most identity systems. If you need immediate revocation or fine-grained auditing, state isn't the enemy — it's exactly what you need. The debate isn't "JWT bad, sessions good": it's about when the cost of stateless outweighs the benefit.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the choice actually means
&lt;/h2&gt;

&lt;p&gt;The dichotomy gets oversimplified way too often. Stateless JWT means the server doesn't need to query any store to validate a token: all the information is in the token itself, signed. That's genuinely valuable in certain contexts. The problem is when that design gets applied without asking what happens when something goes wrong.&lt;/p&gt;

&lt;p&gt;With pure stateless JWT you have two levers: the expiration time (&lt;code&gt;exp&lt;/code&gt;) and the signature. If the secret or private key hasn't been compromised, any signed and valid token is... valid. Full stop. There's no "revoke this specific token" without adding state somewhere.&lt;/p&gt;

&lt;p&gt;Stateful sessions flip that trade-off: the server keeps the session somewhere it controls — memory, Redis, a database — and can kill it on demand. The cost moves to the store: if Redis doesn't respond, validation fails. That's a real cost that shouldn't be minimized.&lt;/p&gt;

&lt;p&gt;The mistake isn't picking one or the other. The mistake is not asking &lt;strong&gt;what level of control the system you're working on actually needs&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What RFC 7009 says — and what it doesn't
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7009" rel="noopener noreferrer"&gt;RFC 7009 — OAuth 2.0 Token Revocation&lt;/a&gt; is the standard that defines how a client can request token revocation from an Authorization Server. It defines the &lt;code&gt;/revoke&lt;/code&gt; endpoint, the expected parameters, and server behavior.&lt;/p&gt;

&lt;p&gt;What the RFC explicitly says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Authorization Server &lt;strong&gt;should&lt;/strong&gt; revoke dependent tokens when a refresh token is revoked (section 4.1).&lt;/li&gt;
&lt;li&gt;Revoking a JWT access token doesn't eliminate the token from the world: it only registers that it was revoked on the server implementing the endpoint.&lt;/li&gt;
&lt;li&gt;The spec &lt;strong&gt;does not define&lt;/strong&gt; how the Resource Server finds out a token was revoked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the one tutorials most consistently skip. RFC 7009 solves the communication between client and Authorization Server. It does not solve the problem that a Resource Server validating JWTs in a fully stateless manner &lt;strong&gt;has no way of knowing&lt;/strong&gt; that token was revoked — unless it goes and queries the Authorization Server or a shared store.&lt;/p&gt;

&lt;p&gt;Spring Security documents this clearly in its &lt;a href="https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html" rel="noopener noreferrer"&gt;OAuth2 Resource Server guide&lt;/a&gt;: default JWT validation is local (signature verification + claims like &lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;nbf&lt;/code&gt;, &lt;code&gt;iss&lt;/code&gt;). For active revocation you need to implement token introspection or your own blocklist mechanism.&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;// Default stateless JWT validation in Spring Security&lt;/span&gt;
&lt;span class="c1"&gt;// Only verifies signature, exp, iss — does NOT query any external store&lt;/span&gt;
&lt;span class="n"&gt;http&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;oauth2ResourceServer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;oauth2&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jwt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwt&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;decoder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NimbusJwtDecoder&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withJwkSetUri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://auth.example.com/.well-known/jwks.json"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// For real revocation you need active token introspection&lt;/span&gt;
&lt;span class="c1"&gt;// The Resource Server queries the Authorization Server on every request&lt;/span&gt;
&lt;span class="n"&gt;http&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;oauth2ResourceServer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;oauth2&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;opaqueToken&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opaque&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;opaque&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;introspectionUri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://auth.example.com/introspect"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;introspectionClientCredentials&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"client-id"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"client-secret"&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;The second option adds per-request latency. That's the honest cost of real revocation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where people go wrong: the hidden cost of stateless
&lt;/h2&gt;

&lt;p&gt;The most common argument for stateless JWT in identity systems is scalability: "no state, no coordination between instances, horizontal scaling for free." It's a valid argument for public read APIs with short-lived tokens. For an identity system with real users, it's frequently a mirage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hidden cost number one: long compromise windows.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're issuing tokens with 1-hour-or-more expiration and no revocation, a stolen credential has a proportional attack window. In an identity system where the token grants access to sensitive operations — profile changes, document signing, access to personal data — that window matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hidden cost number two: impossible auditing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Identity systems in regulated contexts or with compliance requirements need to know which token was used, when, from which IP, for which operation. With pure stateless JWT, that information doesn't exist on the server unless you explicitly log it on every Resource Server. If you have multiple services validating the same JWT, the audit trail ends up fragmented or simply absent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The concrete counterexample:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine a system where a user reports their account was compromised. With stateful sessions in Redis, the response is immediate:&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;# Invalidate all active sessions for the user — immediate response&lt;/span&gt;
redis-cli DEL &lt;span class="s2"&gt;"session:user:abc123"&lt;/span&gt;
&lt;span class="c"&gt;# Or with a pattern if you have multiple sessions per user&lt;/span&gt;
redis-cli &lt;span class="nt"&gt;--scan&lt;/span&gt; &lt;span class="nt"&gt;--pattern&lt;/span&gt; &lt;span class="s2"&gt;"session:user:abc123:*"&lt;/span&gt; | xargs redis-cli DEL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With pure stateless JWT, the response is: "we wait for them to expire." Or you implement a blocklist — which means adding state, exactly what you were trying to avoid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stateless JWT actually does well:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short-lived tokens (minutes, not hours) with long-lived refresh tokens and refresh revocation.&lt;/li&gt;
&lt;li&gt;Internal service-to-service APIs where tokens don't represent user sessions.&lt;/li&gt;
&lt;li&gt;Contexts where introspection latency is prohibitive and the compromise risk is low.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Decision matrix: when to use each approach
&lt;/h2&gt;

&lt;p&gt;Before choosing, answer these questions. They're the ones I use as a filter in any identity system design:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Stateless JWT&lt;/th&gt;
&lt;th&gt;Stateful (session / token store)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Do you need to revoke individual tokens immediately?&lt;/td&gt;
&lt;td&gt;❌ Not without a blocklist&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do you have per-session auditing requirements?&lt;/td&gt;
&lt;td&gt;❌ Complex&lt;/td&gt;
&lt;td&gt;✅ Natural&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do tokens represent end-user sessions?&lt;/td&gt;
&lt;td&gt;⚠️ Watch out with long exp&lt;/td&gt;
&lt;td&gt;✅ Better fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Are these short-lived machine-to-machine tokens?&lt;/td&gt;
&lt;td&gt;✅ Ideal&lt;/td&gt;
&lt;td&gt;⚠️ Unnecessary overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is horizontal scaling without coordination critical?&lt;/td&gt;
&lt;td&gt;✅ Real advantage&lt;/td&gt;
&lt;td&gt;⚠️ Requires shared store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do you have per-request latency budget for introspection?&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅ Required&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The alarm checklist for stateless JWT:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Access token expiration longer than 30 minutes for end users
[ ] No documented revocation mechanism
[ ] Sensitive operations authorized by token alone (no second validation)
[ ] Session auditing required by regulation or internal policy
[ ] Multiple Resource Servers with no shared store for blocklist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you check two or more, pure stateless is probably not the right architecture for that system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pattern I use most in practice:&lt;/strong&gt; short-lived JWT (15 minutes) + opaque refresh token with state in Redis. The access token is stateless for fast per-request validation. The refresh token is stateful and revocable. The compromise window is capped at the 15-minute access token lifetime — reasonable for most scenarios.&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;// Typical token configuration on an Authorization Server with Spring Security&lt;/span&gt;
&lt;span class="c1"&gt;// Short access token, revocable refresh token stored in Redis&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;TokenSettings&lt;/span&gt; &lt;span class="nf"&gt;tokenSettings&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="nc"&gt;TokenSettings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;// Short window for stateless — max revocation delay 15min&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accessTokenTimeToLive&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofMinutes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="c1"&gt;// Long-lived refresh token, revocable in Redis&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refreshTokenTimeToLive&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofDays&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="c1"&gt;// Controlled reuse: each refresh rotates the token&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reuseRefreshTokens&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&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 pattern appears in the OAuth 2.0 spec (RFC 6749) as recommended practice for reducing the exposure window without completely sacrificing the stateless benefit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common mistakes and gotchas that surface late
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"The JWT has all the necessary info, I don't need anything else."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This becomes a problem when that "necessary info" changes before the token expires. Updated user roles, suspended account, org change — with stateless JWT, the info in the token can be stale for its entire lifetime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confusing stateless with simple.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Implementing stateless JWT correctly in an identity system requires key rotation, a JWKS endpoint, claims validation, clock skew handling, and refresh token management. It's not less code than a well-implemented session; it's different code with different failure points.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blocklist without TTL.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you add a blocklist for revocation, make sure entries have a TTL equal to the token's expiration time. A blocklist that grows indefinitely is a slow memory leak. Redis with &lt;code&gt;EXPIRE&lt;/code&gt; solves this in one line:&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;# Add token to blocklist with TTL equal to remaining expiration time&lt;/span&gt;
&lt;span class="c"&gt;# Assuming you calculate remaining seconds before adding&lt;/span&gt;
redis-cli SET &lt;span class="s2"&gt;"blocklist:jti:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN_JTI&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"revoked"&lt;/span&gt; EX &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SECONDS_UNTIL_EXP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ignoring &lt;code&gt;jti&lt;/code&gt; (JWT ID).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;jti&lt;/code&gt; claim defined in RFC 7519 is the token's unique identifier. It's what you need for an efficient blocklist. If you're not issuing it, revoking individual tokens gets much harder — you'd have to revoke by &lt;code&gt;sub&lt;/code&gt; (user), which is more aggressive and can affect other legitimate sessions.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ: JWT vs stateful sessions in identity systems
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is stateless JWT insecure by nature?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Stateless JWT is insecure when used in contexts where active session control is a non-negotiable requirement. The mechanism itself, correctly signed with asymmetric algorithms (RS256, ES256), is solid. The problem is the semantics of "this token is valid until it expires" in systems where you need to say "this token is no longer valid" before that moment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I have the best of both worlds?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, with the hybrid pattern: short-lived stateless JWT access token + opaque stateful refresh token. The cost is the added complexity of the refresh flow. Worth it in most identity systems with end users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Doesn't token introspection solve everything?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It solves revocation, yes. The cost is a call to the Authorization Server on every validation request — additional latency that can be significant depending on volume. For high-frequency internal microservices, the cost may not be justified. For lower-frequency end-user endpoints, it's usually acceptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about traditional session cookies vs JWT?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They're different mechanisms at different layers. JWT is a token format; cookies are a transport mechanism. You can transport JWT in an httpOnly+Secure cookie and get XSS protection while still using the JWT format. The "JWT vs cookies" debate usually mixes these layers and creates more confusion than clarity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Spring Security support both approaches?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. For stateless JWT you use the &lt;a href="https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html" rel="noopener noreferrer"&gt;Resource Server with JWT decoder&lt;/a&gt;. For active introspection you use the opaque token support with the introspection endpoint. For traditional sessions, the &lt;code&gt;HttpSession&lt;/code&gt; support with Redis or JDBC is well documented in Spring Session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the architecture described in &lt;a href="https://juanchi.dev/en/blog/digital-identity-backend-architecture-decisions-tutorials-skip" rel="noopener noreferrer"&gt;the identity architecture decisions post&lt;/a&gt; address this at the root?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Identity architecture decisions and the JWT vs state choice are orthogonal but related. A good identity architecture should force this question before issuing the first token, not after the system is already in production. That post covers the "what to build"; this one covers the "how to validate what you emit."&lt;/p&gt;




&lt;h2&gt;
  
  
  The state isn't the enemy — ambiguity is
&lt;/h2&gt;

&lt;p&gt;The industry went through a "stateless everywhere" phase that led a lot of identity systems to optimize for horizontal scaling before they had any real scaling problem. The frequent result: systems that can't revoke tokens, can't audit sessions, and have no operational response when something gets compromised.&lt;/p&gt;

&lt;p&gt;The uncomfortable part is that stateless JWT has genuine advantages. I'm not dismissing them. What I don't buy is treating "stateless" as a default setting instead of a deliberate trade-off. In identity systems — where the question "who is this user and are they still valid?" has real consequences — the cost of stateless rigidity shows up sooner than tutorials promise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My practical recommendation:&lt;/strong&gt; start with the hybrid pattern (short access token + opaque revocable refresh token). If store overhead is a real measured problem, look at whether you can reduce the access token TTL before eliminating state from the refresh. Pure stateless is an optimization for later, not the starting point.&lt;/p&gt;

&lt;p&gt;The concrete next step: if you have a system issuing JWTs with expiration longer than 30 minutes and no blocklist, read &lt;a href="https://datatracker.ietf.org/doc/html/rfc7009" rel="noopener noreferrer"&gt;RFC 7009&lt;/a&gt; to understand what you still need to implement for real revocation. This isn't theory — it's the contract the OAuth ecosystem expects you to fulfill. And if you can't answer "how do we revoke this token right now" in one sentence, that's your actual bug ticket.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related reading:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://juanchi.dev/en/blog/digital-identity-backend-architecture-decisions-tutorials-skip" rel="noopener noreferrer"&gt;Digital identity backend architecture: the decisions tutorials leave out&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://juanchi.dev/en/blog/digital-signature-format-certificate-validation-policy-layers" rel="noopener noreferrer"&gt;Digital signature: format, certificate, and validation policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://juanchi.dev/en/blog/spring-boot-payara-glassfish-benchmark-java-enterprise" rel="noopener noreferrer"&gt;The benchmark that changed my mind about Jakarta EE in 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Primary sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OAuth 2.0 Token Revocation RFC 7009: &lt;a href="https://datatracker.ietf.org/doc/html/rfc7009" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc7009&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Spring Security OAuth2 Resource Server: &lt;a href="https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html" rel="noopener noreferrer"&gt;https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.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/stateless-jwt-vs-stateful-sessions-identity-systems" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>seguridad</category>
      <category>jwt</category>
      <category>arquitectura</category>
    </item>
    <item>
      <title>JWT sin estado vs sesiones con estado: el criterio que uso para elegir en sistemas de identidad</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 18 Aug 2026 12:00:27 +0000</pubDate>
      <link>https://dev.to/jtorchia/jwt-sin-estado-vs-sesiones-con-estado-el-criterio-que-uso-para-elegir-en-sistemas-de-identidad-2k5l</link>
      <guid>https://dev.to/jtorchia/jwt-sin-estado-vs-sesiones-con-estado-el-criterio-que-uso-para-elegir-en-sistemas-de-identidad-2k5l</guid>
      <description>&lt;h1&gt;
  
  
  JWT sin estado vs sesiones con estado: el criterio que uso para elegir en sistemas de identidad
&lt;/h1&gt;

&lt;p&gt;Estaba revisando la arquitectura de validación de tokens de un backend de identidad cuando me encontré con algo que me incomodó bastante: el sistema emitía JWT con expiración de 24 horas y no había ningún mecanismo de revocación. Si un token se comprometía, el único remedio era esperar que expirara. Veinticuatro horas de ventana para un atacante con credencial válida.&lt;/p&gt;

&lt;p&gt;Pregunté por qué. La respuesta fue la de siempre: "JWT es stateless, escala mejor, no necesita base de datos." Como argumento de escalabilidad, no está mal — pero acá lo que estaba en juego no era escala, era revocación. Y ahí la respuesta se cae.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mi tesis&lt;/strong&gt;: JWT stateless es una optimización prematura en la mayoría de sistemas de identidad. Si necesitás revocación inmediata o auditoría fina, el estado no es el enemigo — es exactamente lo que necesitás. El debate no es "JWT malo, sesiones buenas": es cuándo el costo de stateless supera el beneficio.&lt;/p&gt;




&lt;h2&gt;
  
  
  jwt vs sesiones con estado identidad digital criterio: qué significa realmente la elección
&lt;/h2&gt;

&lt;p&gt;La dicotomía se simplifica demasiado seguido. JWT stateless significa que el servidor no necesita consultar ningún store para validar un token: toda la información está en el token mismo, firmada. Eso es genuinamente valioso en ciertos contextos. El problema es cuando ese diseño se aplica sin preguntarse qué pasa cuando algo sale mal.&lt;/p&gt;

&lt;p&gt;Con JWT stateless puro tenés dos palancas: el tiempo de expiración (&lt;code&gt;exp&lt;/code&gt;) y la firma. Si el secreto o la clave privada no se comprometieron, cualquier token firmado y vigente es válido. Punto. No hay "revocar este token específico" sin agregar estado en algún lado.&lt;/p&gt;

&lt;p&gt;Las sesiones con estado son otra cosa: el servidor guarda un registro de la sesión (en memoria, Redis, base de datos) y puede darlo de baja cuando quiera, sin esperar a que nada expire. El trade-off es la dependencia del store: si Redis no responde, la validación falla. Eso es un costo real que no debería minimizarse.&lt;/p&gt;

&lt;p&gt;El error no es elegir uno u otro. El error es no preguntarse &lt;strong&gt;qué nivel de control necesita el sistema en el que estás trabajando&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lo que dice el RFC 7009 — y lo que no dice
&lt;/h2&gt;

&lt;p&gt;El &lt;a href="https://datatracker.ietf.org/doc/html/rfc7009" rel="noopener noreferrer"&gt;RFC 7009 — OAuth 2.0 Token Revocation&lt;/a&gt; es el estándar que define cómo un cliente puede solicitar la revocación de un token ante el Authorization Server. Define el endpoint &lt;code&gt;/revoke&lt;/code&gt;, los parámetros esperados y el comportamiento del servidor.&lt;/p&gt;

&lt;p&gt;Lo que el RFC dice explícitamente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El Authorization Server &lt;strong&gt;debería&lt;/strong&gt; revocar los tokens dependientes cuando se revoca un refresh token (sección 4.1).&lt;/li&gt;
&lt;li&gt;La revocación de un access token JWT no elimina el token del mundo: solo registra que fue revocado en el servidor que implementa el endpoint.&lt;/li&gt;
&lt;li&gt;La especificación &lt;strong&gt;no define&lt;/strong&gt; cómo el Resource Server se entera de que un token fue revocado.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ese último punto es el que más se omite en los tutoriales. El RFC 7009 resuelve la comunicación entre cliente y Authorization Server. No resuelve el problema de que un Resource Server validando JWT de forma completamente stateless &lt;strong&gt;no tiene forma de saber&lt;/strong&gt; que ese token fue revocado, a menos que vaya a consultar al Authorization Server o a un store compartido.&lt;/p&gt;

&lt;p&gt;Spring Security documenta esto con claridad en su &lt;a href="https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html" rel="noopener noreferrer"&gt;guía de OAuth2 Resource Server&lt;/a&gt;: la validación JWT por defecto es local (verificación de firma + claims como &lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;nbf&lt;/code&gt;, &lt;code&gt;iss&lt;/code&gt;). Para revocación activa necesitás implementar token introspection o un mecanismo propio de blocklist.&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;// Validación JWT stateless por defecto en Spring Security&lt;/span&gt;
&lt;span class="c1"&gt;// Solo verifica firma, exp, iss — NO consulta ningún store externo&lt;/span&gt;
&lt;span class="n"&gt;http&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;oauth2ResourceServer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;oauth2&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jwt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwt&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;decoder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NimbusJwtDecoder&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withJwkSetUri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://auth.ejemplo.com/.well-known/jwks.json"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Para revocación real necesitás token introspection activa&lt;/span&gt;
&lt;span class="c1"&gt;// El Resource Server consulta al Authorization Server en cada request&lt;/span&gt;
&lt;span class="n"&gt;http&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;oauth2ResourceServer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;oauth2&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;opaqueToken&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opaque&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;opaque&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;introspectionUri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://auth.ejemplo.com/introspect"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;introspectionClientCredentials&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"client-id"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"client-secret"&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;La segunda opción tiene latencia por request adicional. Eso es el costo honesto de la revocación real.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dónde se equivoca la gente: el costo oculto del stateless
&lt;/h2&gt;

&lt;p&gt;El argumento más común a favor de JWT stateless en sistemas de identidad es la escalabilidad: "sin state, sin coordinación entre instancias, horizontal scaling gratis." Es un argumento válido para APIs públicas de lectura con tokens de corta vida. Para un sistema de identidad con usuarios reales, es frecuentemente un espejismo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El costo oculto número uno: ventanas de compromiso largas.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Si emitís tokens con expiración de 1 hora o más y no tenés revocación, una credencial robada tiene una ventana de ataque proporcional. En un sistema de identidad donde el token da acceso a operaciones sensibles — cambios de perfil, firma de documentos, acceso a datos personales — esa ventana importa.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El costo oculto número dos: auditoría imposible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Los sistemas de identidad en contextos regulados o con requisitos de compliance necesitan saber qué token se usó, cuándo, desde qué IP, para qué operación. Con JWT stateless puro, esa información no existe en el servidor a menos que la loguees explícitamente en cada Resource Server. Si tenés varios servicios validando el mismo JWT, la auditoría queda fragmentada o directamente ausente.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El caso tipico que uso para explicarlo:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pensá en un usuario que reporta que le comprometieron la cuenta. Con sesiones con estado en Redis, la respuesta es inmediata:&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;# Invalidar todas las sesiones activas del usuario — respuesta inmediata&lt;/span&gt;
redis-cli DEL &lt;span class="s2"&gt;"session:usuario:abc123"&lt;/span&gt;
&lt;span class="c"&gt;# O con un patrón si tenés múltiples sesiones por usuario&lt;/span&gt;
redis-cli &lt;span class="nt"&gt;--scan&lt;/span&gt; &lt;span class="nt"&gt;--pattern&lt;/span&gt; &lt;span class="s2"&gt;"session:usuario:abc123:*"&lt;/span&gt; | xargs redis-cli DEL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con JWT stateless puro, la respuesta es: "esperamos que expiren." O implementás una blocklist, que es agregar estado — exactamente lo que querías evitar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lo que sí funciona bien con JWT stateless:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tokens de corta vida (minutos, no horas) con refresh tokens de larga vida y revocación del refresh.&lt;/li&gt;
&lt;li&gt;APIs internas entre servicios donde los tokens no representan sesiones de usuario.&lt;/li&gt;
&lt;li&gt;Contextos donde la latencia de introspección es prohibitiva y el riesgo de compromiso es bajo.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Matriz de decisión: cuándo usar cada enfoque
&lt;/h2&gt;

&lt;p&gt;Antes de elegir, respondé estas preguntas. Son las que yo uso como filtro en cualquier diseño de sistema de identidad:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterio&lt;/th&gt;
&lt;th&gt;Stateless JWT&lt;/th&gt;
&lt;th&gt;Con estado (sesión / token store)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;¿Necesitás revocar tokens individuales inmediatamente?&lt;/td&gt;
&lt;td&gt;❌ No sin blocklist&lt;/td&gt;
&lt;td&gt;✅ Sí&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Tenés requisitos de auditoría por sesión?&lt;/td&gt;
&lt;td&gt;❌ Complejo&lt;/td&gt;
&lt;td&gt;✅ Natural&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Los tokens representan sesiones de usuario final?&lt;/td&gt;
&lt;td&gt;⚠️ Cuidado con exp largo&lt;/td&gt;
&lt;td&gt;✅ Mejor fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Son tokens máquina-a-máquina de corta vida?&lt;/td&gt;
&lt;td&gt;✅ Ideal&lt;/td&gt;
&lt;td&gt;⚠️ Overhead innecesario&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Escala horizontal sin coordinación es crítica?&lt;/td&gt;
&lt;td&gt;✅ Ventaja real&lt;/td&gt;
&lt;td&gt;⚠️ Requiere store compartido&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Tenés latencia disponible por request para introspección?&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅ Necesario&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;El checklist de alarma para JWT stateless:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Expiración mayor a 30 minutos en tokens de acceso de usuarios finales
[ ] Sin mecanismo de revocación documentado
[ ] Operaciones sensibles autorizadas solo por el token (sin segunda validación)
[ ] Auditoría de sesiones requerida por regulación o política interna
[ ] Múltiples Resource Servers sin store compartido para blocklist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si marcás dos o más, stateless puro probablemente no sea la arquitectura correcta para ese sistema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El patrón que más uso en práctica:&lt;/strong&gt; JWT de corta vida (15 minutos) + refresh token opaco con estado en Redis. El access token es stateless para validación rápida en cada request. El refresh token es stateful y revocable. La ventana de compromiso queda acotada a los 15 minutos del access token — tiempo razonable para la mayoría de los escenarios.&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;// Configuración típica de token en un Authorization Server con Spring Security&lt;/span&gt;
&lt;span class="c1"&gt;// Access token corto, refresh token revocable almacenado en Redis&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;TokenSettings&lt;/span&gt; &lt;span class="nf"&gt;tokenSettings&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="nc"&gt;TokenSettings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;// Ventana corta para stateless — revocación máxima 15min&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accessTokenTimeToLive&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofMinutes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="c1"&gt;// Refresh token de larga vida, revocable en Redis&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refreshTokenTimeToLive&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofDays&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="c1"&gt;// Reutilización controlada: cada refresh rota el token&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reuseRefreshTokens&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&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 patrón aparece documentado en la especificación OAuth 2.0 (RFC 6749) como práctica recomendada para reducir la ventana de exposición sin sacrificar completamente el beneficio del stateless.&lt;/p&gt;




&lt;h2&gt;
  
  
  Errores comunes y gotchas que aparecen tarde
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"El JWT tiene toda la info necesaria, no necesito más."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Esta frase se convierte en problema cuando esa "info necesaria" cambia antes de que el token expire. Roles del usuario actualizados, cuenta suspendida, cambio de organización — con JWT stateless, la info en el token puede estar desactualizada durante toda su ventana de vida.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confundir stateless con simple.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Implementar JWT stateless correctamente en un sistema de identidad requiere rotación de claves, JWKS endpoint, validación de claims, manejo de clock skew y gestión de refresh tokens. No es menos código que una sesión bien implementada; es código diferente con distintos puntos de falla.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blocklist sin TTL.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Si agregás una blocklist para revocación, asegurate de que los registros tengan TTL igual al tiempo de expiración del token. Una blocklist que crece indefinidamente es un memory leak lento. Redis con &lt;code&gt;EXPIRE&lt;/code&gt; o similares resuelve esto con una línea:&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;# Agregar token a blocklist con TTL igual al tiempo restante de expiración&lt;/span&gt;
&lt;span class="c"&gt;# Asumiendo que calculás los segundos restantes antes de agregar&lt;/span&gt;
redis-cli SET &lt;span class="s2"&gt;"blocklist:jti:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN_JTI&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"revoked"&lt;/span&gt; EX &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SEGUNDOS_HASTA_EXP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ignorar el &lt;code&gt;jti&lt;/code&gt; (JWT ID).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;El claim &lt;code&gt;jti&lt;/code&gt; definido en RFC 7519 es el identificador único del token. Es lo que necesitás para una blocklist eficiente. Si no lo estás emitiendo, revocar tokens individuales se vuelve mucho más complejo — tendrías que revocar por &lt;code&gt;sub&lt;/code&gt; (usuario), que es más agresivo y puede afectar otras sesiones legítimas.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ sobre JWT vs sesiones con estado en sistemas de identidad
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿JWT stateless es inseguro por naturaleza?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. JWT stateless es inseguro cuando se usa en contextos donde el control de sesión activo es un requisito no negociable. El mecanismo en sí, firmado correctamente con algoritmos asimétricos (RS256, ES256), es sólido. El problema es la semántica de "este token es válido hasta que expire" en sistemas donde necesitás decir "este token ya no es válido" antes de ese momento.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Puedo tener lo mejor de ambos mundos?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sí, con el patrón híbrido: access token JWT stateless de corta vida + refresh token opaco con estado. El costo es la complejidad adicional del flujo de refresh. Vale la pena en la mayoría de sistemas de identidad con usuarios finales.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Token introspection no resuelve todo?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Resuelve la revocación, sí. El costo es una llamada al Authorization Server en cada request de validación — latencia adicional que puede ser significativa dependiendo del volumen. Para microservicios internos de alta frecuencia, el costo puede no justificarse. Para endpoints de usuario final con menor frecuencia, suele ser aceptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué pasa con las cookies de sesión tradicionales vs JWT?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Son mecanismos distintos en capas distintas. JWT es un formato de token; las cookies son un mecanismo de transporte. Podés transportar JWT en una cookie httpOnly+Secure y obtener protección contra XSS mientras usás el formato JWT. El debate "JWT vs cookies" suele mezclar estas capas y confundir más de lo que aclara.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Spring Security soporta ambos enfoques?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sí. Para stateless JWT usás el &lt;a href="https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html" rel="noopener noreferrer"&gt;Resource Server con JWT decoder&lt;/a&gt;. Para introspección activa usás el soporte de opaque tokens con el endpoint de introspección. Para sesiones tradicionales, el soporte de &lt;code&gt;HttpSession&lt;/code&gt; con Redis o JDBC está bien documentado en Spring Session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿La arquitectura que describís en &lt;a href="https://juanchi.dev/es/blog/arquitectura-backend-identidad-digital-jwt-oauth" rel="noopener noreferrer"&gt;el post sobre decisiones de arquitectura de identidad&lt;/a&gt; resuelve esto de raíz?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Las decisiones de arquitectura de identidad y la elección de JWT vs estado son ortogonales pero relacionadas. Una buena arquitectura de identidad debería forzar esta pregunta antes de emitir el primer token, no después de que el sistema esté en producción. Ese post cubre el "qué construir"; este cubre el "cómo validar lo que emitís".&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusión: el estado no es el enemigo, la ambigüedad sí
&lt;/h2&gt;

&lt;p&gt;La industria tuvo un momento de fascinación con "stateless everywhere" que llevó a muchos sistemas de identidad a optimizar para el caso de escala horizontal antes de tener ningún problema de escala real. El resultado frecuente: sistemas que no pueden revocar tokens, no pueden auditar sesiones y no tienen respuesta operativa cuando algo se compromete.&lt;/p&gt;

&lt;p&gt;Lo incómodo es que JWT stateless tiene ventajas genuinas. No las estoy descartando. Estoy diciendo que en sistemas de identidad — donde la pregunta "¿quién es este usuario y sigue siendo válido?" tiene consecuencias reales — el costo de la rigidez stateless aparece antes de lo que los tutoriales prometen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mi recomendación práctica:&lt;/strong&gt; empezá con el patrón híbrido (access token corto + refresh token opaco revocable). Si el overhead del store es un problema real medido, investigá si podés reducir el TTL del access token antes de eliminar el estado del refresh. Stateless puro es una optimización para más adelante, no el punto de partida.&lt;/p&gt;

&lt;p&gt;La pregunta incómoda que te dejo: si hoy alguien te reporta una cuenta comprometida, ¿cuánto tarda tu sistema en cortarle el acceso? Si la respuesta es "hasta que expire el token", ya sabés qué revisar primero — empezá por el &lt;a href="https://datatracker.ietf.org/doc/html/rfc7009" rel="noopener noreferrer"&gt;RFC 7009&lt;/a&gt; y fijate qué te falta implementar para revocación real. No es teoría: es el contrato que el ecosistema OAuth espera que implementes.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Lecturas relacionadas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://juanchi.dev/es/blog/arquitectura-backend-identidad-digital-jwt-oauth" rel="noopener noreferrer"&gt;Arquitectura backend de identidad digital: las decisiones que los tutoriales omiten&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://juanchi.dev/es/blog/firma-digital-formato-certificado-politica-validacion" rel="noopener noreferrer"&gt;Firma digital: formato, certificado y política de validación&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://juanchi.dev/es/blog/spring-boot-payara-glassfish-benchmark-java-enterprise" rel="noopener noreferrer"&gt;El benchmark que me hizo cambiar de opinión sobre Jakarta EE en 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Fuentes originales:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OAuth 2.0 Token Revocation RFC 7009: &lt;a href="https://datatracker.ietf.org/doc/html/rfc7009" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc7009&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Spring Security OAuth2 Resource Server: &lt;a href="https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html" rel="noopener noreferrer"&gt;https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.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/jwt-vs-sesiones-con-estado-identidad-digital-criterio" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>seguridad</category>
      <category>jwt</category>
    </item>
    <item>
      <title>Noroboto: Lying Fonts and Rust Mitigation — A Technical Read Without the Hype</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Mon, 17 Aug 2026 14:30:31 +0000</pubDate>
      <link>https://dev.to/jtorchia/noroboto-lying-fonts-and-rust-mitigation-a-technical-read-without-the-hype-4gaa</link>
      <guid>https://dev.to/jtorchia/noroboto-lying-fonts-and-rust-mitigation-a-technical-read-without-the-hype-4gaa</guid>
      <description>&lt;h1&gt;
  
  
  Noroboto: Lying Fonts and Rust Mitigation — A Technical Read Without the Hype
&lt;/h1&gt;

&lt;p&gt;Fonts are not reliable by default. Yeah, you read that right. The typographic subsystem can return width metrics, kerning, and advance width values that don't match what actually gets rendered — and that changes everything we thought we knew about "it's just text."&lt;/p&gt;

&lt;p&gt;That's what the Noroboto project documents: that the font stack on Linux can report metrics inconsistent with the effective render, and that Rust has something concrete to say about it. The problem isn't new, but the documentation is rare and the technical decision to adopt it is not trivial. My thesis before the first H2: &lt;strong&gt;reading the announcement and copying the dependency isn't enough — you need to turn this into a decision you actually own&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Problem Noroboto Points At
&lt;/h2&gt;

&lt;p&gt;When you render text in an application — whether it's an editor, a terminal, a visual linter, or anything that draws characters — you're depending on metrics the font system promises you. The width of a glyph, the space between characters, the bounding box. The thing is, those metrics may not match what the render engine actually paints on screen.&lt;/p&gt;

&lt;p&gt;This isn't some exotic bug. It's a consequence of layers: the shaper (usually HarfBuzz), the rasterizer (FreeType or similar), the window compositor, the display's DPI, and the hints embedded in the font itself. Each layer can introduce a discrepancy. If a project like Noroboto bothers to document this and build mitigations in Rust, it's because the problem shows up often enough that the ad-hoc solution — compensate by hand, ignore it, pray — stops being sustainable in certain stacks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My concrete point:&lt;/strong&gt; the value of Noroboto isn't that it discovers something new. It's that it formalizes the broken contract and proposes a mitigation surface with types. That matters if you're building something that depends on precise text layout. It matters a lot less if you're not.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Rust Mitigation Proposes and Why the Language Choice Matters
&lt;/h2&gt;

&lt;p&gt;Rust doesn't show up here because it's trendy. The choice has craft logic behind it: when the problem is that a set of returned metrics doesn't match the actual render, you want two things Rust gives you well — types that model the difference explicitly, and zero-cost abstractions so you don't pay overhead on the layout hot path.&lt;/p&gt;

&lt;p&gt;The pattern that emerges in projects like this looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Metrics "promised" by the font system&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PromisedMetrics&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;advance_width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bearing_x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bearing_y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Metrics observed after the actual render&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ObservedMetrics&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;actual_width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pixel_offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// The delta between promise and reality — this is what Noroboto mitigates&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;MetricsDelta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;width_error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cumulative_drift&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// error accumulates over long text runs&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;compute_delta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;promised&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;PromisedMetrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;observed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ObservedMetrics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MetricsDelta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;MetricsDelta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;width_error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;observed&lt;/span&gt;&lt;span class="py"&gt;.actual_width&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;promised&lt;/span&gt;&lt;span class="py"&gt;.advance_width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cumulative_drift&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// calculated in the context of a full line&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is that the &lt;code&gt;MetricsDelta&lt;/code&gt; type forces the rest of the code to acknowledge that a discrepancy exists. You can't implicitly ignore it the way you would with a loose float. That's type-driven design in service of a real invariant.&lt;/p&gt;

&lt;p&gt;Now — and this is the part I actually care about communicating — &lt;strong&gt;this pattern only works if you have a feedback loop between promised metrics and observed render&lt;/strong&gt;. Without that loop, modeling the difference is type bureaucracy, not real mitigation. A struct that nobody feeds with actual measurements is just decoration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where People Go Wrong Reading Projects Like This
&lt;/h2&gt;

&lt;p&gt;The classic mistake is grabbing the solution without understanding the usage contract. With Noroboto and similar projects, I see three recurring confusions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Assuming every font on every setup has this problem&lt;/strong&gt;&lt;br&gt;
It doesn't. This is the claim I want to be most careful with, because it's the one that turns a niche mitigation into unnecessary paranoia: well-hinted fonts in environments with a clean fontconfig and FreeType setup on Linux tend to show minor or negligible discrepancies for many use cases. I haven't run a controlled benchmark across distros to give you a hard number here, so take this as a working assumption to verify in your own stack, not a measured fact. The problem gets real with poorly-hinted fonts, on displays with non-standard DPI, or when subpixel rendering is disabled. Measure first, then mitigate — don't mitigate because the README sounds scary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Conflating text layout with text rendering&lt;/strong&gt;&lt;br&gt;
If you're building something that calculates text positions for UI (React, a canvas, a terminal multiplexer), the problem matters. If you're just rendering text on screen for the user to read, the discrepancies are usually sub-perceptual. The cost of the mitigation can easily exceed the benefit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Assuming Rust solves the problem by being Rust&lt;/strong&gt;&lt;br&gt;
The language gives you memory guarantees and lets you model the delta with types. It doesn't give you guarantees about the operating system's metrics. If FreeType or fontconfig hands you back a wrong number, Rust receives that wrong number just the same. The mitigation requires actual measurement, not just more precise types.&lt;/p&gt;

&lt;p&gt;This connects to something I learned staring at PostgreSQL execution plans for years: a well-placed index isn't magic, it's understanding the actual access pattern. Same thing here — a well-modeled type isn't magic, it's understanding what you're measuring.&lt;/p&gt;


&lt;h2&gt;
  
  
  Decision Checklist: When to Investigate Noroboto and When to Skip It
&lt;/h2&gt;

&lt;p&gt;Before adding any dependency like this, run through this list. If you answer "I don't know" to more than two, the right experiment is to measure first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Does your application calculate text positions for layout (not just rendering)?
✅ Do you have variable-width text (not fixed monospace)?
✅ Are you running on Linux with non-standard DPI or fonts without hinting?
✅ Does broken layout have visible or functional consequences for the user?
✅ Have you already measured real discrepancies between promised metrics and observed render?

⛔ Do you just want "more precision" without having seen the problem in practice?
⛔ Is the stack already using HarfBuzz + FreeType with a tested config and clean fontconfig?
⛔ Is the discrepancy you observed &amp;lt; 0.5px at standard 96dpi?
⛔ Does the project not have a feedback loop between metrics and render?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If three or more of the &lt;code&gt;⛔&lt;/code&gt; items apply to your case, the mitigation costs more than the problem. The maintenance overhead of the abstraction is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to measure before deciding&lt;/strong&gt; — on Linux you can run a rudimentary test with &lt;code&gt;fc-query&lt;/code&gt; to inspect the declared metrics of a font and compare them against what a rasterizer like FreeType returns in practice:&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;# Inspect declared metrics of an installed font&lt;/span&gt;
fc-query /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"spacing|size|pixelsize"&lt;/span&gt;

&lt;span class="c"&gt;# See what fonts your system is actually using for a specific pattern&lt;/span&gt;
fc-match &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"DejaVu Sans:size=12"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"file|size|spacing"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn't give you the render delta, but it does confirm whether the font system is resolving what you think it's resolving. If the font that matches isn't the one you expected, any metric you assume is wrong from the start.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Can't Be Concluded Yet
&lt;/h2&gt;

&lt;p&gt;Here's the honest limit of this analysis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Without your own benchmark, there's no reliable number.&lt;/strong&gt; The overhead of the Rust mitigation depends on the use case, the text size, the hardware, and how much work the feedback loop does. I don't have a public verifiable number and I'm not going to invent one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Without discrepancy logs from your own production, you don't know if the problem exists in your stack.&lt;/strong&gt; The project description frames the problem in general terms, but general framing isn't your environment. If you're running Ubuntu with well-configured fontconfig and system fonts, you may never see the bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust mitigates, it doesn't eliminate.&lt;/strong&gt; If the shaper returns incorrect data from upstream, the Rust mitigation is operating on bad data. The fix may require going further up the chain — fontconfig configuration, font selection, explicit DPI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kind of signal vs. noise analysis is the same exercise I run whenever I evaluate whether a new pattern in the ecosystem deserves team time. I did it with &lt;a href="https://juanchi.dev/en/blog/needle-gemini-tool-calling-26m-parameters-technical-read" rel="noopener noreferrer"&gt;small agents for tool calling&lt;/a&gt;, with &lt;a href="https://juanchi.dev/en/blog/rate-limiting-nextjs-what-to-protect-before-choosing-library" rel="noopener noreferrer"&gt;retry and load amplification&lt;/a&gt;, and with &lt;a href="https://juanchi.dev/en/blog/prisma-server-actions-nextjs-16-n1-composition-patterns" rel="noopener noreferrer"&gt;the N+1 that shows up in Prisma when you least expect it&lt;/a&gt;. The question is always the same: do I have evidence of this problem in my context, or am I optimizing against a ghost?&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;What exactly are the "lying fonts" Noroboto documents?&lt;/strong&gt;&lt;br&gt;
It's the phenomenon where the metrics the font subsystem reports (advance width, bearing, bounding box) don't match the pixels the rasterizer actually paints. The discrepancy can be sub-pixel in simple cases or accumulate over long text runs with complex kerning, especially with poorly-hinted fonts or in environments with non-standard DPI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is this problem exclusive to Linux?&lt;/strong&gt;&lt;br&gt;
No, but Linux is where the most variability exists due to the combination of fontconfig, FreeType, HarfBuzz, and multiple compositors. macOS has CoreText with a more controlled pipeline. Windows has DirectWrite. Some form of discrepancy is possible on all of them, but the magnitude and frequency vary a lot, and I don't have cross-platform measurements to quantify that gap here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Rust and not C or C++ for the mitigation?&lt;/strong&gt;&lt;br&gt;
Rust lets you model the delta with types the compiler verifies, with no runtime overhead. The argument isn't that C++ can't do the same — it can — but that Rust makes it harder to accidentally ignore the discrepancy. It's a type ergonomics argument, not a performance one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need this if I'm only using fonts in a web app or React?&lt;/strong&gt;&lt;br&gt;
Probably not. Browsers have their own text pipeline (Skia, CoreText, or DirectWrite depending on the OS) and the layout engine handles the adjustment. The problem is mainly relevant when you're building something that calculates text positions outside the DOM — canvas, custom editors, terminals, visualization tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I know if I have the problem before adding the dependency?&lt;/strong&gt;&lt;br&gt;
Measure. Take a string, calculate its expected width using system metrics, render it, and measure the actual pixel width. If the difference is consistently greater than 1px on normal-length text at 96dpi, the problem exists in your environment. If the difference is sub-pixel noise, you probably don't need the mitigation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this affect code editors like VS Code?&lt;/strong&gt;&lt;br&gt;
VS Code uses Electron with Chromium's render engine, which has its own text pipeline. For most practical cases, the problem is mitigated by the browser engine. If you're building an extension that does custom text layout over canvas, then yes, it could be relevant.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Take and the Concrete Next Step
&lt;/h2&gt;

&lt;p&gt;What I find valuable about Noroboto isn't the solution itself — it's that it formalizes a contract most apps quietly ignore: &lt;strong&gt;the font system is a dependency with promises that may not be kept, and that deserves to be modeled explicitly if text layout matters to you&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What I don't buy is the "let's add this just in case" read. The cost of maintaining a feedback loop between promised and observed metrics is real. If you don't have evidence of the problem in your environment, you're paying that cost with no measurable benefit — you're modeling a discrepancy you never confirmed exists.&lt;/p&gt;

&lt;p&gt;The honest decision is: measure first with &lt;code&gt;fc-query&lt;/code&gt; and a manual render test, verify whether the discrepancy actually exists in your specific stack, and only then evaluate whether the abstraction makes sense. If you're on VS Code on Ubuntu 24.04 with system fonts and standard DPI, there's a good chance this problem is purely theoretical for your case — and adding the dependency anyway is just cargo-culting a mitigation you don't need.&lt;/p&gt;

&lt;p&gt;The same logic applies when you're evaluating &lt;a href="https://juanchi.dev/en/blog/spring-boot-startup-time-2026-graalvm-native-aot-cds" rel="noopener noreferrer"&gt;startup time in Spring Boot&lt;/a&gt; or deciding &lt;a href="https://juanchi.dev/en/blog/why-i-stopped-using-useeffect-sync-state-react-19" rel="noopener noreferrer"&gt;what to sync with useEffect and what not to&lt;/a&gt;: the signal matters, context calibrates it.&lt;/p&gt;

&lt;p&gt;The concrete next step: if you have an application doing text layout on Linux, run the checklist above before your next dependency decision. If three or more of the ⛔ items apply, save the time for something else — and if you do run the &lt;code&gt;fc-query&lt;/code&gt; test and find a real gap, that's worth a comment, because that's the kind of evidence that actually moves this conversation forward.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/noroboto-lying-fonts-rust-mitigation-technical-analysis" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>linux</category>
      <category>sistemas</category>
      <category>rust</category>
    </item>
    <item>
      <title>Noroboto: Lying Fonts y mitigación en Rust — lectura técnica sin hype</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Mon, 17 Aug 2026 14:30:25 +0000</pubDate>
      <link>https://dev.to/jtorchia/noroboto-lying-fonts-y-mitigacion-en-rust-lectura-tecnica-sin-hype-33k1</link>
      <guid>https://dev.to/jtorchia/noroboto-lying-fonts-y-mitigacion-en-rust-lectura-tecnica-sin-hype-33k1</guid>
      <description>&lt;h1&gt;
  
  
  Noroboto: Lying Fonts y mitigación en Rust — lectura técnica sin hype
&lt;/h1&gt;

&lt;p&gt;Las fuentes no son confiables por defecto. Sí, leíste bien. El subsistema tipográfico puede devolver métricas de ancho, kerning y advance width que no coinciden con el render real — y eso cambia todo lo que pensábamos sobre "solo es texto".&lt;/p&gt;

&lt;p&gt;Eso es lo que documenta el proyecto Noroboto: que el stack de fuentes sobre Linux puede mentirte con métricas inconsistentes entre el query y el render efectivo, y que Rust tiene algo concreto que decir al respecto. El problema no es nuevo, pero la documentación es rara y la decisión técnica de adoptarlo no es trivial. Mi tesis antes del primer H2: &lt;strong&gt;no alcanza con leer la noticia y copiar la dependencia; hay que convertir esto en una decisión con criterio propio&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  El problema real que Noroboto señala
&lt;/h2&gt;

&lt;p&gt;Cuando renderizás texto en una aplicación — sea un editor, una terminal, un linter visual o cualquier cosa que dibuje caracteres — dependés de métricas que el sistema de fuentes te promete. El ancho de un glifo, el espacio entre caracteres, el bounding box. El asunto es que esas métricas pueden no coincidir con lo que el motor de render termina pintando en pantalla.&lt;/p&gt;

&lt;p&gt;Esto no es un bug exótico. Es una consecuencia de capas: el shaper (HarfBuzz habitualmente), el rasterizer (FreeType o similar), el compositor de ventanas, el DPI del display y las hints embebidas en la fuente misma. Cada capa puede introducir una discrepancia. Si un proyecto como Noroboto se molesta en documentarlo y construir mitigaciones en Rust, es porque el problema aparece con suficiente frecuencia como para que la solución ad-hoc (compensar a mano, ignorarlo, rezar) deje de ser sostenible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mi punto concreto:&lt;/strong&gt; el valor de Noroboto no está en que descubre algo nuevo, sino en que formaliza el contrato roto y propone una superficie de mitigación con tipos. Eso es relevante si estás construyendo algo que depende de layout de texto preciso.&lt;/p&gt;




&lt;h2&gt;
  
  
  Qué propone la mitigación en Rust y por qué importa el lenguaje
&lt;/h2&gt;

&lt;p&gt;Rust no aparece acá por moda. La elección tiene lógica de oficio: cuando el problema es que un conjunto de métricas retornadas no matchea el render real, querés dos cosas que Rust da bien — tipos que modelen la diferencia explícitamente y zero-cost abstractions para no pagar overhead en el hot path de layout.&lt;/p&gt;

&lt;p&gt;El patrón que emerge en proyectos de este tipo es algo así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Las métricas "prometidas" por el sistema de fuentes&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PromisedMetrics&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;advance_width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bearing_x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bearing_y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Las métricas observadas después del render real&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ObservedMetrics&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;actual_width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pixel_offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// El delta entre promesa y realidad — esto es lo que Noroboto mitiga&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;MetricsDelta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;width_error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cumulative_drift&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// el error se acumula en texto largo&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;compute_delta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;promised&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;PromisedMetrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;observed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ObservedMetrics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MetricsDelta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;MetricsDelta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;width_error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;observed&lt;/span&gt;&lt;span class="py"&gt;.actual_width&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;promised&lt;/span&gt;&lt;span class="py"&gt;.advance_width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cumulative_drift&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// calculado en contexto de línea completa&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La clave es que el tipo &lt;code&gt;MetricsDelta&lt;/code&gt; fuerza al resto del código a reconocer que existe una discrepancia. No podés ignorarla implícitamente como harías con un float suelto. Eso es diseño con tipos al servicio de un invariante real.&lt;/p&gt;

&lt;p&gt;Ahora bien — y acá empieza la parte que me importa comunicar — &lt;strong&gt;este patrón solo sirve si tenés un loop de feedback entre métricas prometidas y render observado&lt;/strong&gt;. Sin ese loop, modelar la diferencia es burocracia de tipos, no mitigación real.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dónde se equivoca la gente al leer este tipo de proyectos
&lt;/h2&gt;

&lt;p&gt;El error clásico es agarrar la solución sin entender el contrato de uso. Con Noroboto o proyectos similares, veo tres confusiones frecuentes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Creer que es un problema de todas las fuentes.&lt;/strong&gt; No siempre lo es, y ahí está el matiz que importa: las fuentes bien hinted en entornos con configuración limpia de fontconfig y FreeType en Linux suelen tener discrepancias menores o despreciables para muchos casos de uso. El problema se vuelve real en fuentes con hinting deficiente, en pantallas con DPI no estándar, o cuando el subpixel rendering está deshabilitado. Primero medí, después mitigá — esto lo digo como criterio prudente, no como medición propia verificada en cada stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Confundir layout de texto con render de texto.&lt;/strong&gt; Si estás construyendo algo que calcula posiciones de texto para UI (React, un canvas, un terminal multiplexer), el problema importa. Si solo renderizás texto en pantalla para que el usuario lo lea, las discrepancias suelen ser subperceptuales. El costo de la mitigación puede ser mayor que el beneficio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Asumir que Rust resuelve el problema por ser Rust.&lt;/strong&gt; El lenguaje da garantías de memoria y permite modelar el delta con tipos. No da garantías sobre las métricas del sistema operativo. Si FreeType o fontconfig te devuelven un número incorrecto, Rust lo recibe igualmente incorrecto. La mitigación requiere medición real, no solo tipos más precisos.&lt;/p&gt;

&lt;p&gt;Esto conecta con algo que aprendí mirando planes de ejecución en PostgreSQL: un índice bien puesto no es magia, es entender el acceso real. Lo mismo acá — un tipo bien modelado no es magia, es entender qué estás midiendo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checklist de decisión: cuándo investigar Noroboto y cuándo no
&lt;/h2&gt;

&lt;p&gt;Antes de agregar cualquier dependencia de este tipo, pasá esta lista. Si contestás "no sé" a más de dos, el experimento correcto es medir primero.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ ¿Tu aplicación calcula posiciones de texto para layout (no solo render)?
✅ ¿Tenés texto de ancho variable (no monospace fijo)?
✅ ¿Corrés en Linux con configuración de DPI no estándar o fuentes sin hinting?
✅ ¿El layout roto tiene consecuencias visibles o funcionales para el usuario?
✅ ¿Ya mediste discrepancias reales entre métricas prometidas y render observado?

⛔ ¿Solo querés "más precisión" sin haber visto el problema en práctica?
⛔ ¿El stack ya usa HarfBuzz + FreeType con configuración probada y fontconfig limpio?
⛔ ¿La discrepancia que observaste es &amp;lt; 0.5px en 96dpi estándar?
⛔ ¿El proyecto no tiene un loop de feedback entre métricas y render?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si tres o más de los &lt;code&gt;⛔&lt;/code&gt; aplican a tu caso, la mitigación tiene costo mayor que el problema. El overhead de mantenimiento de la abstracción es real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cómo medir antes de decidir&lt;/strong&gt; — en Linux podés hacer un test rudimentario con &lt;code&gt;fc-query&lt;/code&gt; para inspeccionar las métricas declaradas de una fuente y compararlas contra lo que un rasterizer como FreeType devuelve en práctica:&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;# Inspeccionar métricas declaradas de una fuente instalada&lt;/span&gt;
fc-query /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"spacing|size|pixelsize"&lt;/span&gt;

&lt;span class="c"&gt;# Ver qué fuentes está usando tu sistema para un pattern específico&lt;/span&gt;
fc-match &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"DejaVu Sans:size=12"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"file|size|spacing"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto no te da el delta de render, pero sí te confirma si el sistema de fuentes está resolviendo lo que creés que resuelve. Si la fuente que matchea no es la que esperabas, cualquier métrica que asumas es incorrecta desde el origen.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lo que no se puede concluir todavía
&lt;/h2&gt;

&lt;p&gt;Acá está el límite honesto del análisis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sin benchmark propio, no hay número confiable.&lt;/strong&gt; El overhead de la mitigación en Rust depende del caso de uso, del tamaño del texto, del hardware y de cuánto trabajo hace el loop de feedback. No tengo un número público verificable y no voy a inventar uno.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sin logs de discrepancia en producción, no sabés si el problema existe en tu stack.&lt;/strong&gt; La descripción del proyecto señala el problema en términos generales. Si corrés Ubuntu con fontconfig bien configurado y fuentes del sistema, puede que nunca veas el bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust mitiga, no elimina.&lt;/strong&gt; Si el shaper devuelve datos incorrectos por upstream, la mitigación en Rust opera sobre datos malos. El fix puede requerir ir más arriba en la cadena — configuración de fontconfig, elección de fuente, DPI explícito.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Este tipo de análisis de señal vs. ruido es el mismo ejercicio que hago cuando evalúo si un patrón nuevo en el ecosistema merece tiempo de equipo. Lo hice con &lt;a href="https://juanchi.dev/es/blog/show-needle-distilled-gemini-tool-calling-modelo-pequeno-analisis" rel="noopener noreferrer"&gt;agentes pequeños para tool calling&lt;/a&gt;, con &lt;a href="https://juanchi.dev/es/blog/rate-limiting-aplicaciones-web-nextjs-que-proteger-antes-de-elegir-libreria" rel="noopener noreferrer"&gt;retry y amplificación de carga&lt;/a&gt; y con &lt;a href="https://juanchi.dev/es/blog/prisma-server-actions-nextjs-16-n1-produccion" rel="noopener noreferrer"&gt;el N+1 que aparece en Prisma cuando no lo esperás&lt;/a&gt;. La pregunta siempre es la misma: ¿tengo evidencia del problema en mi contexto o estoy optimizando contra un fantasma?&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;¿Qué son exactamente las "lying fonts" que documenta Noroboto?&lt;/strong&gt;&lt;br&gt;
Es el fenómeno donde las métricas que el subsistema de fuentes reporta (advance width, bearing, bounding box) no coinciden con los píxeles que el rasterizer termina pintando. La discrepancia puede ser subpixel en casos simples o acumularse en texto largo con kerning complejo, especialmente en fuentes con hinting deficiente o en entornos con DPI no estándar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿El problema es exclusivo de Linux?&lt;/strong&gt;&lt;br&gt;
No, pero Linux es donde más variabilidad existe por la combinación de fontconfig, FreeType, HarfBuzz y múltiples compositores. macOS tiene CoreText con un pipeline más controlado. Windows tiene DirectWrite. En todos los casos existe alguna forma de discrepancia posible, pero la magnitud y frecuencia varían mucho.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Por qué Rust y no C o C++ para la mitigación?&lt;/strong&gt;&lt;br&gt;
Rust permite modelar el delta con tipos que el compilador verifica, sin overhead en runtime. El argumento no es que C++ no pueda hacer lo mismo — puede — sino que Rust hace más difícil ignorar la discrepancia por accidente. Es un argumento de ergonomía de tipos, no de performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Necesito esto si solo uso fuentes en una app web o en React?&lt;/strong&gt;&lt;br&gt;
Probablemente no. Los navegadores tienen su propio pipeline de texto (Skia, CoreText o DirectWrite según el OS) y el layout engine se encarga del ajuste. El problema es relevante principalmente cuando construís algo que calcula posiciones de texto fuera del DOM — canvas, editores custom, terminales, herramientas de visualización.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo sé si tengo el problema antes de agregar la dependencia?&lt;/strong&gt;&lt;br&gt;
Medí. Tomá una cadena de texto, calculá su ancho esperado con las métricas del sistema, renderizá y medí el ancho real en píxeles. Si la diferencia es consistentemente mayor a 1px en texto de longitud normal en 96dpi, el problema existe en tu entorno. Si la diferencia es ruido subpixel, probablemente no necesitás la mitigación.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Esto afecta a editores de código como VS Code?&lt;/strong&gt;&lt;br&gt;
VS Code usa Electron con el motor de render de Chromium, que tiene su propio pipeline de texto. Para la mayoría de los casos prácticos, el problema está mitigado por el motor del navegador. Si construís una extensión que hace layout custom de texto sobre canvas, sí podría ser relevante.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mi postura y el próximo paso concreto
&lt;/h2&gt;

&lt;p&gt;Lo que me parece valioso de Noroboto no es la solución en sí, sino que formaliza un contrato que la mayoría de las apps ignoran: &lt;strong&gt;el sistema de fuentes es una dependencia con promesas que pueden no cumplirse, y eso merece ser modelado explícitamente si el layout de texto importa&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Lo que no compro es la lectura de "agreguemos esto por las dudas". El costo de mantener un loop de feedback entre métricas prometidas y observadas es real. Si no tenés evidencia del problema en tu entorno, estás pagando ese costo sin beneficio medible. Esa es la parte incómoda que casi nadie dice cuando lee un proyecto nuevo con entusiasmo: la abstracción más elegante no vale nada si no tenés el log que la justifique.&lt;/p&gt;

&lt;p&gt;La decisión honesta es: medí primero con &lt;code&gt;fc-query&lt;/code&gt; y un test de render manual, verificá si la discrepancia existe en tu stack específico, y solo después evaluá si la abstracción tiene sentido. Si estás en VS Code sobre Ubuntu 24.04 con fuentes del sistema y DPI estándar, hay buenas chances de que el problema sea teórico para tu caso.&lt;/p&gt;

&lt;p&gt;Esto aplica igual cuando evaluás &lt;a href="https://juanchi.dev/es/blog/spring-boot-startup-time-2026-graalvm-native-aot-cds" rel="noopener noreferrer"&gt;startup time en Spring Boot&lt;/a&gt; o cuando decidís &lt;a href="https://juanchi.dev/es/blog/useeffect-sincronizar-estado-alternativa-react-19" rel="noopener noreferrer"&gt;qué sincronizar con useEffect y qué no&lt;/a&gt;: la señal importa, el contexto la calibra.&lt;/p&gt;

&lt;p&gt;El próximo paso concreto: si tenés una aplicación que hace layout de texto en Linux, corré el checklist de arriba antes de la próxima decisión de dependencia. Si tres o más de los ⛔ aplican, guardá el tiempo para otra cosa — y si alguien te pide sumar la mitigación "por las dudas" sin haber medido nada, esa es la pregunta incómoda que hay que hacer antes de escribir una línea de código.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/noroboto-lying-fonts-mitigacion-rust-lectura-tecnica" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>linux</category>
      <category>sistemas</category>
    </item>
    <item>
      <title>Cline in production: the autonomous code agent for VS Code I use with deliberate constraints</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Mon, 17 Aug 2026 12:00:33 +0000</pubDate>
      <link>https://dev.to/jtorchia/cline-in-production-the-autonomous-code-agent-for-vs-code-i-use-with-deliberate-constraints-14fb</link>
      <guid>https://dev.to/jtorchia/cline-in-production-the-autonomous-code-agent-for-vs-code-i-use-with-deliberate-constraints-14fb</guid>
      <description>&lt;h1&gt;
  
  
  Cline in production: the autonomous code agent for VS Code I use with deliberate constraints
&lt;/h1&gt;

&lt;p&gt;Why does everyone show what Cline &lt;em&gt;can&lt;/em&gt; do and nobody talks about what it &lt;em&gt;shouldn't&lt;/em&gt; do? We've spent months watching demos of agents that write tests, refactor entire modules, and even browse the web to pull data — all inside VS Code, all "autonomous." But the day someone lets an agent run &lt;code&gt;rm -rf&lt;/code&gt; without reviewing the context, the conversation about productivity takes a very different tone.&lt;/p&gt;

&lt;p&gt;I'll put my thesis before the first H2: &lt;strong&gt;autonomous code agents are productive if you design their limits before using them, and dangerous if you trust that they know on their own where to stop.&lt;/strong&gt; The value of Cline isn't in how much it can do alone — it's in how much you can trust it without losing control of the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Cline is and what the official docs actually say
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" rel="noopener noreferrer"&gt;Cline&lt;/a&gt; is a VS Code extension that exposes an AI agent with direct action capabilities: it can read and write files, execute commands in the integrated terminal, use the browser (via Playwright), and call MCPs (Model Context Protocol servers). It supports Claude via the Anthropic API, OpenRouter, and other configurable providers.&lt;/p&gt;

&lt;p&gt;What the official page makes clear — and what a lot of people gloss over — is that Cline operates in different approval modes. The default mode requires user confirmation for each action. But that confirmation can be turned off. That's where the mental model problem starts.&lt;/p&gt;

&lt;p&gt;What the documentation &lt;strong&gt;doesn't say&lt;/strong&gt; is when it makes sense to hand it a complete task versus when to use it as an interactive assistant. That judgment you have to bring yourself. The tool doesn't solve it by design.&lt;/p&gt;

&lt;p&gt;Two capabilities worth understanding before using Cline without restrictions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Command execution&lt;/strong&gt;: Cline can run any command the system terminal accepts. If the workspace has broad permissions, the agent has them too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser use&lt;/strong&gt;: Cline can open pages, click around, and extract content. Useful for scraping docs. Also potentially risky if the context isn't controlled.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where people go wrong configuring it
&lt;/h2&gt;

&lt;p&gt;The most common recipe I see floating around: install the extension, connect the Claude or OpenRouter API, open a project, and tell Cline "refactor this module." The agent starts working, asks for confirmations, you hit "approve" several times in a row without really reading — and at some point the agent executes something you didn't expect.&lt;/p&gt;

&lt;p&gt;The hidden cost isn't technical, it's attentional. Cline asks for approvals, but if you train the reflex to approve everything quickly, the approval stops being a real control and becomes a rubber stamp. The "I'm in control" mental model breaks down exactly there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The counterexample that worries me most&lt;/strong&gt;: an agent with terminal access, running in a workspace that includes environment variables in non-gitignored &lt;code&gt;.env&lt;/code&gt; files, with instructions along the lines of "clean up the temporary files in this project." The agent doesn't know what "temporary" means to you — it only has context for what it can see.&lt;/p&gt;

&lt;p&gt;A pattern I've seen repeatedly in teams adopting code agents: the first few weeks go fine because everyone's paying attention. The following weeks, attention drops and errors show up in the least expected places — not in the generated code, but in the side effects of the commands that were executed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision matrix: what I allow, what I don't, and why
&lt;/h2&gt;

&lt;p&gt;Before opening Cline in any project, I run through this checklist. It's not from the official docs — it's the criteria I've built over time, and I'm offering it as a starting point for you to build your own.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ What I allow without hesitation
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read any file in the workspace&lt;/td&gt;
&lt;td&gt;Read-only, reversible by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create new files in &lt;code&gt;src/&lt;/code&gt; or &lt;code&gt;components/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Changes visible in the Git diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generate unit tests in isolated files&lt;/td&gt;
&lt;td&gt;Easy to review, no side effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explain existing code&lt;/td&gt;
&lt;td&gt;Zero write risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Suggest refactors (without applying them alone)&lt;/td&gt;
&lt;td&gt;Control stays in my hands&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  ⚠️ What I allow with explicit review
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Modify existing files in critical modules&lt;/td&gt;
&lt;td&gt;Only if the diff is readable in &amp;lt; 2 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run build or test commands&lt;/td&gt;
&lt;td&gt;Only in environments without production access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install dependencies (&lt;code&gt;npm install X&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;I check the package before approving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use the browser to pull documentation&lt;/td&gt;
&lt;td&gt;With known URLs and clear context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  ❌ What I never allow autonomously
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Execute commands that touch environment variables&lt;/td&gt;
&lt;td&gt;Risk of unintentional exposure or modification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete files (any form of &lt;code&gt;rm&lt;/code&gt;, &lt;code&gt;del&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Irreversible if Git isn't up to date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run database migrations&lt;/td&gt;
&lt;td&gt;Without context of the real schema state, it can corrupt data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access credentials, tokens, or &lt;code&gt;.env&lt;/code&gt; files&lt;/td&gt;
&lt;td&gt;Hard limit, always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operate in auto-approve mode in projects with infra&lt;/td&gt;
&lt;td&gt;The agent doesn't know what's beyond the workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The logic behind this matrix is simple: &lt;strong&gt;reversibility and visibility&lt;/strong&gt;. If an action is easy to undo and I can see it before it's applied, I can delegate. If it's opaque or irreversible, I don't delegate — no matter how much I trust the model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuration snippet: how I structure the initial context
&lt;/h2&gt;

&lt;p&gt;A frequent configuration mistake is starting a session without giving the agent context about the scope of the work. Cline reads the workspace, but it doesn't know what the operational limits are unless you declare them.&lt;/p&gt;

&lt;p&gt;This is the kind of context instruction I include in the extension's &lt;code&gt;Custom Instructions&lt;/code&gt; (the "System Prompt" section in the settings):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Operational constraints for this workspace&lt;/span&gt;

&lt;span class="gu"&gt;## What you can do without asking for additional permission&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Read any file in the project
&lt;span class="p"&gt;-&lt;/span&gt; Create new files in /src, /components, /tests
&lt;span class="p"&gt;-&lt;/span&gt; Propose changes with an explanation before applying them

&lt;span class="gu"&gt;## What requires explicit confirmation from me&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Modify configuration files (&lt;span class="ge"&gt;*.config.*&lt;/span&gt;, tsconfig, vite.config, etc.)
&lt;span class="p"&gt;-&lt;/span&gt; Install or remove dependencies
&lt;span class="p"&gt;-&lt;/span&gt; Execute any command in the terminal

&lt;span class="gu"&gt;## What you must never do, even if I ask you to&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Read, modify, or mention the contents of .env files
&lt;span class="p"&gt;-&lt;/span&gt; Execute commands with rm, del, drop, truncate
&lt;span class="p"&gt;-&lt;/span&gt; Run database migrations or seeds
&lt;span class="p"&gt;-&lt;/span&gt; Operate in auto-approve mode without my explicit confirmation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under 15 lines. The model processes them as part of the system context and respects them — not as an absolute guarantee, but as a strong signal of what behavior you expect. This doesn't replace reviewing each approval, but it reduces the friction of having to repeat the same constraints every single conversation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest limits: what I won't claim without my own data
&lt;/h2&gt;

&lt;p&gt;There are claims circulating about Cline that I have no way to validate without a controlled experiment, and I'd rather say that plainly than dress it up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Cline speeds up development X times"&lt;/strong&gt;: There's no publicly reproducible metric. It depends on the type of task, the model chosen, and the quality of the context. If someone gives you a number without showing you the setup, discard it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Auto-approve mode is safe if the project is well structured"&lt;/strong&gt;: There's no public evidence backing this as a general practice. It's a hypothesis each team would have to validate with their own test suite, Git hooks, and log review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Claude is better than GPT-4o for Cline"&lt;/strong&gt;: Depends on the task type. For refactoring with long context, Claude has documented advantages from Anthropic — but for specific tasks, the difference can be marginal. This requires your own experiment, not third-party benchmarks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I can stand behind with the public documentation: Cline exposes the capabilities it describes on the Marketplace, the approval modes exist and are configurable, and using MCPs expands the agent's action surface well beyond the filesystem. Those are the facts. The rest is judgment I'm not going to pretend is more validated than it is.&lt;/p&gt;

&lt;p&gt;My actual recommendation, stated as a practice rather than a promise: build an isolated test project — no real credentials, no infra access — and run Cline there before you trust it anywhere that matters. I can't tell you the numbers you'll get. I can tell you that skipping this step is how the &lt;code&gt;.env&lt;/code&gt; scenario above stops being hypothetical.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Is Cline free?&lt;/strong&gt;&lt;br&gt;
The extension is free on the VS Code Marketplace. What costs money is the API of whatever model you use — whether that's Anthropic (Claude), OpenRouter, or another compatible provider. The cost depends on the model chosen and the volume of tokens the agent consumes per session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which model should I use with Cline?&lt;/strong&gt;&lt;br&gt;
The official documentation lists Claude (Anthropic) as the reference model, but Cline is compatible with any provider that supports the API. For coding tasks with long context, Claude 3.5 Sonnet and Claude 3.7 Sonnet have a solid reputation in the community. For experimenting with controlled costs, OpenRouter lets you try multiple models without committing to a single provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it safe to let Cline execute terminal commands?&lt;/strong&gt;&lt;br&gt;
Depends on which commands and with what permissions. If approval mode is active and you review each action before confirming, the risk is manageable. If you use auto-approve in a workspace with access to credentials or infra, the risk is real. Security doesn't come from the tool — it comes from the judgment you bring when you configure it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How is Cline different from GitHub Copilot?&lt;/strong&gt;&lt;br&gt;
Copilot is primarily a code completion assistant — it suggests lines or blocks as you type. Cline is an agent: it can take chained actions, execute commands, write multiple files, and operate with a degree of autonomy. They're tools with different mental models. Copilot helps you write faster; Cline tries to execute tasks. The difference matters because the level of review required is also different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the Model Context Protocol (MCP) and why does it matter in Cline?&lt;/strong&gt;&lt;br&gt;
MCP is an open protocol that lets agents connect to external servers to extend their capabilities — database access, APIs, external file systems, third-party tools. In Cline, MCPs expand the agent's action surface beyond the local workspace. More capabilities = more utility, but also more risk surface if you don't know what MCP servers you're connecting to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Cline for TypeScript and Next.js projects?&lt;/strong&gt;&lt;br&gt;
Yes, and it works well for that stack. Cline understands TypeScript module context, can read &lt;code&gt;tsconfig.json&lt;/code&gt;, navigate a Next.js App Router project structure, and generate typed code. Where you have to be careful is with Server Components vs Client Components routes — the agent can get that distinction wrong if the context isn't explicit. Always review the imports and &lt;code&gt;"use client"&lt;/code&gt; directives before approving changes in that layer.&lt;/p&gt;




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

&lt;p&gt;I started this piece with a concrete friction: everyone shows Cline's potential, nobody talks about the limits. Here's the decision that friction pushed me toward.&lt;/p&gt;

&lt;p&gt;Cline is not a junior you delegate to and stop watching. It's not a toy you have to use fearfully either. What I'll actually say, with the certainty the public docs support and no more: it does what the Marketplace page says it does, the approval modes are real, and MCPs genuinely widen its reach. Everything past that — speed claims, "it's safe if your project is tidy," model comparisons — is judgment I haven't verified myself, and I'm not going to hand it to you dressed as fact.&lt;/p&gt;

&lt;p&gt;My mental model, for what it's worth: &lt;strong&gt;Cline is an executor, not an arbiter&lt;/strong&gt;. It executes well what you ask it to within the context you give it. If that context includes clear restrictions, it respects them. If it doesn't, it assumes everything is fair game — because it has no way of knowing what's irreversible for you.&lt;/p&gt;

&lt;p&gt;The time investment isn't in learning every feature of the extension. It's in writing the operational contract before the first session: what it can touch, what it can execute, what it can never do. Ten minutes of configuration prevents the kind of mistake that has no undo — and I say that as the reminder I'd give myself before opening it in a project that actually matters.&lt;/p&gt;

&lt;p&gt;If you're already using agents in your workflow and want to think about the broader security layer, the analysis of &lt;a href="https://juanchi.dev/en/blog/deepseek-api-typescript-secure-integration-model-evaluation" rel="noopener noreferrer"&gt;OWASP LLM Top 10&lt;/a&gt; or how &lt;a href="https://juanchi.dev/en/blog/nodejs-runtime-that-changed-backend-forever" rel="noopener noreferrer"&gt;Node.js handles the event loop in backend architectures&lt;/a&gt; gives useful context for understanding where the agent does — and doesn't — have real visibility into the system.&lt;/p&gt;

&lt;p&gt;The uncomfortable question worth sitting with before your next session: if Cline ran the wrong command right now, could you undo it in under a minute — or are you trusting the approval prompt to catch what your own attention already stopped catching?&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Cline — VS Code Marketplace: &lt;a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" rel="noopener noreferrer"&gt;https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev&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/cline-vs-code-autonomous-ai-agent-deliberate-limits" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>typescript</category>
      <category>llm</category>
      <category>seguridad</category>
    </item>
  </channel>
</rss>
