<?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>PyTorch: the deep learning framework that won the war</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Fri, 26 Jun 2026 12:02:16 +0000</pubDate>
      <link>https://dev.to/jtorchia/pytorch-the-deep-learning-framework-that-won-the-war-299n</link>
      <guid>https://dev.to/jtorchia/pytorch-the-deep-learning-framework-that-won-the-war-299n</guid>
      <description>&lt;p&gt;This is part 5 of &lt;strong&gt;Awesome Curated: The Tools&lt;/strong&gt;, where I do deep dives on the tools that pass the filter of our automatic curation system. If you landed here directly, I'd recommend starting from &lt;a href="https://juanchi.dev/en/blog/docker-for-novices-resource-16-awesome-lists-recommend" rel="noopener noreferrer"&gt;post #1 on Docker for Novices&lt;/a&gt; to understand how the process works. In the &lt;a href="https://juanchi.dev/en/blog/tensorflow-ml-at-scale-serious-production-deployment" rel="noopener noreferrer"&gt;previous post&lt;/a&gt; we covered TensorFlow. Today it's its eternal rival — and, spoiler, the one that ended up winning the battle for researchers' hearts.&lt;/p&gt;




&lt;p&gt;A couple years ago I was trying to reproduce an NLP paper. Completely normal thing in the academic world: the author publishes the code, you download it, you pray, and you try to get it to run. The paper was from 2019. The code was in TensorFlow 1.x. The absolute mess I got into with the versions, the static graphs, the &lt;code&gt;tf.Session()&lt;/code&gt;, the &lt;code&gt;placeholder&lt;/code&gt;s... I lost half a day. Then I found an unofficial reimplementation in PyTorch. It worked in fifteen minutes. That difference — the feeling that the framework is working &lt;em&gt;with&lt;/em&gt; you and not &lt;em&gt;against&lt;/em&gt; you — is exactly what I'm going to try to explain in this post.&lt;/p&gt;

&lt;p&gt;PyTorch doesn't need an introduction in 2025, but it deserves an honest explanation. Because there's a difference between knowing something exists and understanding &lt;em&gt;why&lt;/em&gt; it won.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/pytorch/pytorch" rel="noopener noreferrer"&gt;PyTorch&lt;/a&gt; is an open source machine learning library developed primarily by Meta AI (formerly Facebook AI Research). It's based on Torch, a scientific computing library that came from the Lua world, and since 2016 it's lived in Python as a first-class citizen.&lt;/p&gt;

&lt;p&gt;The technical differentiator that defines it is its &lt;strong&gt;define-by-run&lt;/strong&gt; approach (also called dynamic graph or eager execution). Unlike the original TensorFlow, which built a static computation graph and then executed it, PyTorch builds the graph &lt;em&gt;as it executes&lt;/em&gt;. That might sound like an implementation detail, but in practice it changes everything: you can use a normal debugger, you can throw a &lt;code&gt;print()&lt;/code&gt; in the middle of your neural network and actually see what's happening, you can have real conditional logic with Python &lt;code&gt;if&lt;/code&gt;s and &lt;code&gt;for&lt;/code&gt;s.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch.nn&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;

&lt;span class="c1"&gt;# Simple neural network definition — pure Python, no magic
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SimpleNet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# One hidden layer with 128 neurons, one output layer with 10 classes
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;784&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# input: flattened 28x28 image
&lt;/span&gt;            &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ReLU&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;            &lt;span class="c1"&gt;# activation function
&lt;/span&gt;            &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# output: 10 classes (e.g. MNIST digits)
&lt;/span&gt;        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Instantiate the network and send it to GPU if available
&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;device&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SimpleNet&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Autograd computes gradients automatically — free backprop
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Native GPU support via CUDA is transparent: you move a tensor with &lt;code&gt;.to(device)&lt;/code&gt; and that's it. The autograd system automatically computes gradients for any operation you perform on tensors, which means implementing custom backpropagation is surprisingly manageable.&lt;/p&gt;

&lt;p&gt;The ecosystem that grew around it is monumental: &lt;strong&gt;torchvision&lt;/strong&gt; for computer vision, &lt;strong&gt;torchaudio&lt;/strong&gt; for audio processing, &lt;strong&gt;HuggingFace Transformers&lt;/strong&gt; (which runs primarily on PyTorch), &lt;strong&gt;PyTorch Lightning&lt;/strong&gt; for structuring the training loop without losing your mind. If you're looking for the official implementation of some paper from the last five years, odds are high it's in PyTorch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Basic training loop — this is what Lightning later abstracts away
&lt;/span&gt;&lt;span class="n"&gt;optimizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Adam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;criterion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CrossEntropyLoss&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;epoch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dataloader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# dataloader iterates the dataset
&lt;/span&gt;        &lt;span class="n"&gt;images&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zero_grad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;         &lt;span class="c1"&gt;# clear gradients from previous step
&lt;/span&gt;        &lt;span class="n"&gt;predictions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;net&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# forward pass
&lt;/span&gt;        &lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# compute error
&lt;/span&gt;        &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backward&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;               &lt;span class="c1"&gt;# backward pass — autograd in action
&lt;/span&gt;        &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;              &lt;span class="c1"&gt;# update weights
&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Epoch &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;epoch&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, Loss: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why it's on the list
&lt;/h2&gt;

&lt;p&gt;It showed up in &lt;strong&gt;6 independent awesome lists&lt;/strong&gt;. That's not a coincidence. The curation system we use in this series treats that consensus signal as a strong indicator: when different communities, with different criteria, all agree on recommending the same tool, something is going on.&lt;/p&gt;

&lt;p&gt;What's going on with PyTorch is that it won the deep learning framework war — and it won it in the most convincing way possible: winning research first, then bleeding into production. Today the majority of papers at NeurIPS, ICML and similar conferences publish code in PyTorch. HuggingFace, which is basically the most important model hub in the world, is built on PyTorch. That creates a brutal flywheel: more researchers → more papers → more code → more adoption → more researchers.&lt;/p&gt;

&lt;p&gt;Compared to TensorFlow (which we &lt;a href="https://juanchi.dev/en/blog/tensorflow-ml-at-scale-serious-production-deployment" rel="noopener noreferrer"&gt;covered in the previous post&lt;/a&gt;), PyTorch has a more pythonic API and a significantly more human debugging experience. TensorFlow clawed back ground with Keras and eager execution, but the research community's perception was already set. For teams that build and experiment fast, PyTorch is the option with the least friction.&lt;/p&gt;

&lt;p&gt;Meta's backing guarantees serious development resources. This isn't a hobby project at risk of being abandoned — it's critical infrastructure for one of the biggest players in the AI ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use it
&lt;/h2&gt;

&lt;p&gt;First and foremost: if you're not doing deep learning, you probably don't need it. For classification, regression, decision trees, clustering — &lt;a href="https://github.com/scikit-learn/scikit-learn" rel="noopener noreferrer"&gt;scikit-learn&lt;/a&gt; will get you the same result with a tenth of the complexity. PyTorch is a cannon, and not every problem is an elephant.&lt;/p&gt;

&lt;p&gt;Second: production deployment has historically been its Achilles' heel. TensorFlow with TFLite or TensorFlow Serving has a longer, more battle-tested track record for serving models at the edge or in high-scale APIs. PyTorch improved this with &lt;strong&gt;TorchScript&lt;/strong&gt; (for serializing models) and &lt;strong&gt;ONNX&lt;/strong&gt; (for exporting to other runtimes), but those tools add real friction — and if you came from the &lt;a href="https://juanchi.dev/en/blog/m2cgen-export-ml-model-to-java-go-csharp-without-python" rel="noopener noreferrer"&gt;m2cgen post&lt;/a&gt;, you already know that sometimes the most elegant solution to deployment is to not bring the framework to production at all.&lt;/p&gt;

&lt;p&gt;Third: GPU memory consumption for large models is a world of its own. Without knowledge of the internals — gradient checkpointing, mixed precision, data parallelism — it's easy to run out of VRAM and have no idea why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;PyTorch is one of those tools that has community consensus not because of marketing but because it solved a real problem better than the competition. The dynamic graph, the pythonic API, the ecosystem that grew around it — everything points in the same direction. If you're getting into deep learning, it's the most reasonable starting point that exists today.&lt;/p&gt;

&lt;p&gt;This was entry #5 of &lt;a href="https://juanchi.dev/en/blog/series/awesome-curated-tools" rel="noopener noreferrer"&gt;Awesome Curated: The Tools&lt;/a&gt;. The series continues — every tool that shows up here went through a curation process that combines signal from multiple awesome lists, AI analysis, and my own human verdict. If you want to see the full journey from Docker to here, start from the &lt;a href="https://juanchi.dev/en/blog/docker-for-novices-resource-16-awesome-lists-recommend" rel="noopener noreferrer"&gt;first post&lt;/a&gt;. The next tool is already in the pipeline.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/pytorch-deep-learning-framework-won-the-war" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>machinelearning</category>
      <category>deeplearning</category>
      <category>opensource</category>
    </item>
    <item>
      <title>PyTorch: el framework de deep learning que ganó la guerra</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Fri, 26 Jun 2026 12:02:12 +0000</pubDate>
      <link>https://dev.to/jtorchia/pytorch-el-framework-de-deep-learning-que-gano-la-guerra-21l9</link>
      <guid>https://dev.to/jtorchia/pytorch-el-framework-de-deep-learning-que-gano-la-guerra-21l9</guid>
      <description>&lt;p&gt;Esta es la parte 5 de &lt;strong&gt;Awesome Curated: The Tools&lt;/strong&gt;, donde hago deep dives en las herramientas que pasan el filtro de nuestro sistema de curación automático. Si llegaste directo acá, te recomiendo arrancar desde el &lt;a href="https://juanchi.dev/es/blog/docker-for-novices-recurso-curado-16-awesome-lists" rel="noopener noreferrer"&gt;post #1 sobre Docker for Novices&lt;/a&gt; para entender cómo funciona el proceso. En el &lt;a href="https://juanchi.dev/es/blog/tensorflow-framework-ml-produccion-deployment-escala" rel="noopener noreferrer"&gt;post anterior&lt;/a&gt; estuvimos con TensorFlow. Hoy toca su eterno rival — y, spoiler, el que terminó ganando la batalla por los corazones de los investigadores.&lt;/p&gt;




&lt;p&gt;Hace un par de años estaba intentando reproducir un paper de NLP. Cosa de todos los días en el mundo académico: el autor publica el código, vos lo bajás, rezás, y tratás de que corra. El paper era de 2019. El código, en TensorFlow 1.x. El quilombo que me armé con las versiones, los grafos estáticos, el &lt;code&gt;tf.Session()&lt;/code&gt;, los &lt;code&gt;placeholder&lt;/code&gt;... perdí medio día. Después encontré una reimplementación no oficial en PyTorch. Funcionó en quince minutos. Esa diferencia — la de sentir que el framework trabaja &lt;em&gt;con&lt;/em&gt; vos y no &lt;em&gt;contra&lt;/em&gt; vos — es exactamente lo que voy a intentar explicar en este post.&lt;/p&gt;

&lt;p&gt;PyTorch no necesita presentación en 2025, pero merece una explicación honesta. Porque hay una diferencia entre saber que algo existe y entender &lt;em&gt;por qué&lt;/em&gt; ganó.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué hace
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/pytorch/pytorch" rel="noopener noreferrer"&gt;PyTorch&lt;/a&gt; es una librería open source de machine learning desarrollada principalmente por Meta AI (antes Facebook AI Research). Está basada en Torch, una librería de cómputo científico que venía del mundo Lua, y desde 2016 vive en Python como ciudadano de primera clase.&lt;/p&gt;

&lt;p&gt;El diferencial técnico que lo define es su enfoque &lt;strong&gt;define-by-run&lt;/strong&gt; (también llamado grafo dinámico o eager execution). A diferencia del TensorFlow original que construía un grafo de computación estático y después lo ejecutaba, PyTorch construye el grafo &lt;em&gt;mientras ejecuta&lt;/em&gt;. Esto puede sonar como un detalle de implementación, pero en la práctica cambia todo: podés usar un debugger normal, podés poner un &lt;code&gt;print()&lt;/code&gt; en el medio de tu red neuronal y ver qué está pasando, podés tener lógica condicional real con &lt;code&gt;if&lt;/code&gt; y &lt;code&gt;for&lt;/code&gt; de Python.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch.nn&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;

&lt;span class="c1"&gt;# Definición de una red neuronal simple — todo es Python puro, sin magia
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RedSimple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# Una capa oculta de 128 neuronas, una de salida con 10 clases
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;784&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# entrada: imagen 28x28 aplanada
&lt;/span&gt;            &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ReLU&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;            &lt;span class="c1"&gt;# función de activación
&lt;/span&gt;            &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# salida: 10 clases (ej: dígitos MNIST)
&lt;/span&gt;        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Instanciamos la red y la mandamos a GPU si está disponible
&lt;/span&gt;&lt;span class="n"&gt;dispositivo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;device&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;red&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RedSimple&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dispositivo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# El autograd calcula los gradientes automáticamente — backprop gratis
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;red&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El soporte nativo de GPU vía CUDA es transparente: movés un tensor con &lt;code&gt;.to(device)&lt;/code&gt; y listo. El sistema de autograd calcula los gradientes automáticamente para cualquier operación que hagas sobre tensores, lo que significa que implementar backpropagation custom es sorprendentemente manejable.&lt;/p&gt;

&lt;p&gt;El ecosistema que creció alrededor es monumental: &lt;strong&gt;torchvision&lt;/strong&gt; para computer vision, &lt;strong&gt;torchaudio&lt;/strong&gt; para procesamiento de audio, &lt;strong&gt;HuggingFace Transformers&lt;/strong&gt; (que corre principalmente sobre PyTorch), &lt;strong&gt;PyTorch Lightning&lt;/strong&gt; para estructurar el training loop sin volverse loco. Si buscás la implementación oficial de algún paper de los últimos cinco años, con alta probabilidad está en PyTorch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Ejemplo de training loop básico — esto es lo que Lightning después abstrae
&lt;/span&gt;&lt;span class="n"&gt;optimizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Adam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;red&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;criterio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CrossEntropyLoss&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;epoch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;imagenes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etiquetas&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dataloader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# dataloader itera el dataset
&lt;/span&gt;        &lt;span class="n"&gt;imagenes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;imagenes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dispositivo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;etiquetas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;etiquetas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dispositivo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zero_grad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;# limpiamos gradientes del paso anterior
&lt;/span&gt;        &lt;span class="n"&gt;predicciones&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;red&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imagenes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# forward pass
&lt;/span&gt;        &lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;criterio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predicciones&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etiquetas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# calculamos error
&lt;/span&gt;        &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backward&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                &lt;span class="c1"&gt;# backward pass — autograd en acción
&lt;/span&gt;        &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;               &lt;span class="c1"&gt;# actualizamos pesos
&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Epoch &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;epoch&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, Loss: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Por qué está en la lista
&lt;/h2&gt;

&lt;p&gt;Apareció en &lt;strong&gt;6 awesome lists independientes&lt;/strong&gt;. Eso no es casualidad. El sistema de curación que usamos en esta serie trata esa señal de consenso como un indicador fuerte: cuando comunidades distintas, con criterios distintos, coinciden en recomendar la misma herramienta, algo está pasando.&lt;/p&gt;

&lt;p&gt;Lo que está pasando con PyTorch es que ganó la guerra de los frameworks de deep learning — y la ganó de la manera más convincente posible: ganándola primero en investigación y después filtrándose a producción. Hoy la mayoría de los papers en NeurIPS, ICML y similares publican código en PyTorch. HuggingFace, que es básicamente el hub de modelos más importante del mundo, está construido sobre PyTorch. Eso genera un flywheel brutal: más investigadores → más papers → más código → más adopción → más investigadores.&lt;/p&gt;

&lt;p&gt;Comparado con TensorFlow (que &lt;a href="https://juanchi.dev/es/blog/tensorflow-framework-ml-produccion-deployment-escala" rel="noopener noreferrer"&gt;cubrimos en el post anterior&lt;/a&gt;), PyTorch tiene una API más pythónica y una experiencia de debugging significativamente más humana. TensorFlow recuperó terreno con Keras y eager execution, pero la percepción de la comunidad investigadora ya estaba formada. Para equipos que construyen y experimentan rápido, PyTorch es la opción que tiene menos fricción.&lt;/p&gt;

&lt;p&gt;El respaldo de Meta garantiza recursos de desarrollo serios. No es un proyecto de hobby con riesgo de abandono — es infraestructura crítica para uno de los jugadores más grandes del ecosistema AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuándo NO usarlo
&lt;/h2&gt;

&lt;p&gt;Primero y principal: si no estás haciendo deep learning, probablemente no lo necesitás. Para clasificación, regresión, árboles de decisión, clustering — &lt;a href="https://github.com/scikit-learn/scikit-learn" rel="noopener noreferrer"&gt;scikit-learn&lt;/a&gt; te va a dar lo mismo con un décimo de la complejidad. PyTorch es un cañón, y no todos los problemas son un elefante.&lt;/p&gt;

&lt;p&gt;Segundo: el deployment a producción históricamente fue el talón de Aquiles. TensorFlow con TFLite o TensorFlow Serving tiene una historia más larga y más rodada para servir modelos en edge o en APIs de alta escala. PyTorch mejoró esto con &lt;strong&gt;TorchScript&lt;/strong&gt; (para serializar modelos) y &lt;strong&gt;ONNX&lt;/strong&gt; (para exportar a otros runtimes), pero esas herramientas agregan fricción real — y si venís del post de &lt;a href="https://juanchi.dev/es/blog/m2cgen-exportar-modelos-ml-sin-dependencias-python" rel="noopener noreferrer"&gt;m2cgen&lt;/a&gt;, sabés que a veces la solución más elegante al deployment es no llevar el framework a producción en absoluto.&lt;/p&gt;

&lt;p&gt;Tercero: el consumo de memoria en GPU para modelos grandes es un mundo aparte. Sin conocimiento de las internals — gradient checkpointing, mixed precision, data parallelism — es fácil quedarse sin VRAM y no entender por qué.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cierre
&lt;/h2&gt;

&lt;p&gt;PyTorch es de esas herramientas que tiene el consenso de la comunidad no por marketing sino porque resolvió un problema real mejor que la competencia. El grafo dinámico, la API pythónica, el ecosistema que creció alrededor — todo apunta en la misma dirección. Si vas a meterte en deep learning, es el punto de partida más razonable que existe hoy.&lt;/p&gt;

&lt;p&gt;Esta fue la entrega #5 de &lt;a href="https://juanchi.dev/es/blog/series/awesome-curated-tools" rel="noopener noreferrer"&gt;Awesome Curated: The Tools&lt;/a&gt;. La serie sigue — cada tool que aparece acá pasó por un proceso de curación que combina señal de múltiples awesome lists, análisis de IA y veredicto humano mío. Si querés ver el recorrido completo desde Docker hasta acá, arrancá desde el &lt;a href="https://juanchi.dev/es/blog/docker-for-novices-recurso-curado-16-awesome-lists" rel="noopener noreferrer"&gt;primer post&lt;/a&gt;. La próxima herramienta ya está en el pipeline.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/pytorch-framework-deep-learning-estandar-investigacion-produccion" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>machinelearning</category>
      <category>deeplearning</category>
    </item>
    <item>
      <title>TensorFlow: the ML elephant that's still standing</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 23 Jun 2026 12:01:30 +0000</pubDate>
      <link>https://dev.to/jtorchia/tensorflow-the-ml-elephant-thats-still-standing-19ca</link>
      <guid>https://dev.to/jtorchia/tensorflow-the-ml-elephant-thats-still-standing-19ca</guid>
      <description>&lt;p&gt;This is post #4 in the &lt;strong&gt;Awesome Curated: The Tools&lt;/strong&gt; series — where I do deep dives on the tools that pass the filter of our automated curation system. If you landed here directly, you might also want to check out how &lt;a href="https://juanchi.dev/en/blog/m2cgen-export-ml-model-to-java-go-csharp-without-python" rel="noopener noreferrer"&gt;m2cgen lets you export ML models without shipping Python to production&lt;/a&gt;, because it connects pretty directly to what we're talking about today.&lt;/p&gt;




&lt;p&gt;I was in an architecture meeting a few months ago. A team wanted to tear down their ML production stack and migrate everything to PyTorch because "TensorFlow is old and nobody uses it anymore." The main argument was that all the recent papers use PyTorch. I asked: where does the model run today? On a server on Google Cloud. Are there mobile endpoints? Yes — an iOS and Android app. How much traffic? Millions of requests per day.&lt;/p&gt;

&lt;p&gt;I told them migrating wasn't necessarily a bad idea, but could they walk me through how much effort it would take to rewrite the deployment pipeline, the production serving layer, and the compiled TFLite model running on those mobile devices. Silence. The migration makes technical sense in an ideal world where you have six months and zero users waiting. In the real world, TensorFlow is still the answer when deployment matters more than the elegance of your training code.&lt;/p&gt;

&lt;p&gt;And that's exactly what puts it here, on the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/tensorflow/tensorflow" rel="noopener noreferrer"&gt;TensorFlow&lt;/a&gt; is Google's open-source machine learning framework. It started as a C++ library with Python bindings, and that's not a minor detail — the performance core is written in C++ and CUDA, and the Python API is essentially a very powerful wrapper over that. Today it has over 185k GitHub stars, which makes it one of the most starred repos on the entire platform.&lt;/p&gt;

&lt;p&gt;The core proposition is: you build a computational graph that describes your model, and TF optimizes and executes it. In TF2 this got a lot friendlier with eager execution on by default (you can run operations line by line, just like in PyTorch), but the real power kicks in when you use &lt;code&gt;@tf.function&lt;/code&gt; to compile functions into optimized graphs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tensorflow&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;

&lt;span class="c1"&gt;# Define the model — I'm using Keras which ships built into TF2
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;activation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;relu&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_shape&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;784&lt;/span&gt;&lt;span class="p"&gt;,)),&lt;/span&gt;
    &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dropout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# Regularization to prevent overfitting
&lt;/span&gt;    &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;activation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;softmax&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 10 output classes
&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Compile with optimizer and loss function
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;adam&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sparse_categorical_crossentropy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accuracy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Training — X_train and y_train are your data
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;epochs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validation_split&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But where TF really shines is the deployment ecosystem. &lt;strong&gt;TFLite&lt;/strong&gt; converts trained models into optimized versions for mobile and edge devices — with quantization that shrinks model size from megabytes to kilobytes without losing too much accuracy. &lt;strong&gt;TensorFlow Serving&lt;/strong&gt; is a production model server that scales horizontally, handles versioning, and delivers extremely low latencies. &lt;strong&gt;TensorFlow.js&lt;/strong&gt; runs models in the browser. It's an entire ecosystem, not just a training library.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Export to TFLite for mobile deployment
&lt;/span&gt;&lt;span class="n"&gt;converter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TFLiteConverter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_keras_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Dynamic quantization — reduces size without retraining
&lt;/span&gt;&lt;span class="n"&gt;converter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optimizations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Optimize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEFAULT&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Convert — the output is a .tflite file that goes straight to iOS/Android
&lt;/span&gt;&lt;span class="n"&gt;tflite_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;converter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Save to disk
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;optimized_model.tflite&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tflite_model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# This file is typically 3-10x smaller than the original model
# and runs without needing Python on the target device
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;TFLite model size: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tflite_model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; KB&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why it made the list
&lt;/h2&gt;

&lt;p&gt;The curation system picked it up in &lt;strong&gt;6 independent awesome lists&lt;/strong&gt;. That doesn't happen because of hype — it happens because 6 different communities, with different criteria, all reached the same conclusion: this is a tool you can't ignore. And the verdict from both the AI analysis and my own review was &lt;strong&gt;GEM&lt;/strong&gt;, which in our system means exactly what it sounds like: something with real, lasting value.&lt;/p&gt;

&lt;p&gt;What differentiates TF from PyTorch in this context isn't which one trains models better — in that game, honestly, PyTorch won the cultural war, especially in research. What sets TF apart is the &lt;strong&gt;deployment story&lt;/strong&gt;. TFLite has no direct equivalent in the PyTorch ecosystem that's anywhere near as mature for production mobile use. TorchScript exists, but if you've ever tried to integrate a PyTorch model into a native iOS app you already know the pain compared to TFLite. TensorFlow Serving spent years handling brutal production workloads inside Google before it was ever open-sourced — that translates into a level of robustness you simply can't manufacture overnight.&lt;/p&gt;

&lt;p&gt;The other factor is Google Cloud. If your infrastructure lives there, the native integration with Vertex AI, Cloud ML Engine, and the rest of the GCP ecosystem is a real multiplier. It's not ideological lock-in — it's architectural pragmatism.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use it
&lt;/h2&gt;

&lt;p&gt;If you're learning ML from scratch or doing research, &lt;strong&gt;PyTorch&lt;/strong&gt; (&lt;a href="https://github.com/pytorch/pytorch" rel="noopener noreferrer"&gt;github.com/pytorch/pytorch&lt;/a&gt;) will make your life much simpler. The API is more Pythonic, debugging is more intuitive because everything runs in eager mode by default, and the research community lives there — which means new papers come with PyTorch code, not TF. The historical baggage of TF1 vs TF2 still haunts Stack Overflow: you find contradictory answers because people mix versions without clarifying which is which. It's disorienting.&lt;/p&gt;

&lt;p&gt;I wouldn't use it for small projects where deployment is just a normal Python web server either. In that case, you can train with whatever you want and export with &lt;a href="https://juanchi.dev/en/blog/m2cgen-export-ml-model-to-java-go-csharp-without-python" rel="noopener noreferrer"&gt;m2cgen&lt;/a&gt; if the model is simple enough, or serve with FastAPI + pickle if you don't need scale. TF adds real complexity — use it when the problem justifies it.&lt;/p&gt;

&lt;p&gt;And if your team has nobody with TF experience, the onboarding cost for a new project probably isn't worth it unless edge or mobile deployment is a concrete requirement from day zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  TF is still standing, and there are reasons for that
&lt;/h2&gt;

&lt;p&gt;What pushed me to confirm the human GEM verdict is this: TensorFlow isn't on 6 lists because it's trending. It's there because it solves production problems that others don't solve as well. It's the kind of tool you won't pick out of enthusiasm — you'll pick it out of necessity. And when you need it, you'll be glad it exists and that it's spent a decade getting battle-tested.&lt;/p&gt;

&lt;p&gt;This is post #4 in &lt;a href="https://juanchi.dev/en/blog/series/awesome-curated-tools" rel="noopener noreferrer"&gt;Awesome Curated: The Tools&lt;/a&gt;. The series continues — every tool that shows up here has been through a community signal filter, AI analysis, and a human verdict before making the cut. If you're particularly interested in the ML angle, the &lt;a href="https://juanchi.dev/en/blog/m2cgen-export-ml-model-to-java-go-csharp-without-python" rel="noopener noreferrer"&gt;post on m2cgen&lt;/a&gt; pairs really well with this one: it's exactly the other side of the coin, when the model is already trained and you need to get Python out of your production stack entirely.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/tensorflow-ml-at-scale-serious-production-deployment" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>machinelearning</category>
      <category>deeplearning</category>
      <category>arquitectura</category>
    </item>
    <item>
      <title>TensorFlow: el elefante de ML que sigue en pie</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 23 Jun 2026 12:01:26 +0000</pubDate>
      <link>https://dev.to/jtorchia/tensorflow-el-elefante-de-ml-que-sigue-en-pie-58el</link>
      <guid>https://dev.to/jtorchia/tensorflow-el-elefante-de-ml-que-sigue-en-pie-58el</guid>
      <description>&lt;p&gt;Este es el post #4 de la serie &lt;strong&gt;Awesome Curated: The Tools&lt;/strong&gt; — donde hago deep dives en las herramientas que pasan el filtro de nuestro sistema de curación automático. Si llegaste directo acá, quizás te interese ver también cómo &lt;a href="https://juanchi.dev/es/blog/m2cgen-exportar-modelos-ml-sin-dependencias-python" rel="noopener noreferrer"&gt;m2cgen te permite exportar modelos de ML sin llevar Python a producción&lt;/a&gt;, que tiene bastante que ver con lo que vamos a hablar hoy.&lt;/p&gt;




&lt;p&gt;Estaba en una reunión de arquitectura hace unos meses. Un equipo quería tirar abajo su stack de ML en producción y migrar todo a PyTorch porque "TensorFlow es viejo y nadie lo usa". El argumento principal era que en los papers más recientes todo el mundo usa PyTorch. Les pregunté: ¿dónde corre el modelo hoy? En un servidor con Google Cloud. ¿Hay endpoints móviles? Sí, una app iOS y Android. ¿Cuánto tráfico? Millones de requests por día.&lt;/p&gt;

&lt;p&gt;Les dije que no era una mala idea migrar, pero que me explicaran cuánto esfuerzo tenían para reescribir la pipeline de deployment, el serving en producción y el modelo compilado para TFLite en los móviles. Silencio. La migración técnicamente tiene sentido en un mundo ideal donde tenés seis meses y cero usuarios esperando. En el mundo real, TensorFlow sigue siendo la opción cuando el deployment importa más que la elegancia del código de entrenamiento.&lt;/p&gt;

&lt;p&gt;Y eso es exactamente lo que lo pone acá, en la lista.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué hace
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/tensorflow/tensorflow" rel="noopener noreferrer"&gt;TensorFlow&lt;/a&gt; es el framework de machine learning open-source de Google. Arrancó como una librería de C++ con bindings para Python, y eso no es un detalle menor — el core de rendimiento está escrito en C++ y CUDA, y la API de Python es básicamente un wrapper muy poderoso sobre eso. Hoy tiene más de 185k estrellas en GitHub, lo que lo convierte en uno de los repos más starreados de toda la plataforma.&lt;/p&gt;

&lt;p&gt;La propuesta central es: construís un grafo computacional que describe tu modelo, y TF lo optimiza y ejecuta. En TF2 esto se volvió más amigable con eager execution por default (podés ejecutar operaciones línea a línea como en PyTorch), pero el poder real está cuando usás &lt;code&gt;@tf.function&lt;/code&gt; para compilar funciones en grafos optimizados:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tensorflow&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;

&lt;span class="c1"&gt;# Definimos el modelo — acá uso Keras que viene integrado en TF2
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;activation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;relu&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_shape&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;784&lt;/span&gt;&lt;span class="p"&gt;,)),&lt;/span&gt;
    &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dropout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# Regularización para evitar overfitting
&lt;/span&gt;    &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;activation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;softmax&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 10 clases de salida
&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Compilamos con optimizador y función de pérdida
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;adam&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sparse_categorical_crossentropy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accuracy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Entrenamiento — X_train e y_train son tus datos
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;epochs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validation_split&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pero donde TF realmente brilla es en el ecosistema de deployment. &lt;strong&gt;TFLite&lt;/strong&gt; convierte modelos entrenados en versiones optimizadas para móviles y dispositivos edge — con cuantización que reduce el tamaño del modelo de megabytes a kilobytes sin perder demasiada precisión. &lt;strong&gt;TensorFlow Serving&lt;/strong&gt; es un servidor de modelos en producción que escala horizontal, maneja versioning y tiene latencias bajísimas. &lt;strong&gt;TensorFlow.js&lt;/strong&gt; corre modelos en el browser. Es un ecosistema entero, no solo una librería de entrenamiento.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Exportar a TFLite para deployment en móvil
&lt;/span&gt;&lt;span class="n"&gt;converter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TFLiteConverter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_keras_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Cuantización dinámica — reduce tamaño sin reentrenar
&lt;/span&gt;&lt;span class="n"&gt;converter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optimizations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Optimize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEFAULT&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Convertimos — el resultado es un archivo .tflite que va directo a iOS/Android
&lt;/span&gt;&lt;span class="n"&gt;tflite_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;converter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Lo guardamos al disco
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;modelo_optimizado.tflite&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tflite_model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Este archivo pesa típicamente 3-10x menos que el modelo original
# y corre sin necesidad de Python en el dispositivo final
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Tamaño del modelo TFLite: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tflite_model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; KB&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Por qué está en la lista
&lt;/h2&gt;

&lt;p&gt;El sistema de curación lo detectó en &lt;strong&gt;6 awesome lists independientes&lt;/strong&gt;. Eso no pasa por hype — pasa porque 6 comunidades distintas, con criterios distintos, llegaron a la misma conclusión: es una herramienta que no podés ignorar. Y el veredicto tanto del análisis de IA como el mío fue &lt;strong&gt;GEM&lt;/strong&gt;, que en nuestro sistema significa exactamente lo que parece: algo que tiene valor real y duradero.&lt;/p&gt;

&lt;p&gt;Lo que lo diferencia de PyTorch en este contexto no es quién entrena mejor los modelos — en ese juego, honestamente, PyTorch ganó la batalla cultural, especialmente en investigación. Lo que diferencia a TF es el &lt;strong&gt;deployment story&lt;/strong&gt;. TFLite no tiene equivalente directo en el ecosistema PyTorch que sea tan maduro para producción móvil. TorchScript existe, pero si alguna vez intentaste integrar un modelo PyTorch en una app iOS nativa vas a saber el dolor que es comparado con TFLite. TensorFlow Serving lleva años corriendo cargas de producción brutal en Google antes de ser open-source — eso se traduce en robustez que no se consigue de un día para el otro.&lt;/p&gt;

&lt;p&gt;El otro factor es Google Cloud. Si tu infraestructura vive ahí, la integración nativa con Vertex AI, Cloud ML Engine y el resto del ecosistema GCP es un multiplicador real. No es lock-in ideológico — es pragmatismo de arquitectura.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuándo NO usarlo
&lt;/h2&gt;

&lt;p&gt;Si estás aprendiendo ML desde cero o haciendo investigación, &lt;strong&gt;PyTorch&lt;/strong&gt; (&lt;a href="https://github.com/pytorch/pytorch" rel="noopener noreferrer"&gt;github.com/pytorch/pytorch&lt;/a&gt;) te va a hacer la vida mucho más simple. La API es más pythónica, el debugging es más intuitivo porque todo corre en eager mode por default, y la comunidad investigadora está ahí — lo que significa que los papers nuevos tienen código en PyTorch, no en TF. La deuda histórica de TF1 vs TF2 todavía se siente en Stack Overflow: encontrás respuestas contradictorias porque mezclan versiones sin aclarar cuál es cuál. Eso marea.&lt;/p&gt;

&lt;p&gt;Tampoco lo usaría para proyectos pequeños donde el deployment es un servidor web normal con Python. En ese caso, podés entrenar con lo que quieras y exportar con &lt;a href="https://juanchi.dev/es/blog/m2cgen-exportar-modelos-ml-sin-dependencias-python" rel="noopener noreferrer"&gt;m2cgen&lt;/a&gt; si el modelo es suficientemente simple, o servir con FastAPI + pickle si no necesitás escala. TF agrega complejidad real — usala cuando el problema lo justifica.&lt;/p&gt;

&lt;p&gt;Y si tu equipo no tiene nadie con experiencia en TF, el costo de onboarding para un proyecto nuevo probablemente no vale la pena salvo que el deployment en edge o móvil sea un requerimiento concreto desde el día cero.&lt;/p&gt;

&lt;h2&gt;
  
  
  TF sigue en pie, y hay razones para eso
&lt;/h2&gt;

&lt;p&gt;Lo que me llevó a confirmar el GEM humano es esto: TensorFlow no está en 6 listas porque esté de moda. Está porque resuelve problemas de producción que otros no resuelven igual de bien. Es el tipo de herramienta que no vas a elegir por entusiasmo sino por necesidad — y cuando la necesitás, vas a estar contento de que existe y de que lleva una década siendo battle-tested.&lt;/p&gt;

&lt;p&gt;Este es el post #4 de &lt;a href="https://juanchi.dev/es/blog/series/awesome-curated-tools" rel="noopener noreferrer"&gt;Awesome Curated: The Tools&lt;/a&gt;. La serie sigue — cada herramienta que aparece pasó por un filtro de señal de comunidad, análisis de IA y veredicto humano antes de llegar acá. Si te interesa el tema de ML en particular, el &lt;a href="https://juanchi.dev/es/blog/m2cgen-exportar-modelos-ml-sin-dependencias-python" rel="noopener noreferrer"&gt;post sobre m2cgen&lt;/a&gt; cierra muy bien con este: es exactamente la otra cara de la moneda, cuando el modelo ya está entrenado y necesitás sacarte Python de encima en producción.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/tensorflow-framework-ml-produccion-deployment-escala" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>machinelearning</category>
      <category>deeplearning</category>
    </item>
    <item>
      <title>Rate limiting in Next.js: what to protect before picking a library</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Mon, 22 Jun 2026 14:32:17 +0000</pubDate>
      <link>https://dev.to/jtorchia/rate-limiting-in-nextjs-what-to-protect-before-picking-a-library-2blc</link>
      <guid>https://dev.to/jtorchia/rate-limiting-in-nextjs-what-to-protect-before-picking-a-library-2blc</guid>
      <description>&lt;h1&gt;
  
  
  Rate limiting in Next.js: what to protect before picking a library
&lt;/h1&gt;

&lt;p&gt;There's a pattern I keep seeing in web projects: someone reads about credential stuffing, opens the terminal, and runs &lt;code&gt;npm install @upstash/ratelimit&lt;/code&gt;. Fifteen minutes later there's a middleware capping 10 requests per IP per minute across &lt;strong&gt;all&lt;/strong&gt; routes. The problem isn't the library — it's a good one — the problem is that configuration protects a profile image API with the same intensity as a login endpoint, and it blocks a legitimate user behind a corporate NAT before they ever get to authenticate.&lt;/p&gt;

&lt;p&gt;My thesis is simple: &lt;strong&gt;rate limiting isn't a dependency, it's an abuse policy.&lt;/strong&gt; And a policy without a defined asset, expected abuse pattern, and false positive cost isn't security — it's noise with latency.&lt;/p&gt;

&lt;p&gt;Before installing anything, there are three questions you need to answer. This post is about those questions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rate limiting in Next.js web apps: the missing mental model
&lt;/h2&gt;

&lt;p&gt;When we think about rate limiting, we tend to think "how many requests per second." But that confuses the mechanism with the goal. The real goal is making certain abuse patterns expensive for the attacker without making them expensive for the legitimate user.&lt;/p&gt;

&lt;p&gt;OWASP covers this in their &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;Authentication Cheat Sheet&lt;/a&gt; from the authentication angle: progressive failed attempt counting, temporary lockout, user notification. What it doesn't say — and this is equally important — is &lt;em&gt;what not to block&lt;/em&gt;. That part you have to decide yourself.&lt;/p&gt;

&lt;p&gt;The framework I find most honest has four columns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;What you're trying to define&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What asset are you protecting?&lt;/td&gt;
&lt;td&gt;Specific endpoint, resource, flow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What abuse pattern do you expect?&lt;/td&gt;
&lt;td&gt;Credential stuffing, scraping, layer-7 DDoS, form spam&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What does a false positive cost you?&lt;/td&gt;
&lt;td&gt;Blocked user, lost conversion, wrong support ticket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How are you going to observe it?&lt;/td&gt;
&lt;td&gt;Metrics, logs, alerts, hit/block differentiation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Without all four columns filled in, any configuration you pick is a guess. It might work. It might also block real users in production with nobody noticing until the complaint comes in.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the standard recipe breaks
&lt;/h2&gt;

&lt;p&gt;Global rate limiting middleware in Next.js has a legitimate use case: protecting public routes from mass scraping or brute force on login. But it comes with costs that tutorials tend to skip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The shared IP problem.&lt;/strong&gt; If you limit by IP and the user is behind a corporate proxy or a university NAT, dozens or hundreds of different users share the same address. One active user can burn through everyone else's budget. This isn't an edge case — it's the normal scenario for any B2B app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The scope-too-wide problem.&lt;/strong&gt; A middleware in &lt;code&gt;middleware.ts&lt;/code&gt; that intercepts &lt;code&gt;/(.*)&lt;/code&gt;  applies the limit to &lt;code&gt;/api/auth/login&lt;/code&gt;, &lt;code&gt;/api/profile/avatar&lt;/code&gt;, &lt;code&gt;/api/search&lt;/code&gt;, and &lt;code&gt;/sitemap.xml&lt;/code&gt; equally. The cost of a false positive on login is very different from the cost of one on image assets. Mixing them gives you apparent protection, not real protection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The missing observability problem.&lt;/strong&gt; How many requests did you block today? How many were legitimate? Without that distinction, you can't calibrate. It's not that rate limiting is bad — it's that without observability, you don't know if it's working or if it's causing damage.&lt;/p&gt;

&lt;p&gt;A more defensive pattern in Next.js App Router looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware.ts — selective rate limiting by route, not global&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Explicit list of routes that justify protection&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PROTECTED_ROUTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/auth/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/auth/register&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/contact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;

  &lt;span class="c1"&gt;// Only apply on routes we've defined as critical assets&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;PROTECTED_ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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 counting mechanism goes here (Upstash, Redis, etc.)&lt;/span&gt;
  &lt;span class="c1"&gt;// The point: this block has explicit scope, not implicit&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/auth/:path*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/contact/:path*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference isn't in the library. It's that the &lt;code&gt;matcher&lt;/code&gt; is explicit. If you add &lt;code&gt;/api/upload&lt;/code&gt; tomorrow, it doesn't inherit the limit by accident — you have to consciously decide whether to protect it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The decision matrix before choosing a mechanism
&lt;/h2&gt;

&lt;p&gt;This is the part I most want to share because it's the part most people skip. Before choosing between Redis + Upstash, a stateless token middleware, or your cloud provider's built-in rate limiting, you need to answer:&lt;/p&gt;

&lt;h3&gt;
  
  
  What asset are you protecting?
&lt;/h3&gt;

&lt;p&gt;Not all routes carry the same risk under abuse. One way to think about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High sensitivity&lt;/strong&gt;: login, registration, password reset, payment endpoints, email sending. Abuse here has direct consequences: compromised accounts, real costs in third-party services, spam.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medium sensitivity&lt;/strong&gt;: search, public listings, internal APIs. Abuse here is more about scraping or overload, not account takeover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low sensitivity&lt;/strong&gt;: static assets, UI routes, sitemap. Protecting these with rate limiting adds latency without reducing real risk.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What abuse pattern do you expect?
&lt;/h3&gt;

&lt;p&gt;This changes the mechanism, not just the threshold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credential stuffing on login&lt;/strong&gt;: you want a limit by IP &lt;em&gt;and&lt;/em&gt; by username, with progressive backoff. OWASP specifically recommends against permanent account lockout to prevent attackers from using that as a DoS vector against legitimate users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scraping of listings&lt;/strong&gt;: IP limit with a sliding window. Here throughput matters, not failed attempts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contact form spam&lt;/strong&gt;: IP limit + honeypot + origin validation. Rate limiting alone isn't enough if the form doesn't have a CSRF token.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What does a false positive cost you?
&lt;/h3&gt;

&lt;p&gt;This is the most uncomfortable question because it forces you to put a number on something that feels abstract. Some questions to calibrate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much is an erroneously blocked user session worth? (support cost, lost conversion)&lt;/li&gt;
&lt;li&gt;How many legitimate users share an IP in your target market segment?&lt;/li&gt;
&lt;li&gt;Is there a low-friction recovery path if the rate limit fires incorrectly?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the false positive cost is high and the asset is critical, the threshold needs to be conservative on blocking but generous on recovery time.&lt;/p&gt;

&lt;h3&gt;
  
  
  How are you going to observe it?
&lt;/h3&gt;

&lt;p&gt;A rate limiter without metrics is a black box. The minimum you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Minimal logging example when rejecting a request&lt;/span&gt;
&lt;span class="c1"&gt;// Adapt to your own logging system (pino, winston, structured stdout)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;logRateLimitEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blocked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;allowed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Never log auth headers or body here&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;With this, you can at least run a daily query: how many blocked, on which routes, at what time? Without it, the policy is opaque.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common mistakes and their real costs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Using a global limit as a substitute for analysis.&lt;/strong&gt; "10 requests per minute per IP across the whole domain" sounds reasonable until a bot uses 10,000 rotating IPs and gets through anyway, while a real user on a corporate VPN gets locked out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trusting IP as a unique identifier.&lt;/strong&gt; IPv4 with NAT and CDNs makes IP a noisy identifier. For authenticated routes, the identifier should be the user ID, not the IP. For public routes, IP is what you've got — but with all the limitations that implies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not differentiating between &lt;code&gt;429 Too Many Requests&lt;/code&gt; with and without &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/strong&gt; If you block a request and don't return a &lt;code&gt;Retry-After&lt;/code&gt; header, the client (and the user) has no idea when to retry. OWASP calls out backoff as an explicit mechanism; the header is how the server communicates it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Correct response with recovery information&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Too many attempts. Please wait a moment.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Retry-After&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;60&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// seconds until they can retry&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-RateLimit-Reset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Adding rate limiting without checking if an upstream layer already exists.&lt;/strong&gt; Railway, Vercel, and Cloudflare all have their own rate limiting controls. Adding your own in middleware without knowing what the upstream is doing can create unexpected behavior — or simply duplicate work without reducing additional risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  Limits of this guide: what you can't conclude without your own data
&lt;/h2&gt;

&lt;p&gt;I need to be direct about what this guide does &lt;em&gt;not&lt;/em&gt; give you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;There are no universal thresholds.&lt;/strong&gt; "10 requests per minute" for login might be too low for an app with mobile users reconnecting frequently, and too high for a B2B app where a legitimate login rarely repeats more than twice in a row. The right number comes from observing your actual users' real behavior.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;There's no evidence that rate limiting alone prevents account takeover.&lt;/strong&gt; OWASP treats it as a &lt;em&gt;complementary&lt;/em&gt; control, not the main defense. Without MFA, without compromised credential detection (haveibeenpwned.com has a public API for this), rate limiting on login slows down simple brute force but not sophisticated credential stuffing with rotating IPs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You can't calibrate the false positive without logs.&lt;/strong&gt; Whatever number you pick today is a hypothesis. Calibration comes from observing how many legitimate requests approach the threshold under normal conditions.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This connects to something I covered in the post on &lt;a href="https://juanchi.dev/en/blog/oauth-scope-creep-vercel-incident-audit-integrations" rel="noopener noreferrer"&gt;OAuth Scope Creep&lt;/a&gt;: security controls have to be designed from specific risk, not from a generic recipe. And in &lt;a href="https://juanchi.dev/en/blog/owasp-llm-top-10-audit-typescript-agent-pipeline" rel="noopener noreferrer"&gt;OWASP LLM Top 10 for agents&lt;/a&gt; I landed on a similar conclusion: the guide gives you the framework, but calibration comes from your own data.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ: Rate limiting in Next.js
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Upstash the only option for rate limiting in Next.js with App Router?&lt;/strong&gt;&lt;br&gt;
No. Upstash with Redis is popular because it works well in serverless environments (Vercel, Railway with Workers), but you can implement rate limiting with any shared storage: your own Redis, Memcached, or even a database if the volume allows. The choice depends on tolerated latency and your deployment model. If the middleware runs on the edge, you need something with low latency that's compatible with the edge runtime (no native Node.js).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it make sense to rate limit static asset routes?&lt;/strong&gt;&lt;br&gt;
In most cases, no. Static assets (&lt;code&gt;/_next/static/&lt;/code&gt;, public images) have low abuse cost and high false positive cost (real users hammer them heavily during page loads). CDN or hosting provider rate limiting already covers this better than custom middleware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I handle users behind NAT or a corporate VPN?&lt;/strong&gt;&lt;br&gt;
For authenticated routes, use the user ID as the limit identifier, not the IP. For public routes, you can combine IP with header fingerprinting or use more generous limits paired with anomaly detection (many failed attempts from the same IP). There's no perfect solution here — it's a trade-off between blocking precision and false positive cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does the server return when rate limiting fires? Does the message matter?&lt;/strong&gt;&lt;br&gt;
More than you'd think. A &lt;code&gt;429&lt;/code&gt; without &lt;code&gt;Retry-After&lt;/code&gt; leaves the client with no information on when to retry. A message that's too specific ("blocked due to excessive login attempts") can hand the attacker information about the mechanism. The reasonable approach: &lt;code&gt;429&lt;/code&gt; with &lt;code&gt;Retry-After&lt;/code&gt; and a generic user-facing message ("Too many requests, please wait a moment").&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does rate limiting in Next.js middleware also cover Server Actions?&lt;/strong&gt;&lt;br&gt;
It depends on how you configure it. Server Actions generate POST requests to the same page URL, not a separate API route. If the middleware matcher doesn't cover those routes, Server Actions won't have the limit applied. Check the &lt;code&gt;matcher&lt;/code&gt; explicitly if you want to protect forms using Server Actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Railway have native rate limiting that replaces middleware?&lt;/strong&gt;&lt;br&gt;
Railway doesn't have native application-level rate limiting (as of when I'm writing this). It has infrastructure-level protection, but no granular control per route or per user. For application-specific abuse logic, you need to implement it yourself. If you're running Cloudflare in front of Railway, Cloudflare does offer per-route rate limiting that can be enough for simple cases.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: the policy before the library
&lt;/h2&gt;

&lt;p&gt;Thirty years in tech taught me that the most expensive mistakes aren't the ones that break the system — they're the ones that give you the feeling of control without actually having it. A rate limiting middleware installed without a defined policy falls squarely in that category: the &lt;code&gt;200 OK&lt;/code&gt; from the deploy masks the fact that you don't know what you're protecting, against what, with what threshold, or whether you're silently damaging legitimate users.&lt;/p&gt;

&lt;p&gt;My practical recommendation: before opening any library, fill in the four columns of the matrix. Asset, expected abuse, false positive cost, observability. If you can't fill them in, you don't have a policy — you have configuration by imitation.&lt;/p&gt;

&lt;p&gt;Then, yes, pick the mechanism that fits your stack. Upstash for serverless, your own Redis if you have the control, the cloud provider's rate limiting if the case is simple. The technology is the easy part. The hard part is the design decision that comes before it.&lt;/p&gt;

&lt;p&gt;The concrete next step: take a single critical route in the app — preferably login or registration — and answer the four questions for that route alone. Not the whole system. One route, four answers, one threshold calibrated with logs. That's more useful than a global middleware configured with numbers someone copied from a tutorial.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Authentication Cheat Sheet — defensive authentication and abuse controls&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/rate-limiting-nextjs-policy-before-library" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>typescript</category>
      <category>nextjs</category>
      <category>approuter</category>
    </item>
    <item>
      <title>Rate limiting en Next.js: qué proteger antes de elegir una librería</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Mon, 22 Jun 2026 14:32:09 +0000</pubDate>
      <link>https://dev.to/jtorchia/rate-limiting-en-nextjs-que-proteger-antes-de-elegir-una-libreria-glb</link>
      <guid>https://dev.to/jtorchia/rate-limiting-en-nextjs-que-proteger-antes-de-elegir-una-libreria-glb</guid>
      <description>&lt;h1&gt;
  
  
  Rate limiting en Next.js: qué proteger antes de elegir una librería
&lt;/h1&gt;

&lt;p&gt;Hay un patrón que se repite en proyectos web: alguien lee sobre credential stuffing, abre la terminal y corre &lt;code&gt;npm install @upstash/ratelimit&lt;/code&gt;. Quince minutos después, hay un middleware que limita a 10 requests por IP por minuto sobre &lt;strong&gt;todas&lt;/strong&gt; las rutas. El problema no es la librería —es buena— el problema es que esa configuración protege una API de imágenes de perfil con el mismo rigor que un endpoint de login, y bloquea a un usuario legítimo detrás de un NAT corporativo antes de que llegue a autenticarse.&lt;/p&gt;

&lt;p&gt;Mi tesis es simple: &lt;strong&gt;rate limiting no es una dependencia, es una política de abuso&lt;/strong&gt;. Y una política sin definición del activo, del abuso esperado y del costo del falso positivo no es seguridad; es ruido con latencia.&lt;/p&gt;

&lt;p&gt;Antes de instalar nada, hay tres preguntas que necesitás responder. Este post es sobre esas preguntas.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rate limiting en aplicaciones web Next.js: el modelo mental que falta
&lt;/h2&gt;

&lt;p&gt;Cuando pensamos en rate limiting, tendemos a pensar en "cuántos requests por segundo". Pero eso confunde el mecanismo con el objetivo. El objetivo real es hacer que ciertos patrones de abuso sean costosos para el atacante sin hacerlos costosos para el usuario legítimo.&lt;/p&gt;

&lt;p&gt;OWASP lo plantea en su &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;Authentication Cheat Sheet&lt;/a&gt; desde el ángulo de autenticación: cuenta progresiva de intentos fallidos, lockout temporal, notificación al usuario. Lo que no dice —y es igual de importante— es &lt;em&gt;qué no bloquear&lt;/em&gt;. Esa parte la tenés que decidir vos.&lt;/p&gt;

&lt;p&gt;El marco que me parece más honesto tiene cuatro columnas:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pregunta&lt;/th&gt;
&lt;th&gt;Lo que buscás definir&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;¿Qué activo protegés?&lt;/td&gt;
&lt;td&gt;Endpoint específico, recurso, flujo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Qué patrón de abuso esperás?&lt;/td&gt;
&lt;td&gt;Credential stuffing, scraping, DDoS de capa 7, spam de formularios&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Cuánto cuesta el falso positivo?&lt;/td&gt;
&lt;td&gt;Usuario bloqueado, conversión perdida, soporte incorrecto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Cómo lo vas a observar?&lt;/td&gt;
&lt;td&gt;Métricas, logs, alertas, diferenciación hit/block&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sin esas cuatro columnas llenas, cualquier configuración que elijas es una conjetura. Puede funcionar. También puede bloquear usuarios reales en producción sin que nadie se entere hasta que llega el reclamo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dónde se rompe la receta estándar
&lt;/h2&gt;

&lt;p&gt;El middleware global de rate limiting en Next.js tiene un caso de uso legítimo: proteger rutas públicas de scraping masivo o de ataques de fuerza bruta sobre login. Pero viene con costos que los tutoriales suelen omitir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El problema de la IP compartida.&lt;/strong&gt; Si limitás por IP y el usuario está detrás de un proxy corporativo o un NAT universitario, decenas o cientos de usuarios distintos comparten la misma dirección. Un solo usuario activo puede consumir el budget del resto. No es un edge case: es el escenario normal de cualquier app B2B.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El problema del scope demasiado ancho.&lt;/strong&gt; Un middleware en &lt;code&gt;middleware.ts&lt;/code&gt; que intercepta &lt;code&gt;/(.*)&lt;/code&gt;  aplica el límite a &lt;code&gt;/api/auth/login&lt;/code&gt;, &lt;code&gt;/api/profile/avatar&lt;/code&gt;, &lt;code&gt;/api/search&lt;/code&gt; y &lt;code&gt;/sitemap.xml&lt;/code&gt; por igual. El costo de un falso positivo en login es muy distinto al de un falso positivo en imágenes. Mezclarlos te da protección aparente, no real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El problema de la observabilidad ausente.&lt;/strong&gt; ¿Cuántos requests bloqueaste hoy? ¿Cuántos eran legítimos? Sin esa distinción, no podés calibrar. No es que rate limiting sea malo —es que sin observabilidad, no sabés si está funcionando ni si está dañando.&lt;/p&gt;

&lt;p&gt;Un patrón más defensivo en Next.js App Router se ve así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware.ts — rate limiting selectivo por ruta, no global&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Lista explícita de rutas que justifican protección&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RUTAS_PROTEGIDAS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/auth/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/auth/register&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/contact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;

  &lt;span class="c1"&gt;// Solo aplicar en rutas que definimos como activos críticos&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;RUTAS_PROTEGIDAS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;ruta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ruta&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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 mecanismo de conteo va aquí (Upstash, Redis, etc.)&lt;/span&gt;
  &lt;span class="c1"&gt;// Lo importante: este bloque tiene scope explícito, no implícito&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/auth/:path*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/contact/:path*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La diferencia no está en la librería. Está en que el &lt;code&gt;matcher&lt;/code&gt; es explícito. Si mañana agregás &lt;code&gt;/api/upload&lt;/code&gt;, no hereda el límite por accidente: tenés que decidir conscientemente si lo protegés.&lt;/p&gt;




&lt;h2&gt;
  
  
  La matriz de decisión antes de elegir el mecanismo
&lt;/h2&gt;

&lt;p&gt;Esta es la parte que más me interesa compartir porque es la que más se saltea. Antes de elegir entre Redis + Upstash, un middleware stateless con tokens, o el rate limiting del proveedor de nube, necesitás responder:&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué activo protegés?
&lt;/h3&gt;

&lt;p&gt;No todas las rutas tienen el mismo valor bajo abuso. Una forma de pensar en esto:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alta sensibilidad&lt;/strong&gt;: login, registro, reset de contraseña, endpoints de pago, envío de emails. El abuso acá tiene consecuencias directas: cuentas comprometidas, costo real en servicios de terceros, spam.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensibilidad media&lt;/strong&gt;: búsqueda, listados públicos, APIs internas. El abuso acá es más de scraping o sobrecarga, no de account takeover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Baja sensibilidad&lt;/strong&gt;: assets estáticos, rutas de UI, sitemap. Protegerlos con rate limiting agrega latencia sin reducir riesgo real.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ¿Qué patrón de abuso esperás?
&lt;/h3&gt;

&lt;p&gt;Esto cambia el mecanismo, no solo el umbral:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credential stuffing en login&lt;/strong&gt;: querés límite por IP &lt;em&gt;y&lt;/em&gt; por username, con backoff progresivo. OWASP recomienda específicamente no lockear cuentas de forma permanente para evitar que el atacante use eso como vector de DoS contra usuarios legítimos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scraping de listados&lt;/strong&gt;: límite por IP con ventana deslizante. Acá sí importa el throughput, no los intentos fallidos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spam de formularios de contacto&lt;/strong&gt;: límite por IP + honeypot + validación de origen. Rate limiting solo no alcanza si el formulario no tiene CSRF token.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ¿Cuánto cuesta el falso positivo?
&lt;/h3&gt;

&lt;p&gt;Esta es la pregunta que más incomoda porque obliga a poner número a algo que parece abstracto. Algunas preguntas para calibrar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Cuánto vale una sesión de usuario bloqueada por error? (costo de soporte, conversión perdida)&lt;/li&gt;
&lt;li&gt;¿Cuántos usuarios legítimos comparten IP en el segmento de mercado propio?&lt;/li&gt;
&lt;li&gt;¿Hay algún mecanismo de recuperación sin fricción si el rate limit se activa equivocado?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si el costo del falso positivo es alto y el activo es crítico, el umbral tiene que ser conservador en el bloqueo pero generoso en el tiempo de recuperación.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cómo lo vas a observar?
&lt;/h3&gt;

&lt;p&gt;Un rate limiter sin métricas es un black box. Lo mínimo que necesitás:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Ejemplo de logging mínimo al rechazar un request&lt;/span&gt;
&lt;span class="c1"&gt;// Adaptar al sistema de logs propio (pino, winston, stdout estructurado)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;logRateLimitEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resultado&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bloqueado&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;permitido&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;evento&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;ruta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desconocida&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;resultado&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Nunca logear headers de autenticación ni body acá&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evento&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;Con esto, al menos podés hacer una query diaria: ¿cuántos bloqueados, en qué rutas, a qué hora? Sin eso, la política es opaca.&lt;/p&gt;




&lt;h2&gt;
  
  
  Errores comunes y sus costos reales
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Usar el límite global como sustituto del análisis.&lt;/strong&gt; "10 requests por minuto por IP en todo el dominio" suena razonable hasta que un bot usa 10.000 IPs rotativas y pasa igual, mientras que un usuario real con VPN corporativa se queda afuera.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confiar en IP como identificador único.&lt;/strong&gt; IPv4 con NAT y CDNs hacen que la IP sea un identificador ruidoso. Para rutas autenticadas, el identificador debería ser el user ID, no la IP. Para rutas públicas, la IP es lo que tenés, pero con los límites que implica.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No diferenciar entre &lt;code&gt;429 Too Many Requests&lt;/code&gt; con y sin &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/strong&gt; Si bloqueás un request y no devolvés un header &lt;code&gt;Retry-After&lt;/code&gt;, el cliente (y el usuario) no sabe cuándo reintentar. OWASP menciona el backoff como mecanismo explícito; el header es la forma en que el servidor lo comunica.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Respuesta correcta con información de recuperación&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Demasiados intentos. Esperá un momento.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Retry-After&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;60&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// segundos hasta que puede reintentar&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-RateLimit-Reset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Agregar rate limiting sin revisar si ya existe una capa upstream.&lt;/strong&gt; Railway, Vercel y Cloudflare tienen controles de rate limiting propios. Agregar uno propio en middleware sin saber qué hace el upstream puede crear comportamientos inesperados —o simplemente duplicar trabajo sin reducir riesgo adicional.&lt;/p&gt;




&lt;h2&gt;
  
  
  Límites de esta guía: qué no podés concluir sin datos propios
&lt;/h2&gt;

&lt;p&gt;Necesito ser directo sobre lo que esta guía &lt;em&gt;no&lt;/em&gt; te da:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No hay umbrales universales.&lt;/strong&gt; "10 requests por minuto" para login puede ser demasiado bajo para una app con usuarios móviles con reconexión frecuente, y demasiado alto para una app B2B donde un login legítimo rara vez se repite más de dos veces seguidas. El número correcto viene de observar el comportamiento real de los propios usuarios.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No hay evidencia de que rate limiting solo prevenga account takeover.&lt;/strong&gt; OWASP lo trata como un control &lt;em&gt;complementario&lt;/em&gt;, no como la defensa principal. Sin MFA, sin detección de credenciales comprometidas (haveibeenpwned.com tiene una API pública para esto), el rate limiting en login frena fuerza bruta simple pero no credential stuffing sofisticado con IPs rotativas.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No podés calibrar el falso positivo sin logs.&lt;/strong&gt; Cualquier número que elijas hoy es una hipótesis. La calibración viene de observar cuántos requests legítimos se acercan al umbral en condiciones normales.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esto conecta con algo que ya traté en el post sobre &lt;a href="https://juanchi.dev/es/blog/oauth-scope-creep-auditoria-integraciones-terceros-seguridad" rel="noopener noreferrer"&gt;OAuth Scope Creep&lt;/a&gt;: los controles de seguridad tienen que diseñarse desde el riesgo específico, no desde la receta genérica. Y en &lt;a href="https://juanchi.dev/es/blog/owasp-llm-top-10-agentes-produccion-typescript" rel="noopener noreferrer"&gt;OWASP LLM Top 10 en agentes&lt;/a&gt; llegué a una conclusión parecida: la guía te da el marco, pero la calibración la hacés con los propios datos.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ: Rate limiting en Next.js
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Upstash es la única opción para rate limiting en Next.js con App Router?&lt;/strong&gt;&lt;br&gt;
No. Upstash con Redis es popular porque funciona bien en entornos serverless (Vercel, Railway con Workers), pero podés implementar rate limiting con cualquier almacenamiento compartido: Redis propio, Memcached, o incluso una base de datos si el volumen lo permite. La elección depende de la latencia tolerada y del modelo de despliegue. Si el middleware corre en el edge, necesitás algo con latencia baja y compatible con el runtime de edge (sin Node.js nativo).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Tiene sentido aplicar rate limiting en rutas de assets estáticos?&lt;/strong&gt;&lt;br&gt;
En la mayoría de los casos, no. Los assets estáticos (&lt;code&gt;/_next/static/&lt;/code&gt;, imágenes públicas) tienen un costo de abuso bajo y un costo de falso positivo alto (usuarios reales los consumen intensivamente en cargas de página). El rate limiting de CDN o del proveedor de hosting ya cubre este caso mejor que un middleware propio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo manejo usuarios detrás de NAT o VPN corporativa?&lt;/strong&gt;&lt;br&gt;
Para rutas autenticadas, usá el user ID como identificador del límite, no la IP. Para rutas públicas, podés combinar IP con fingerprinting de headers o con límites más generosos acompañados de detección de anomalías (muchos intentos fallidos de la misma IP). No hay solución perfecta acá: es un trade-off entre precisión del bloqueo y costo del falso positivo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué devuelve el servidor cuando activo el rate limit? ¿Importa el mensaje?&lt;/strong&gt;&lt;br&gt;
Importa más de lo que parece. Un &lt;code&gt;429&lt;/code&gt; sin &lt;code&gt;Retry-After&lt;/code&gt; deja al cliente sin información para reintentar. Un mensaje demasiado específico ("bloqueado por exceso de intentos de login") puede dar información al atacante sobre el mecanismo. Lo razonable: &lt;code&gt;429&lt;/code&gt; con &lt;code&gt;Retry-After&lt;/code&gt; y un mensaje genérico orientado al usuario ("Demasiadas solicitudes, esperá un momento").&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿El rate limiting en middleware de Next.js protege también las Server Actions?&lt;/strong&gt;&lt;br&gt;
Depende de cómo lo configurés. Las Server Actions generan requests POST a la misma URL de la página, no a una ruta de API separada. Si el matcher del middleware no cubre esas rutas, las Server Actions no tienen el límite. Revisá el &lt;code&gt;matcher&lt;/code&gt; explícitamente si querés proteger formularios que usan Server Actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Railway tiene rate limiting nativo que reemplace al del middleware?&lt;/strong&gt;&lt;br&gt;
Railway no tiene rate limiting de aplicación nativo (al momento de publicar esto). Sí tiene protección a nivel de infraestructura, pero no control granular por ruta o por usuario. Para lógica de abuso específica de la aplicación, necesitás implementarla vos. Si usás un proxy como Cloudflare delante de Railway, Cloudflare sí ofrece rate limiting por ruta que puede ser suficiente para casos simples.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusión: la política antes que la librería
&lt;/h2&gt;

&lt;p&gt;Treinta años de historia con tecnología me enseñaron que los errores más caros no son los que rompen el sistema —son los que dan sensación de control sin tenerlo. Un middleware de rate limiting instalado sin política definida entra en esa categoría: el &lt;code&gt;200 OK&lt;/code&gt; del deploy tapa el hecho de que no sabés qué protegés, contra qué, con qué umbral ni si estás dañando usuarios legítimos en silencio.&lt;/p&gt;

&lt;p&gt;Mi recomendación práctica: antes de abrir cualquier librería, completá las cuatro columnas de la matriz. Activo, abuso esperado, costo del falso positivo, observabilidad. Si no podés completarlas, no tenés política —tenés configuración por imitación.&lt;/p&gt;

&lt;p&gt;Después sí, elegí el mecanismo que encaje con el stack. Upstash para serverless, Redis propio si tenés el control, el rate limiting del proveedor de nube si el caso es simple. La tecnología es la parte fácil. Lo difícil es la decisión de diseño que va antes.&lt;/p&gt;

&lt;p&gt;El próximo paso concreto: tomá una sola ruta crítica de la app —preferentemente login o registro— y respondé las cuatro preguntas para esa ruta sola. No todo el sistema. Una ruta, cuatro respuestas, un límite calibrado con logs. Eso es más útil que un middleware global configurado con números que alguien copió de un tutorial.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Authentication Cheat Sheet — controles defensivos de autenticación y abuso&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/rate-limiting-aplicaciones-web-nextjs-politica-abuso" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>typescript</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>npm Dependencies: How to Evaluate a Library Before Shipping It to Production</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Mon, 22 Jun 2026 12:02:41 +0000</pubDate>
      <link>https://dev.to/jtorchia/npm-dependencies-how-to-evaluate-a-library-before-shipping-it-to-production-3bo3</link>
      <guid>https://dev.to/jtorchia/npm-dependencies-how-to-evaluate-a-library-before-shipping-it-to-production-3bo3</guid>
      <description>&lt;h1&gt;
  
  
  npm Dependencies: How to Evaluate a Library Before Shipping It to Production
&lt;/h1&gt;

&lt;p&gt;Back in 2005, when I was 16 and managing the network at a cyber café, I learned something no manual ever taught me: every cable you plugged in was debt. If the vendor for that cable disappeared or changed the connector, the problem was yours. Not the vendor's, not the customer's. Yours. Today, when I look at a &lt;code&gt;package.json&lt;/code&gt; with 180 direct dependencies in a TypeScript project, I think exactly the same thing. Every entry in that file is a cable someone is going to have to maintain. And in most cases, that someone is you.&lt;/p&gt;

&lt;p&gt;My take is direct: &lt;strong&gt;adding an npm dependency isn't just installing code — it's assuming its maintenance, its CVE history, its transitive dependencies, and the exit cost when the library gets abandoned&lt;/strong&gt;. The question isn't "does it work?" The question is "what happens when it stops working in six months?"&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Evaluating npm Dependencies Is a Maintenance Decision, Not Just a Security One
&lt;/h2&gt;

&lt;p&gt;The official npm documentation defines a package as "a file or directory described by a &lt;code&gt;package.json&lt;/code&gt;" (&lt;a href="https://docs.npmjs.com/about-packages-and-modules" rel="noopener noreferrer"&gt;npm docs&lt;/a&gt;). That's all npm guarantees as a platform: that the file exists and has metadata. Nothing about whether the author is still active, whether it has tests, whether the types are correct, or whether you'll be able to upgrade in two years without breaking half the system.&lt;/p&gt;

&lt;p&gt;What the official docs don't say — and where people get burned — is that a published package can freeze in time. The author might not have bandwidth, might abandon the project, or might simply never hear about a relevant CVE. And at that point, the debt is yours.&lt;/p&gt;

&lt;p&gt;There are three dimensions that matter before installing anything in a TypeScript project with pnpm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Active maintenance&lt;/strong&gt;: When was the last commit? Are there PRs that have gone unanswered for months? Any releases in the past year?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attack surface and types&lt;/strong&gt;: Does the package ship its own types (&lt;code&gt;@types/&lt;/code&gt;) or generate them? How many transitive dependencies does it drag in?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit cost&lt;/strong&gt;: If you need to rip it out tomorrow, how much of your own code changes?&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  How to Audit a Dependency Before &lt;code&gt;pnpm add&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The usual recipe is: search on npm, check if it has GitHub stars, install it, done. The problem is that measures popularity, not quality or longevity. Popularity and active maintenance are not the same thing.&lt;/p&gt;

&lt;p&gt;Here's the process I use, step by step and fully reproducible:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Check the Real State of the Repository
&lt;/h3&gt;

&lt;p&gt;Before installing, open the repo on GitHub and look at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Last commit on &lt;code&gt;main&lt;/code&gt;&lt;/strong&gt;: if it's been more than 12 months with no activity and it's not a stable utility library (like &lt;code&gt;lodash&lt;/code&gt;), that's a signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open issues&lt;/strong&gt;: Are there bugs sitting unanswered for months? CVEs mentioned but not patched?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CHANGELOG or releases&lt;/strong&gt;: a serious project has a version history. If it doesn't, the risk surface goes up.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Analyze Transitive Dependencies with &lt;code&gt;pnpm why&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install in an isolated test project&lt;/span&gt;
pnpm add &amp;lt;package-name&amp;gt;

&lt;span class="c"&gt;# See what it brought along&lt;/span&gt;
pnpm why &amp;lt;package-name&amp;gt;

&lt;span class="c"&gt;# Or a full dependency tree&lt;/span&gt;
pnpm list &lt;span class="nt"&gt;--depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A dependency that looks small can drag in 40 transitive packages. That's not automatically bad — but if two of those 40 have active CVEs, the problem is yours even if your own code never calls them directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Run a Security Audit From the Start
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Basic audit with npm (works on pnpm projects too)&lt;/span&gt;
npm audit

&lt;span class="c"&gt;# To see only critical and high vulnerabilities&lt;/span&gt;
npm audit &lt;span class="nt"&gt;--audit-level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;high

&lt;span class="c"&gt;# If you want the JSON to process it&lt;/span&gt;
npm audit &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'.vulnerabilities | to_entries[] | select(.value.severity == "critical")'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npm audit&lt;/code&gt; uses the &lt;a href="https://github.com/advisories" rel="noopener noreferrer"&gt;npm Advisory Database&lt;/a&gt; to cross-reference installed versions against known CVEs. It's not infallible — there are vulnerabilities that don't have an advisory yet — but it's the minimum reasonable floor before committing to a dependency.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Verify TypeScript Types
&lt;/h3&gt;

&lt;p&gt;In a TypeScript project, a dependency without types is guaranteed friction. Check:&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;# Does the package ship its own types?&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;node_modules/&amp;lt;package&amp;gt;/package.json | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'"types"'&lt;/span&gt;

&lt;span class="c"&gt;# Are @types/ available?&lt;/span&gt;
npm info @types/&amp;lt;package&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the package doesn't ship its own types and the &lt;code&gt;@types/&lt;/code&gt; are community-maintained (not by the original author), you have two separate sources of drift. When the package updates and &lt;code&gt;@types/&lt;/code&gt; doesn't, the compiler fails in ways that aren't obvious.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Evaluate Exit Cost With Your Own Interface
&lt;/h3&gt;

&lt;p&gt;This is the step that gets skipped the most. The question isn't just "does it work today?" but "how much code do I change if I rip it out tomorrow?"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pattern that reduces exit cost:&lt;/span&gt;
&lt;span class="c1"&gt;// Wrap the dependency behind your own interface&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ Using the dependency directly throughout the codebase&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;some-date-lib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2025-01-15&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Abstracting behind your own module&lt;/span&gt;
&lt;span class="c1"&gt;// lib/dates.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parse&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;_parse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;some-date-lib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// single entry point&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the library is spread across 40 different files with no abstraction, removing it costs a major refactor. If it lives in one module, removing it costs an internal implementation swap.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Most Common Mistakes When Evaluating npm Dependencies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: Confusing Weekly Downloads With Stability
&lt;/h3&gt;

&lt;p&gt;npm download numbers include automatic mirrors, CIs, and pipelines. A library with 2M weekly downloads can have a maintainer who hasn't merged a PR in a year. Downloads are a lagging indicator of past popularity, not a guarantee of future support.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: Ignoring &lt;code&gt;devDependencies&lt;/code&gt; in Projects With Build Steps
&lt;/h3&gt;

&lt;p&gt;If a vulnerable &lt;code&gt;devDependency&lt;/code&gt; is involved in the build (babel, webpack, esbuild, tsx), the code it generates can be compromised. The &lt;code&gt;devDependencies&lt;/code&gt; field in &lt;code&gt;package.json&lt;/code&gt; separates intent, not risk. If it goes through the compiler, it matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: Not Looking at &lt;code&gt;peerDependencies&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check what versions of React/Node the lib expects&lt;/span&gt;
npm info &amp;lt;package&amp;gt; peerDependencies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A library that asks for React 17 as a peer in a React 19 project might work — or it might produce silent bugs from duplicate context. Peer conflicts are one of the most common hidden costs in stack upgrades.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 4: Assuming a Small Package Is Safe
&lt;/h3&gt;

&lt;p&gt;Attack surface isn't proportional to size. The &lt;code&gt;event-stream&lt;/code&gt; incident in 2018 showed that a small utility package, transferred to a new maintainer, can become an attack vector. Small doesn't mean harmless. (Source: &lt;a href="https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident" rel="noopener noreferrer"&gt;npm blog on the incident&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;This kind of risk connects directly to what I wrote about &lt;a href="https://juanchi.dev/en/blog/oauth-scope-creep-vercel-incident-audit-integrations" rel="noopener noreferrer"&gt;OAuth Scope Creep&lt;/a&gt;: attack surface accumulates at the edges, not the center.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision Matrix: Do I Add This Dependency or Not?
&lt;/h2&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;Green (add it)&lt;/th&gt;
&lt;th&gt;Yellow (evaluate further)&lt;/th&gt;
&lt;th&gt;Red (avoid or wrap)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Last release&lt;/td&gt;
&lt;td&gt;&amp;lt; 6 months&lt;/td&gt;
&lt;td&gt;6–18 months&lt;/td&gt;
&lt;td&gt;&amp;gt; 18 months with no activity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript types&lt;/td&gt;
&lt;td&gt;Bundled in the package&lt;/td&gt;
&lt;td&gt;Active &lt;code&gt;@types/&lt;/code&gt; aligned with the package&lt;/td&gt;
&lt;td&gt;No types or outdated &lt;code&gt;@types/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active CVEs&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Low severity, no public exploit&lt;/td&gt;
&lt;td&gt;Critical or high with no patch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transitive deps&lt;/td&gt;
&lt;td&gt;&amp;lt; 10&lt;/td&gt;
&lt;td&gt;10–40&lt;/td&gt;
&lt;td&gt;&amp;gt; 40 or deps with CVEs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exit cost&lt;/td&gt;
&lt;td&gt;Easy to wrap&lt;/td&gt;
&lt;td&gt;Moderate coupling&lt;/td&gt;
&lt;td&gt;Invasive across multiple modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active maintainer&lt;/td&gt;
&lt;td&gt;Responds to issues/PRs&lt;/td&gt;
&lt;td&gt;Slow but responds&lt;/td&gt;
&lt;td&gt;No visible activity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If a dependency lands in "Red" on more than two criteria, the right question is: do I actually need this abstraction, or can I implement the specific logic I need in 50 lines of my own code?&lt;/p&gt;

&lt;p&gt;When working on projects with pnpm workspaces — like I described in the post about &lt;a href="https://juanchi.dev/en/blog/pnpm-workspaces-monorepo-ci-railway-real-problems" rel="noopener noreferrer"&gt;pnpm workspaces and CI on Railway&lt;/a&gt; — this evaluation matters twice as much: a problematic dependency in a shared package of the monorepo gets inherited by every app in the workspace.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Checklist Can't Guarantee
&lt;/h2&gt;

&lt;p&gt;Being honest about the limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't predict future abandonment&lt;/strong&gt;: a library with recent releases can get abandoned tomorrow. The checklist measures current state, not future state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;npm audit&lt;/code&gt; doesn't cover all vectors&lt;/strong&gt;: business logic vulnerabilities, sophisticated supply chain attacks, and unreported CVEs don't show up in a standard audit. It's the floor, not the ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real exit cost only gets measured in practice&lt;/strong&gt;: estimating the cost of removing a dependency is a heuristic. Until you actually do it, it's a projection. If the project already has the dependency deeply integrated, the retrospective evaluation is more expensive than the prospective one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub metrics are indicators, not proof&lt;/strong&gt;: an archived repository can be stable because it reached feature-complete. A repo with tons of commits can be unstable due to constant refactoring. Context matters.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  FAQ: Evaluating npm Dependencies in TypeScript Projects
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How many direct dependencies is "too many" in a TypeScript project?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's no universal number. What is a warning sign is having more than 50–60 direct dependencies without having actively evaluated which ones could be replaced by your own implementations. The criterion isn't the count — it's whether every entry in &lt;code&gt;dependencies&lt;/code&gt; has a clear reason that can't be solved in fewer than 100 lines of your own code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;pnpm&lt;/code&gt; have security advantages over &lt;code&gt;npm&lt;/code&gt; or &lt;code&gt;yarn&lt;/code&gt; for this kind of audit?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;pnpm has a different storage model (content-addressable store) that avoids duplication and makes the dependency tree more predictable. But for CVE audits, &lt;code&gt;npm audit&lt;/code&gt; is still the standard tool and works with any lockfile. pnpm's advantage in this context is more about tree predictability than intrinsic security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What do I do if a dependency has a CVE but no fix is available?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, evaluate whether the CVE applies to how you're using it. Many CVEs have specific exploitation conditions that may not apply to your context. If it does apply, look for a fork with the fix, replace the dependency, or implement the minimum necessary functionality yourself. Keeping the vulnerable dependency and "noting it for later" is the most comfortable path and the most expensive one in the medium term.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it make sense to evaluate &lt;code&gt;devDependencies&lt;/code&gt; with the same rigor?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Less rigor, but not zero. Build tools, linters, and compilers that go through the CI pipeline deserve a basic review. A &lt;code&gt;devDependency&lt;/code&gt; that's only used on a local machine has less urgency than one that participates in generating the artifact going to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I evaluate a dependency when it has no visible public repository?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If an npm package doesn't have a public repository linked and has more than a couple of months of existence, the default criterion is don't install it in a serious project. The absence of a public source doesn't imply malice, but it eliminates the possibility of a code audit. Without visible source, the analysis is limited to what the package declares in its &lt;code&gt;package.json&lt;/code&gt; — which is incomplete information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does this affect maintaining a monorepo with multiple apps?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A problematic dependency in a &lt;code&gt;shared/&lt;/code&gt; package of the monorepo propagates automatically to all consumers. That makes upfront evaluation more important, not less. The cost of a CVE or a breaking change in a shared dependency multiplies by the number of apps in the workspace. It's worth spending more time on shared package dependencies than on ones specific to a single app.&lt;/p&gt;




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

&lt;p&gt;Adding an npm dependency is a technical decision with consequences that extend far beyond the current sprint. I'm not saying you should avoid libraries — that would be absurd in an ecosystem where composition is the model. I'm saying the upfront evaluation costs half an hour and can prevent weeks of maintenance debt.&lt;/p&gt;

&lt;p&gt;What I don't buy is the idea that a package's popularity is sufficient evidence to install it without further analysis. GitHub stars don't pay the cost of a migration when the library goes unsupported.&lt;/p&gt;

&lt;p&gt;What I do buy: implementing your own logic when the dependency alternative drags in 30 transitives, has no types, or has a maintainer who hasn't responded in a year. In those cases, 80 well-tested lines of your own code are a more honest investment than delegating to a package you can't control.&lt;/p&gt;

&lt;p&gt;The next concrete step: open the &lt;code&gt;package.json&lt;/code&gt; of the most active project you're currently working on. Pick the five dependencies you know the least about. Run &lt;code&gt;pnpm why &amp;lt;package&amp;gt;&lt;/code&gt; on each one and look at the repository on GitHub. In at least one of them, you'll find something that deserves a conversation about whether it's still worth keeping.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;npm package documentation: &lt;a href="https://docs.npmjs.com/about-packages-and-modules" rel="noopener noreferrer"&gt;https://docs.npmjs.com/about-packages-and-modules&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm blog — event-stream incident (2018): &lt;a href="https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident" rel="noopener noreferrer"&gt;https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident&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/npm-dependencies-evaluate-library-before-production" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>typescript</category>
      <category>pnpm</category>
      <category>npm</category>
    </item>
    <item>
      <title>Dependencias npm: cómo evaluar una librería antes de meterla en producción</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Mon, 22 Jun 2026 12:02:37 +0000</pubDate>
      <link>https://dev.to/jtorchia/dependencias-npm-como-evaluar-una-libreria-antes-de-meterla-en-produccion-55e</link>
      <guid>https://dev.to/jtorchia/dependencias-npm-como-evaluar-una-libreria-antes-de-meterla-en-produccion-55e</guid>
      <description>&lt;h1&gt;
  
  
  Dependencias npm: cómo evaluar una librería antes de meterla en producción
&lt;/h1&gt;

&lt;p&gt;En 2005, cuando administraba redes en un cyber café a los 16, aprendí algo que no estaba en ningún manual: cada cable que conectabas era deuda. Si el proveedor de ese cable desaparecía o cambiaba el conector, el problema era tuyo. No del proveedor, no del cliente. Tuyo. Hoy, cuando miro un &lt;code&gt;package.json&lt;/code&gt; con 180 dependencias directas en un proyecto TypeScript, pienso exactamente lo mismo. Cada entrada en ese archivo es un cable que alguien va a tener que mantener. Y en la mayoría de los casos, ese alguien sos vos.&lt;/p&gt;

&lt;p&gt;Mi tesis es directa: &lt;strong&gt;agregar una dependencia npm no es solo instalar código — es asumir su mantenimiento, su historial de CVEs, sus dependencias transitivas y el costo de salida cuando la librería quede abandonada&lt;/strong&gt;. La pregunta no es "¿funciona?". La pregunta es "¿qué pasa cuando deje de funcionar en seis meses?".&lt;/p&gt;




&lt;h2&gt;
  
  
  Por qué evaluar dependencias npm es una decisión de mantenimiento, no solo de seguridad
&lt;/h2&gt;

&lt;p&gt;La documentación oficial de npm define un paquete como "un archivo o directorio descrito por un &lt;code&gt;package.json&lt;/code&gt;" (&lt;a href="https://docs.npmjs.com/about-packages-and-modules" rel="noopener noreferrer"&gt;npm docs&lt;/a&gt;). Eso es todo lo que garantiza npm como plataforma: que el archivo existe y tiene metadatos. Nada sobre si el autor sigue activo, si tiene tests, si los tipos son correctos o si vas a poder actualizar en dos años sin romper la mitad del sistema.&lt;/p&gt;

&lt;p&gt;Lo que la doc oficial no dice — y donde la gente se quema — es que un paquete publicado puede quedarse congelado en el tiempo. El autor puede no tener tiempo, puede abandonar el proyecto o puede simplemente no enterarse de un CVE relevante. Y en ese momento, la deuda es tuya.&lt;/p&gt;

&lt;p&gt;Hay tres dimensiones que importan antes de instalar algo en un proyecto TypeScript con pnpm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mantenimiento activo&lt;/strong&gt;: ¿Cuándo fue el último commit? ¿Hay PRs abiertas sin respuesta hace meses? ¿Tiene releases en el último año?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Superficie de ataque y tipos&lt;/strong&gt;: ¿El paquete tiene tipos propios (&lt;code&gt;@types/&lt;/code&gt;) o los genera? ¿Cuántas dependencias transitivas arrastra?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Costo de salida&lt;/strong&gt;: Si mañana necesitás sacarlo, ¿cuánto código propio cambiás?&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Cómo auditar una dependencia antes de &lt;code&gt;pnpm add&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;La receta habitual es: buscar en npm, ver si tiene estrellas en GitHub, instalarlo y listo. El problema es que eso mide popularidad, no calidad ni longevidad. Popularidad y mantenimiento activo no son lo mismo.&lt;/p&gt;

&lt;p&gt;Acá está el proceso que uso, paso a paso y reproducible:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Revisar el estado real del repositorio
&lt;/h3&gt;

&lt;p&gt;Antes de instalar, abrí el repo en GitHub y mirá:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Último commit en &lt;code&gt;main&lt;/code&gt;&lt;/strong&gt;: si tiene más de 12 meses sin actividad y no es una librería de utilidad estable (tipo &lt;code&gt;lodash&lt;/code&gt;), es una señal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issues abiertas&lt;/strong&gt;: ¿Hay bugs sin respuesta desde hace meses? ¿CVEs mencionados y no parchados?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CHANGELOG o releases&lt;/strong&gt;: un proyecto serio tiene historial de versiones. Si no lo tiene, la superficie de riesgo sube.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Analizar las dependencias transitivas con &lt;code&gt;pnpm why&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Instalá en un proyecto de prueba aislado&lt;/span&gt;
pnpm add &amp;lt;nombre-del-paquete&amp;gt;

&lt;span class="c"&gt;# Mirá qué trajo consigo&lt;/span&gt;
pnpm why &amp;lt;nombre-del-paquete&amp;gt;

&lt;span class="c"&gt;# O un árbol completo de dependencias&lt;/span&gt;
pnpm list &lt;span class="nt"&gt;--depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una dependencia que parece chica puede arrastrar 40 paquetes transitivos. Eso no es automáticamente malo, pero si dos de esos 40 tienen CVEs activos, el problema es tuyo aunque el código propio no los llame directamente.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Correr una auditoría de seguridad desde el inicio
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Auditoría básica con npm (funciona también en proyectos pnpm)&lt;/span&gt;
npm audit

&lt;span class="c"&gt;# Para ver solo vulnerabilidades críticas y altas&lt;/span&gt;
npm audit &lt;span class="nt"&gt;--audit-level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;high

&lt;span class="c"&gt;# Si querés el JSON para procesarlo&lt;/span&gt;
npm audit &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'.vulnerabilities | to_entries[] | select(.value.severity == "critical")'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npm audit&lt;/code&gt; usa la base de datos del &lt;a href="https://github.com/advisories" rel="noopener noreferrer"&gt;npm Advisory Database&lt;/a&gt; para cruzar versiones instaladas contra CVEs conocidos. No es infalible — hay vulnerabilidades que aún no tienen advisory — pero es el piso mínimo razonable antes de comprometerse con una dependencia.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Verificar los tipos TypeScript
&lt;/h3&gt;

&lt;p&gt;En un proyecto TypeScript, una dependencia sin tipos es fricción garantizada. Revisá:&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;# ¿El paquete tiene tipos propios?&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;node_modules/&amp;lt;paquete&amp;gt;/package.json | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'"types"'&lt;/span&gt;

&lt;span class="c"&gt;# ¿Hay @types/ disponibles?&lt;/span&gt;
npm info @types/&amp;lt;paquete&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si el paquete no tiene tipos propios y los &lt;code&gt;@types/&lt;/code&gt; son mantenidos por la comunidad (no por el autor original), tenés dos fuentes distintas de desfasaje. Cuando el paquete actualiza y &lt;code&gt;@types/&lt;/code&gt; no, el compilador falla de formas que no son obvias.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Evaluar el costo de salida con una interfaz propia
&lt;/h3&gt;

&lt;p&gt;Este es el paso que más se saltea. La pregunta no es solo "¿funciona hoy?" sino "¿cuánto código cambio si mañana lo saco?".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Patrón que reduce el costo de salida:&lt;/span&gt;
&lt;span class="c1"&gt;// Wrapeá la dependencia detrás de una interfaz propia&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ Usar la dependencia directamente en toda la codebase&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alguna-lib-de-fechas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fecha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2025-01-15&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Abstraer detrás de un módulo propio&lt;/span&gt;
&lt;span class="c1"&gt;// lib/fechas.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parse&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;_parse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alguna-lib-de-fechas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parsearFecha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// un solo punto de entrada&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si la librería está en 40 archivos distintos sin abstracción, sacarla cuesta una refactorización mayor. Si está en un módulo propio, sacarla cuesta un reemplazo de implementación interno.&lt;/p&gt;




&lt;h2&gt;
  
  
  Los errores más comunes al evaluar dependencias npm
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Error 1: Confundir descargas semanales con estabilidad
&lt;/h3&gt;

&lt;p&gt;Los números de descargas en npm incluyen mirrors automáticos, CIs y pipelines. Una librería con 2M de descargas semanales puede tener un mantenedor que no mergea PRs hace un año. Las descargas son lagging indicator de popularidad pasada, no garantía de soporte futuro.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error 2: Ignorar las &lt;code&gt;devDependencies&lt;/code&gt; en proyectos con build steps
&lt;/h3&gt;

&lt;p&gt;Si una &lt;code&gt;devDependency&lt;/code&gt; vulnerada está involucrada en el build (babel, webpack, esbuild, tsx), el código que genera puede estar comprometido. El campo &lt;code&gt;devDependencies&lt;/code&gt; en &lt;code&gt;package.json&lt;/code&gt; separa intención, no riesgo. Si pasa por el compilador, importa.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error 3: No mirar el &lt;code&gt;peerDependencies&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Mirá qué versiones de React/Node espera la lib&lt;/span&gt;
npm info &amp;lt;paquete&amp;gt; peerDependencies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una librería que pide React 17 como peer en un proyecto React 19 puede funcionar, o puede generar bugs silenciosos de contexto duplicado. Los peer conflicts son uno de los costos ocultos más frecuentes en actualizaciones de stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error 4: Asumir que un paquete pequeño es seguro
&lt;/h3&gt;

&lt;p&gt;La superficie de ataque no es proporcional al tamaño. El incidente de &lt;code&gt;event-stream&lt;/code&gt; en 2018 mostró que un paquete de utilidad pequeño, transferido a un mantenedor nuevo, puede convertirse en vector de ataque. Que sea pequeño no lo hace inofensivo. (Fuente: &lt;a href="https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident" rel="noopener noreferrer"&gt;npm blog sobre el incidente&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Este tipo de riesgo conecta directamente con lo que escribí sobre &lt;a href="https://juanchi.dev/es/blog/oauth-scope-creep-auditoria-integraciones-terceros-seguridad" rel="noopener noreferrer"&gt;OAuth Scope Creep&lt;/a&gt;: la superficie de ataque se acumula en los bordes, no en el centro.&lt;/p&gt;




&lt;h2&gt;
  
  
  Matriz de decisión: ¿agrego esta dependencia o no?
&lt;/h2&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;Verde (agregar)&lt;/th&gt;
&lt;th&gt;Amarillo (evaluar más)&lt;/th&gt;
&lt;th&gt;Rojo (evitar o wrappear)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Último release&lt;/td&gt;
&lt;td&gt;&amp;lt; 6 meses&lt;/td&gt;
&lt;td&gt;6-18 meses&lt;/td&gt;
&lt;td&gt;&amp;gt; 18 meses sin actividad&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tipos TypeScript&lt;/td&gt;
&lt;td&gt;Incluidos en el paquete&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@types/&lt;/code&gt; activos y alineados&lt;/td&gt;
&lt;td&gt;Sin tipos o &lt;code&gt;@types/&lt;/code&gt; desactualizados&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVEs activos&lt;/td&gt;
&lt;td&gt;Ninguno&lt;/td&gt;
&lt;td&gt;Bajos sin exploit público&lt;/td&gt;
&lt;td&gt;Críticos o altos sin parche&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deps transitivas&lt;/td&gt;
&lt;td&gt;&amp;lt; 10&lt;/td&gt;
&lt;td&gt;10-40&lt;/td&gt;
&lt;td&gt;&amp;gt; 40 o con deps con CVEs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Costo de salida&lt;/td&gt;
&lt;td&gt;Fácil de wrappear&lt;/td&gt;
&lt;td&gt;Acoplamiento moderado&lt;/td&gt;
&lt;td&gt;Invasivo en múltiples módulos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mantenedor activo&lt;/td&gt;
&lt;td&gt;Responde issues/PRs&lt;/td&gt;
&lt;td&gt;Lento pero responde&lt;/td&gt;
&lt;td&gt;Sin actividad visible&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Si una dependencia cae en "Rojo" en más de dos criterios, la pregunta correcta es: ¿realmente necesito esta abstracción o puedo implementar la lógica específica que necesito en 50 líneas propias?&lt;/p&gt;

&lt;p&gt;Cuando trabajo con proyectos usando pnpm workspaces — como describí en el post sobre &lt;a href="https://juanchi.dev/es/blog/pnpm-workspaces-monorepo-ci-railway-problemas" rel="noopener noreferrer"&gt;pnpm workspaces y CI en Railway&lt;/a&gt; — esta evaluación importa el doble: una dependencia problemática en un paquete compartido del monorepo la heredan todos los apps del workspace.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lo que esta checklist no puede garantizar
&lt;/h2&gt;

&lt;p&gt;Siendo honesto sobre los límites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No predice el abandono futuro&lt;/strong&gt;: una librería con releases recientes puede quedar abandonada mañana. La checklist mide el estado actual, no el futuro.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;npm audit&lt;/code&gt; no cubre todos los vectores&lt;/strong&gt;: las vulnerabilidades de lógica de negocio, los supply chain attacks sofisticados y los CVEs no reportados no aparecen en la auditoría estándar. Es el piso, no el techo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El costo de salida real solo se mide en práctica&lt;/strong&gt;: estimar el costo de remover una dependencia es una heurística. Hasta que no lo hacés, es una proyección. Si el proyecto ya tiene la dependencia profundamente integrada, la evaluación retrospectiva es más costosa que la prospectiva.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Las métricas de GitHub son indicadores, no pruebas&lt;/strong&gt;: un repositorio archivado puede ser estable porque llegó a feature-complete. Un repo con muchos commits puede ser inestable por refactorizaciones constantes. El contexto importa.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  FAQ: Evaluar dependencias npm en proyectos TypeScript
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Cuántas dependencias directas es "demasiado" en un proyecto TypeScript?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No hay un número universal. Lo que sí es señal de alerta es tener más de 50-60 dependencias directas sin haber evaluado activamente cuáles son reemplazables por implementaciones propias. El criterio no es el conteo sino si cada entrada en &lt;code&gt;dependencies&lt;/code&gt; tiene una razón clara que no pueda resolverse en menos de 100 líneas de código propio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿&lt;code&gt;pnpm&lt;/code&gt; tiene ventajas de seguridad sobre &lt;code&gt;npm&lt;/code&gt; o &lt;code&gt;yarn&lt;/code&gt; para este tipo de auditoría?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;pnpm tiene un modelo de almacenamiento distinto (content-addressable store) que evita duplicación y hace más predecible el árbol de dependencias. Pero para auditorías de CVEs, &lt;code&gt;npm audit&lt;/code&gt; sigue siendo la herramienta estándar y funciona con cualquier lockfile. La ventaja de pnpm en este contexto es más de predictibilidad del árbol que de seguridad intrínseca.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué hago si una dependencia tiene un CVE pero no hay fix disponible?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Primero, evaluá si el CVE aplica a cómo la usás. Muchos CVEs tienen condiciones de explotación específicas que pueden no aplicar en el contexto propio. Si aplica, buscá un fork con el fix, reemplazá la dependencia o implementá la funcionalidad mínima necesaria vos mismo. Quedarse con la dependencia vulnerable y "anotarlo para después" es el camino más cómodo y el más costoso a mediano plazo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Tiene sentido evaluar las &lt;code&gt;devDependencies&lt;/code&gt; con el mismo rigor?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Menos rigor, pero no cero. Las herramientas de build, linters y compiladores que pasan por el pipeline de CI merecen revisión básica. Una &lt;code&gt;devDependency&lt;/code&gt; que solo se usa en la máquina local tiene menos urgencia que una que participa en generar el artefacto que va a producción.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo evalúo una dependencia cuando no tiene repositorio público visible?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Si un paquete npm no tiene un repositorio público linkado y tiene más de un par de meses de existencia, el criterio por defecto es no instalarlo en un proyecto serio. La ausencia de fuente pública no implica malicia, pero elimina la posibilidad de auditoría de código. Sin source visible, el análisis se limita a lo que el paquete declara en su &lt;code&gt;package.json&lt;/code&gt;, que es información incompleta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo afecta esto al mantenimiento de un monorepo con múltiples apps?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Una dependencia problemática en un paquete &lt;code&gt;shared/&lt;/code&gt; del monorepo se propaga a todos los consumers automáticamente. Eso hace que la evaluación previa sea más importante, no menos. El costo de un CVE o una breaking change en una dependencia compartida se multiplica por la cantidad de apps del workspace. Vale la pena dedicar más tiempo a las dependencias de los paquetes compartidos que a las específicas de una sola app.&lt;/p&gt;




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

&lt;p&gt;Agregar una dependencia npm es una decisión técnica con consecuencias que se extienden mucho más allá del sprint actual. No estoy diciendo que haya que evitar librerías — eso sería absurdo en un ecosistema donde la composición es el modelo. Estoy diciendo que la evaluación previa cuesta media hora y puede evitar semanas de deuda de mantenimiento.&lt;/p&gt;

&lt;p&gt;Lo que no compro es la idea de que la popularidad de un paquete sea suficiente evidencia para instalarlo sin más análisis. Las estrellas en GitHub no pagan el costo de una migración cuando la librería queda sin soporte.&lt;/p&gt;

&lt;p&gt;Lo que sí compro: implementar la lógica propia cuando la alternativa de dependencia arrastra 30 transitivas, no tiene tipos o tiene un mantenedor que no responde desde hace un año. En esos casos, 80 líneas propias bien testeadas son una inversión más honesta que delegar en un paquete que no podés controlar.&lt;/p&gt;

&lt;p&gt;El próximo paso concreto: abrí el &lt;code&gt;package.json&lt;/code&gt; del proyecto más activo en el que estés trabajando. Elegí las cinco dependencias que menos conocés en detalle. Corré &lt;code&gt;pnpm why &amp;lt;paquete&amp;gt;&lt;/code&gt; en cada una y mirá el repositorio en GitHub. En al menos una vas a encontrar algo que merece una conversación sobre si sigue valiendo la pena.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;npm package documentation: &lt;a href="https://docs.npmjs.com/about-packages-and-modules" rel="noopener noreferrer"&gt;https://docs.npmjs.com/about-packages-and-modules&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm blog — event-stream incident (2018): &lt;a href="https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident" rel="noopener noreferrer"&gt;https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident&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/evaluar-dependencias-npm-seguridad-mantenimiento" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>typescript</category>
      <category>pnpm</category>
    </item>
    <item>
      <title>lode: Reimplementing DVC's core in Go without breaking the format</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Sun, 21 Jun 2026 07:58:03 +0000</pubDate>
      <link>https://dev.to/jtorchia/lode-reimplementing-dvcs-core-in-go-without-breaking-the-format-41h6</link>
      <guid>https://dev.to/jtorchia/lode-reimplementing-dvcs-core-in-go-without-breaking-the-format-41h6</guid>
      <description>&lt;h1&gt;
  
  
  lode: Reimplementing DVC's core in Go without breaking the format
&lt;/h1&gt;

&lt;p&gt;There's a type of open source project that earns my immediate respect: one that clearly defines what it &lt;strong&gt;doesn't&lt;/strong&gt; do. lode is one of those.&lt;/p&gt;

&lt;p&gt;When I first read the README, the sentence that stopped me was: &lt;em&gt;"lode never invents a format; your repo stays a DVC repo."&lt;/em&gt; In an ecosystem where every new tool wants to be the center of gravity, that level of intentional restraint is rare. And it's exactly the technical decision I want to dissect here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My thesis:&lt;/strong&gt; format compatibility is not a marketing feature. It's operational risk management. In ML teams where DVC is already baked into pipelines, CI scripts, and audit flows, adopting a tool that invents its own artifact format requires a migration with a freeze window. lode eliminates that cost entirely, and it has a price: pipelines and &lt;code&gt;dvc repro&lt;/code&gt; are out of scope. The trade-off is honest.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem lode attacked
&lt;/h2&gt;

&lt;p&gt;DVC is the de facto standard for versioning datasets and models in ML projects. The problem isn't conceptual: it's runtime. When you have a directory with 20,000 files and run &lt;code&gt;dvc add big/&lt;/code&gt;, DVC hashes sequentially in Python, with all the friction of an interpreter. The repo's README shows concrete measurement on the same repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;time &lt;/span&gt;dvc add big/      &lt;span class="c"&gt;# 20,000 files&lt;/span&gt;
&lt;span class="go"&gt;real    0m5.79s

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;time &lt;/span&gt;lode add big/     &lt;span class="c"&gt;# same repo, identical result byte for byte&lt;/span&gt;
&lt;span class="go"&gt;real    0m0.44s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's roughly a &lt;strong&gt;~13×&lt;/strong&gt; difference in that case. I won't universalize that number as a performance guarantee: it depends on hardware, filesystem, individual file sizes, and how many are already in the state DB. What is reproducible is the mechanism: Go compiles to native binary without VM overhead, hashing runs with &lt;code&gt;NumCPU&lt;/code&gt; goroutines in parallel, and the state DB (bbolt, under &lt;code&gt;internal/hashfile&lt;/code&gt;) stores &lt;code&gt;(inode, mtime, size) → md5&lt;/code&gt; to skip files that haven't changed. That combination makes technical sense independent of the exact number.&lt;/p&gt;

&lt;p&gt;The friction of the hot path matters more than it seems in ML workflows. A slow &lt;code&gt;dvc status&lt;/code&gt; makes data scientists avoid it, leading to commits without updated pointer files, leading to broken reproducibility. Accelerating the happy path has real impact on team discipline.&lt;/p&gt;




&lt;h2&gt;
  
  
  The invariant that's non-negotiable
&lt;/h2&gt;

&lt;p&gt;What interested me most about the repo was reading &lt;code&gt;docs/ARCHITECTURE.md&lt;/code&gt; and finding this written as a cardinal principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Byte-compatibility with DVC.&lt;/strong&gt; Anything that changes a serialized artifact (&lt;code&gt;.dvc&lt;/code&gt;, &lt;code&gt;.dir&lt;/code&gt;, cache/remote layout) must keep the oracle test (&lt;code&gt;tests/oracle/&lt;/code&gt;, which runs the real &lt;code&gt;dvc&lt;/code&gt; and compares bytes) green.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This isn't a throwaway comment in the README. It's a design invariant that runs through the entire architecture. The &lt;code&gt;internal/dvcfile&lt;/code&gt; package reads and writes &lt;code&gt;.dvc&lt;/code&gt; files byte-exact with DVC 3.x. The &lt;code&gt;internal/hashfile&lt;/code&gt; package reimplements &lt;code&gt;.dir&lt;/code&gt; manifest serialization to match &lt;em&gt;exactly&lt;/em&gt; with Python's &lt;code&gt;json.dumps&lt;/code&gt; (which has a specific key order). The &lt;code&gt;internal/lock&lt;/code&gt; package implements DVC-compatible locking so both tools can coexist in the same repo without corruption.&lt;/p&gt;

&lt;p&gt;The architecture is organized so format risk is concentrated in specific places:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;internal/
├── dvcfile/   # Read/write .dvc — byte-exact compatibility with DVC 3.x
├── hashfile/  # Parallel MD5 + .dir serialization (the trickiest compat detail)
├── cache/     # Content-addressed object store: files/md5/&amp;lt;2&amp;gt;/&amp;lt;rest&amp;gt;
├── remote/    # S3-compatible backend via minio-go
├── transfer/  # Push/fetch with integrity verification
├── checkout/  # Materialization: reflink → hardlink/symlink → copy
└── lock/      # DVC-compatible locking (global flock + JSON rwlock)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each package has a single responsibility and the highest-risk format code lives in &lt;code&gt;internal/dvcfile&lt;/code&gt; and &lt;code&gt;internal/hashfile/tree.go&lt;/code&gt;. That makes it easier to reason about where compatibility can break if DVC changes its format in a future version.&lt;/p&gt;

&lt;p&gt;CI has an &lt;code&gt;oracle&lt;/code&gt; job that installs real DVC (via &lt;code&gt;pipx install \"dvc[s3]\"&lt;/code&gt;) and runs &lt;code&gt;go test ./tests/oracle/...&lt;/code&gt; to compare bytes. If the invariant breaks, the pipeline fails. No ambiguity.&lt;/p&gt;




&lt;h2&gt;
  
  
  The honest trade-off: what you accelerate and what stays out
&lt;/h2&gt;

&lt;p&gt;lode implements the data layer: &lt;code&gt;add&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;push&lt;/code&gt;, &lt;code&gt;pull&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;checkout&lt;/code&gt;, &lt;code&gt;gc&lt;/code&gt;, &lt;code&gt;remote&lt;/code&gt;, &lt;code&gt;doctor&lt;/code&gt;, &lt;code&gt;verify&lt;/code&gt;. That covers the daily hot path for a team versioning datasets.&lt;/p&gt;

&lt;p&gt;What's &lt;strong&gt;not&lt;/strong&gt; in scope: &lt;code&gt;dvc repro&lt;/code&gt;, &lt;code&gt;dvc run&lt;/code&gt;, pipelines, transformation DAGs. The architecture didn't pretend that was straightforward to reimplement with byte-identical compatibility. They chose to define a clear perimeter and execute it well, instead of building a partial clone of all of DVC.&lt;/p&gt;

&lt;p&gt;Look at the README: &lt;em&gt;"For ML pipelines (&lt;code&gt;dvc repro&lt;/code&gt;), keep using DVC — lode accelerates the data layer and coexists with it."&lt;/em&gt; That sentence isn't an apology. It's a design decision. The two tools coexist because they share the same lock (&lt;code&gt;internal/lock&lt;/code&gt; uses global &lt;code&gt;flock&lt;/code&gt; + JSON &lt;code&gt;rwlock&lt;/code&gt; compatible with DVC) and the same artifact format. You can run &lt;code&gt;lode add&lt;/code&gt; and then &lt;code&gt;dvc repro&lt;/code&gt; without any additional synchronization layer.&lt;/p&gt;

&lt;p&gt;The main risk I see with any format reimplementation is drift: if DVC 4.x changes the &lt;code&gt;.dvc&lt;/code&gt; file schema or the &lt;code&gt;.dir&lt;/code&gt; JSON key order, lode has to update in parallel or compatibility silently breaks. The oracle test mitigates this, but only for the version of DVC installed in CI. That's not a flaw in lode's design; it's the structural cost of being compatible with a format you don't control. A team adopting it should plan for that maintenance.&lt;/p&gt;




&lt;h2&gt;
  
  
  The state DB: optimization with graceful degradation
&lt;/h2&gt;

&lt;p&gt;The mechanism I liked most about the design is how they think about the state DB. The architecture spells it out explicitly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The state DB &lt;code&gt;(inode, mtime, size) -&amp;gt; md5&lt;/code&gt; is an &lt;strong&gt;optimization, never a source of truth&lt;/strong&gt;. It can produce a false "up to date" only if a file's content changes while all three keys stay identical (e.g. NFS quirks, restored backups that reset mtimes, recycled inodes). For those cases &lt;code&gt;--rehash&lt;/code&gt; (and a corrupt/unreadable state DB) degrade to a full re-hash — the always-correct path.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a clear contract about the optimization's limits. Corrupt state or an NFS edge case doesn't break correctness: it degrades to the slow but always-correct path. The &lt;code&gt;--rehash&lt;/code&gt; flag exists exactly for this. On network filesystems or CI environments where inodes can be recycled, it's something to keep in mind.&lt;/p&gt;

&lt;p&gt;What looks like good technical maturity to me is that this limit is documented in the architecture, not buried in a GitHub issue. A team adopting it knows exactly when &lt;code&gt;lode status&lt;/code&gt; can lie (and how to force the correct path).&lt;/p&gt;




&lt;h2&gt;
  
  
  The static binary as an operational argument
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CGO_ENABLED=0&lt;/code&gt; in the build means a binary with no dynamic dependencies. That has practical implications in MLOps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make build       &lt;span class="c"&gt;# single binary with no CGO, no external runtime&lt;/span&gt;
make test-short  &lt;span class="c"&gt;# unit + oracle, no external services&lt;/span&gt;
make &lt;span class="nb"&gt;test&lt;/span&gt;        &lt;span class="c"&gt;# full suite — needs MinIO and real dvc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a training Docker image, installing Python + DVC + S3 dependencies adds layers that can total hundreds of MB and minutes of build time. A static binary is &lt;code&gt;COPY lode /usr/local/bin/lode&lt;/code&gt; and done. The release pipeline uses goreleaser with SBOM (via syft), keyless signing with cosign (OIDC), and build provenance attestation (SLSA). For a freshly built project, that level of rigor in the supply chain is a positive signal about how they think about long-term maintenance.&lt;/p&gt;




&lt;h2&gt;
  
  
  My position
&lt;/h2&gt;

&lt;p&gt;I don't buy the claim of "drop-in compatible" in absolute terms: lode is drop-in compatible for the &lt;strong&gt;data layer&lt;/strong&gt;. If your team's workflow depends on &lt;code&gt;dvc repro&lt;/code&gt;, part of your flow stays in DVC. That's not a problem, but you have to name it honestly to avoid mismatched expectations.&lt;/p&gt;

&lt;p&gt;What I do accept without reservation: the coexistence approach is technically correct. The alternative of inventing a custom format would shift the performance cost to a migration and lock-in cost. In ML teams where data artifacts are also audit evidence (experiment reproducibility, model traceability), changing that artifact format has a cost beyond engineering time.&lt;/p&gt;

&lt;p&gt;The trade-off that feels honest to me: lode solves the hot-path performance problem with a constraint that in most cases is tolerable. The risk is format drift when DVC updates its spec. The oracle test in CI is the detection mechanism, but it requires active maintenance discipline.&lt;/p&gt;

&lt;p&gt;If you manage DVC repos with large datasets and &lt;code&gt;dvc add&lt;/code&gt; or &lt;code&gt;dvc push&lt;/code&gt; time is a real bottleneck, lode deserves an evaluation. The fact that &lt;code&gt;lode verify&lt;/code&gt; and &lt;code&gt;dvc status&lt;/code&gt; can run on the same artifacts and give the same result is the contract that makes the evaluation reversible at no cost.&lt;/p&gt;

&lt;p&gt;What would you do if DVC's format changes in a minor version and silently breaks compatibility in production? Do you have an oracle test to catch it, or do you discover it on the next &lt;code&gt;dvc repro&lt;/code&gt;?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Repo analyzed: &lt;a href="https://github.com/getlode/lode" rel="noopener noreferrer"&gt;getlode/lode&lt;/a&gt; @ commit &lt;code&gt;b6e6d34&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/lode-dvc-compatible-data-versioning-go" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>machinelearning</category>
      <category>opensource</category>
      <category>go</category>
    </item>
    <item>
      <title>lode: Reimplementando el core de DVC en Go sin romper el formato</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Sun, 21 Jun 2026 07:57:57 +0000</pubDate>
      <link>https://dev.to/jtorchia/lode-reimplementando-el-core-de-dvc-en-go-sin-romper-el-formato-1ml</link>
      <guid>https://dev.to/jtorchia/lode-reimplementando-el-core-de-dvc-en-go-sin-romper-el-formato-1ml</guid>
      <description>&lt;h1&gt;
  
  
  lode: Reimplementando el core de DVC en Go sin romper el formato
&lt;/h1&gt;

&lt;p&gt;Hay un tipo de proyecto open source que me genera respeto inmediato: el que define con claridad lo que &lt;strong&gt;no&lt;/strong&gt; hace. lode es uno de esos.&lt;/p&gt;

&lt;p&gt;Cuando leí el README por primera vez, la frase que me frenó fue esta: &lt;em&gt;"lode never invents a format; your repo stays a DVC repo."&lt;/em&gt; En un ecosistema donde cada herramienta nueva quiere ser el centro de gravedad, ese nivel de renuncia intencional es raro. Y es exactamente la decisión técnica que quiero diseccionar acá.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mi tesis:&lt;/strong&gt; la compatibilidad de formato no es una feature de marketing. Es una gestión de riesgo operativo. En equipos de ML donde DVC ya está integrado en pipelines, scripts de CI y flujos de auditoría, adoptar una herramienta que inventa su propio formato de artefactos exige una migración con ventana de congelamiento. lode evita ese costo completamente, y eso tiene un precio: pipelines y &lt;code&gt;dvc repro&lt;/code&gt; quedan fuera de scope. El trade-off es honesto.&lt;/p&gt;




&lt;h2&gt;
  
  
  El problema que lode atacó
&lt;/h2&gt;

&lt;p&gt;DVC es el estándar de facto para versionar datasets y modelos en proyectos de ML. El problema no es conceptual: es el runtime. Cuando tenés un directorio con 20.000 archivos y corrés &lt;code&gt;dvc add big/&lt;/code&gt;, DVC hashea secuencialmente en Python, con toda la fricción del intérprete. El README del repo muestra una medición concreta sobre el mismo repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;time &lt;/span&gt;dvc add big/      &lt;span class="c"&gt;# 20,000 archivos&lt;/span&gt;
&lt;span class="go"&gt;real    0m5.79s

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;time &lt;/span&gt;lode add big/     &lt;span class="c"&gt;# mismo repo, resultado idéntico byte por byte&lt;/span&gt;
&lt;span class="go"&gt;real    0m0.44s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eso es una diferencia de &lt;strong&gt;~13×&lt;/strong&gt; en ese caso. No voy a universalizar ese número como garantía de performance general: depende del hardware, el sistema de archivos, el tamaño de los archivos individuales y cuántos ya están en el state DB. Lo que sí es reproducible es el mecanismo: Go compila a binario nativo sin overhead de VM, el hashing corre con &lt;code&gt;NumCPU&lt;/code&gt; goroutines en paralelo, y el state DB (bbolt, bajo &lt;code&gt;internal/hashfile&lt;/code&gt;) guarda &lt;code&gt;(inode, mtime, size) → md5&lt;/code&gt; para saltear archivos que no cambiaron. Esa combinación tiene sentido técnico independientemente del número exacto.&lt;/p&gt;

&lt;p&gt;La fricción del hot path importa más de lo que parece en flujos de ML. Un &lt;code&gt;dvc status&lt;/code&gt; lento hace que los data scientists lo eviten, lo que lleva a commits sin pointer files actualizados, lo que lleva a reproductibilidad rota. Acelerar el camino feliz tiene impacto real en disciplina del equipo.&lt;/p&gt;




&lt;h2&gt;
  
  
  La invariante que no se negocia
&lt;/h2&gt;

&lt;p&gt;Lo que más me interesó del repo fue leer &lt;code&gt;docs/ARCHITECTURE.md&lt;/code&gt; y encontrar esto escrito como principio cardinal:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Byte-compatibility with DVC.&lt;/strong&gt; Anything that changes a serialized artifact (&lt;code&gt;.dvc&lt;/code&gt;, &lt;code&gt;.dir&lt;/code&gt;, cache/remote layout) must keep the oracle test (&lt;code&gt;tests/oracle/&lt;/code&gt;, which runs the real &lt;code&gt;dvc&lt;/code&gt; and compares bytes) green.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No es un comentario en el README. Es una invariante de diseño que atraviesa toda la arquitectura. El paquete &lt;code&gt;internal/dvcfile&lt;/code&gt; lee y escribe archivos &lt;code&gt;.dvc&lt;/code&gt; byte-exact con DVC 3.x. El paquete &lt;code&gt;internal/hashfile&lt;/code&gt; reimplementa la serialización del &lt;code&gt;.dir&lt;/code&gt; manifest para que matchee &lt;em&gt;exactamente&lt;/em&gt; con &lt;code&gt;json.dumps&lt;/code&gt; de Python (que tiene un orden de claves específico). El paquete &lt;code&gt;internal/lock&lt;/code&gt; implementa locking compatible con DVC para que ambas herramientas puedan coexistir en el mismo repo sin corromperse.&lt;/p&gt;

&lt;p&gt;La arquitectura está organizada para que el riesgo de formato esté concentrado en lugares específicos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;internal/
├── dvcfile/   # Lee/escribe .dvc — compatibilidad byte-exacta con DVC 3.x
├── hashfile/  # MD5 paralelo + serialización .dir (el detalle más delicado de compat)
├── cache/     # Object store content-addressed: files/md5/&amp;lt;2&amp;gt;/&amp;lt;rest&amp;gt;
├── remote/    # Backend S3-compatible via minio-go
├── transfer/  # Push/fetch con verificación de integridad
├── checkout/  # Materialización: reflink → hardlink/symlink → copy
└── lock/      # Locking DVC-compatible (flock global + rwlock JSON)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada paquete tiene una responsabilidad única y el código de mayor riesgo de formato está aislado en &lt;code&gt;internal/dvcfile&lt;/code&gt; e &lt;code&gt;internal/hashfile/tree.go&lt;/code&gt;. Eso facilita razonar sobre dónde puede romperse la compatibilidad si DVC cambia su formato en una versión futura.&lt;/p&gt;

&lt;p&gt;El CI tiene un job &lt;code&gt;oracle&lt;/code&gt; que instala el DVC real (via &lt;code&gt;pipx install "dvc[s3]"&lt;/code&gt;) y corre &lt;code&gt;go test ./tests/oracle/...&lt;/code&gt; para comparar bytes. Si la invariante se rompe, el pipeline falla. No hay ambigüedad.&lt;/p&gt;




&lt;h2&gt;
  
  
  El trade-off honesto: qué acelerás y qué dejás afuera
&lt;/h2&gt;

&lt;p&gt;lode implementa el data layer: &lt;code&gt;add&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;push&lt;/code&gt;, &lt;code&gt;pull&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;checkout&lt;/code&gt;, &lt;code&gt;gc&lt;/code&gt;, &lt;code&gt;remote&lt;/code&gt;, &lt;code&gt;doctor&lt;/code&gt;, &lt;code&gt;verify&lt;/code&gt;. Eso cubre el hot path diario de un equipo que versiona datasets.&lt;/p&gt;

&lt;p&gt;Lo que &lt;strong&gt;no&lt;/strong&gt; está en scope: &lt;code&gt;dvc repro&lt;/code&gt;, &lt;code&gt;dvc run&lt;/code&gt;, pipelines, DAGs de transformación. La arquitectura no fingió que eso era sencillo de reimplementar con compatibilidad byte-identical. Optaron por definir un perímetro claro y ejecutarlo bien, en lugar de hacer un clon parcial de todo DVC.&lt;/p&gt;

&lt;p&gt;Mirá el README: &lt;em&gt;"For ML pipelines (&lt;code&gt;dvc repro&lt;/code&gt;), keep using DVC — lode accelerates the data layer and coexists with it."&lt;/em&gt; Esa frase no es una disculpa. Es una decisión de diseño. Los dos tools conviven porque comparten el mismo lock (&lt;code&gt;internal/lock&lt;/code&gt; usa &lt;code&gt;flock&lt;/code&gt; global + &lt;code&gt;rwlock&lt;/code&gt; JSON compatible con DVC) y el mismo formato de artefactos. Podés correr &lt;code&gt;lode add&lt;/code&gt; y después &lt;code&gt;dvc repro&lt;/code&gt; sin ninguna capa de sincronización adicional.&lt;/p&gt;

&lt;p&gt;El riesgo principal que veo con cualquier reimplementación de formato es la deriva: si DVC 4.x cambia el schema del &lt;code&gt;.dvc&lt;/code&gt; file o el orden de claves del &lt;code&gt;.dir&lt;/code&gt; JSON, lode tiene que actualizarse en paralelo o la compatibilidad se rompe silenciosamente. El oracle test mitiga esto, pero solo para la versión de DVC que está instalada en CI. Eso no es un defecto del diseño de lode; es el costo estructural de ser compatible con un formato que no controlás. Un equipo que lo adopte debería planear ese seguimiento.&lt;/p&gt;




&lt;h2&gt;
  
  
  El state DB: optimización con degradación grácil
&lt;/h2&gt;

&lt;p&gt;El mecanismo que más me gustó del diseño es cómo piensan el state DB. La arquitectura lo dice explícitamente:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The state DB &lt;code&gt;(inode, mtime, size) -&amp;gt; md5&lt;/code&gt; is an &lt;strong&gt;optimization, never a source of truth&lt;/strong&gt;. It can produce a false "up to date" only if a file's content changes while all three keys stay identical (e.g. NFS quirks, restored backups that reset mtimes, recycled inodes). For those cases &lt;code&gt;--rehash&lt;/code&gt; (and a corrupt/unreadable state DB) degrade to a full re-hash — the always-correct path.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Eso es un contrato claro sobre los límites de la optimización. El estado corrupto o un edge case de NFS no rompen la correctness: degradan a la ruta lenta pero siempre correcta. El flag &lt;code&gt;--rehash&lt;/code&gt; existe exactamente para eso. En sistemas de archivos de red o entornos de CI donde los inodes pueden reciclarse, es algo a tener en cuenta.&lt;/p&gt;

&lt;p&gt;Lo que me parece un buen indicador de madurez técnica es que este límite está documentado en la arquitectura, no escondido en un issue de GitHub. Un equipo que lo adopte sabe exactamente cuándo &lt;code&gt;lode status&lt;/code&gt; puede mentir (y cómo forzar la ruta correcta).&lt;/p&gt;




&lt;h2&gt;
  
  
  El binario estático como argumento operativo
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CGO_ENABLED=0&lt;/code&gt; en el build significa un binario sin dependencias dinámicas. Eso tiene implicaciones prácticas en MLOps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make build       &lt;span class="c"&gt;# binario único sin CGO, sin runtime externo&lt;/span&gt;
make test-short  &lt;span class="c"&gt;# unit + oracle, sin servicios externos&lt;/span&gt;
make &lt;span class="nb"&gt;test&lt;/span&gt;        &lt;span class="c"&gt;# suite completa — necesita MinIO y dvc real&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En una imagen Docker de entrenamiento, instalar Python + DVC + dependencias S3 agrega capas que pueden sumar cientos de MB y minutos de build. Un binario estático es &lt;code&gt;COPY lode /usr/local/bin/lode&lt;/code&gt; y terminó. El release pipeline usa goreleaser con SBOM (via syft), firma keyless con cosign (OIDC) y attestation de build provenance (SLSA). Para un proyecto recién armado, ese nivel de rigor en la cadena de supply chain es una señal positiva sobre cómo piensan el mantenimiento a largo plazo.&lt;/p&gt;




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

&lt;p&gt;No compro el claim de "drop-in compatible" de forma absoluta: lode es drop-in compatible para el &lt;strong&gt;data layer&lt;/strong&gt;. Si el workflow del equipo depende de &lt;code&gt;dvc repro&lt;/code&gt;, hay una parte del flujo que sigue en DVC. Eso no es un problema, pero hay que nombrarlo honestamente para no generar expectativas incorrectas.&lt;/p&gt;

&lt;p&gt;Lo que sí acepto sin reservas: el enfoque de coexistencia es técnicamente correcto. La alternativa de inventar un formato propio trasladaría el costo de la performance a un costo de migración y lock-in. En equipos de ML donde los artefactos de datos son también evidencia de auditoría (reproducibilidad de experimentos, trazabilidad de modelos), cambiar el formato de esos artefactos tiene un costo que va más allá del tiempo de ingeniería.&lt;/p&gt;

&lt;p&gt;El trade-off que me parece honesto: lode resuelve el problema de performance del hot path con una restricción que en la mayoría de los casos es tolerable. El riesgo es la deriva de formato cuando DVC actualice su spec. El oracle test en CI es el mecanismo de detección, pero requiere disciplina de mantenimiento activo.&lt;/p&gt;

&lt;p&gt;Si manejás repos DVC con datasets grandes y el tiempo de &lt;code&gt;dvc add&lt;/code&gt; o &lt;code&gt;dvc push&lt;/code&gt; es un cuello de botella real, lode merece una evaluación. El hecho de que &lt;code&gt;lode verify&lt;/code&gt; y &lt;code&gt;dvc status&lt;/code&gt; puedan correr sobre los mismos artefactos y dar el mismo resultado es el contrato que hace que la evaluación sea reversible sin costo.&lt;/p&gt;

&lt;p&gt;¿Qué harías vos si el formato de DVC cambia en una minor version y rompe silenciosamente la compatibilidad en producción? ¿Tenés un oracle test que lo detecte, o lo descubrís en el próximo &lt;code&gt;dvc repro&lt;/code&gt;?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Repo analizado: &lt;a href="https://github.com/getlode/lode" rel="noopener noreferrer"&gt;getlode/lode&lt;/a&gt; @ commit &lt;code&gt;b6e6d34&lt;/code&gt;&lt;/em&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/lode-dvc-compatible-data-versioning-go" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>machinelearning</category>
      <category>opensource</category>
    </item>
    <item>
      <title>OWASP LLM Top 10 in Production: How I Audited My TypeScript Agent Pipeline Against All 10 Risks — and What I Found</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Sat, 20 Jun 2026 12:02:58 +0000</pubDate>
      <link>https://dev.to/jtorchia/owasp-llm-top-10-in-production-how-i-audited-my-typescript-agent-pipeline-against-all-10-risks--3jll</link>
      <guid>https://dev.to/jtorchia/owasp-llm-top-10-in-production-how-i-audited-my-typescript-agent-pipeline-against-all-10-risks--3jll</guid>
      <description>&lt;h1&gt;
  
  
  OWASP LLM Top 10 in Production: How I Audited My TypeScript Agent Pipeline Against All 10 Risks — and What I Found
&lt;/h1&gt;

&lt;p&gt;I was reviewing a system prompt for an MCP agent I'd written three weeks earlier when something hit me hard: the prompt was accepting instructions from the output of an external tool. No sanitization. No validation. No limits whatsoever on what it could do with that output. The tool called a public API, got back JSON, and that JSON landed directly in the model's context.&lt;/p&gt;

&lt;p&gt;That's when I opened the &lt;a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/" rel="noopener noreferrer"&gt;OWASP LLM Top 10&lt;/a&gt; and stopped reading it like a list of best practices — and started using it for what it actually is: an audit framework.&lt;/p&gt;

&lt;p&gt;My thesis is simple: most posts about the OWASP LLM Top 10 explain the ten risks to you. None of them show you how to run them against your own stack and what you actually find when you do it seriously. That's the difference between "reading the checklist" and "auditing the pipeline." This post is the second thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack I Audited — and Why Context Matters
&lt;/h2&gt;

&lt;p&gt;Before getting into the checklist, some context: I have a TypeScript agent pipeline with three layers that interact with each other:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Structured system prompts&lt;/strong&gt; — instructions that define agent behavior, kept separate from user context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP tools&lt;/strong&gt; — tools registered following the Model Context Protocol, which the agent can call during a session&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cline as the client&lt;/strong&gt; — orchestrating execution inside the editor, with access to filesystem, terminal, and other tools&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each layer has a different attack surface. That's exactly what the OWASP LLM Top 10 let me see with surgical precision.&lt;/p&gt;




&lt;h2&gt;
  
  
  All 10 Risks: What I Found in Each One
&lt;/h2&gt;

&lt;h3&gt;
  
  
  LLM01 — Prompt Injection
&lt;/h3&gt;

&lt;p&gt;This was the biggest finding. My MCP agent was receiving output from external tools and injecting it directly into context with zero sanitization layer. In an adversarial scenario, any API the agent queried could return text specifically crafted to overwrite the system prompt instructions.&lt;/p&gt;

&lt;p&gt;The broken pattern looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Insecure pattern: external output goes straight into context&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchContextAndInject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// data.content reaches the model context with no filtering whatsoever&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&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;What I changed it to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Structural validation before injecting into context&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ExternalResponseSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// Only accept fields with defined types — free-form strings flagged as suspicious&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;// Anything not in the schema gets discarded&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchContextSafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// If the schema fails, the agent gets a structured error — not the raw payload&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ExternalResponseSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Title: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nSummary: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used Zod — which was already in the stack for API validation — as the first line of defense. It's not a complete solution to prompt injection, but it reduces the structural attack surface.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLM02 — Insecure Output Handling
&lt;/h3&gt;

&lt;p&gt;The second problem: agent output was reaching the UI without escaping. In an agent that generates HTML or Markdown, that's potential XSS if the output gets rendered directly.&lt;/p&gt;

&lt;p&gt;I traced every place where model output touched the DOM and added explicit sanitization before any render. If the agent generates code, that code goes into a &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; block with escaped characters — not into an &lt;code&gt;innerHTML&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLM03 — Training Data Poisoning
&lt;/h3&gt;

&lt;p&gt;Here the OWASP LLM Top 10 points to risks in the base model, not the application. In my case the model is Claude via API — I don't control the fine-tuning or the dataset. My only action was to document this dependency explicitly: &lt;strong&gt;if Anthropic has a problem here, I have a problem here.&lt;/strong&gt; No system prompt compensates for that.&lt;/p&gt;

&lt;p&gt;Honest limit: you can't audit this from the application layer. It's a dependency you take as a trust boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLM04 — Model Denial of Service
&lt;/h3&gt;

&lt;p&gt;I checked whether I had rate limiting on the endpoints that trigger model calls. I didn't — at least not in the local testing context. In a production scenario this is critical: a badly designed loop or a tool that calls recursively can fire dozens of model requests in seconds.&lt;/p&gt;

&lt;p&gt;I added a simple iteration cap to the agent loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Iteration control to prevent infinite loops in the agent&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_ITERATIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;iterations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agentShouldContinue&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;iterations&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_ITERATIONS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;iterations&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runAgentStep&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;agentShouldContinue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;continueLoop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iterations&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_ITERATIONS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Explicit log — I want to know if this ever fires&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[agent] Iteration limit reached — review loop&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  LLM05 — Supply Chain Vulnerabilities
&lt;/h3&gt;

&lt;p&gt;This risk made me look at two things: the npm packages I use to interact with the model API, and the dependencies of my MCP tools. With pnpm workspaces (something I covered in &lt;a href="https://juanchi.dev/en/blog/pnpm-workspaces-monorepo-ci-railway-real-problems" rel="noopener noreferrer"&gt;the monorepo with Railway post&lt;/a&gt;) you get lockfile visibility — but that's not the same as auditing.&lt;/p&gt;

&lt;p&gt;What I added: &lt;code&gt;pnpm audit&lt;/code&gt; as an explicit CI step before deploying any agent. It doesn't eliminate the risk, but it makes it visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLM06 — Sensitive Information Disclosure
&lt;/h3&gt;

&lt;p&gt;This is where the second uncomfortable finding showed up: my system prompts contained configuration context that included names of internal tools, data structure details, and some system defaults. That context reaches the model — and if the model echoes it in its output, it's exposed.&lt;/p&gt;

&lt;p&gt;The rule I applied: &lt;strong&gt;nothing you wouldn't want to see in a public log should be in a system prompt without explicit confidentiality marking.&lt;/strong&gt; And even that isn't a guarantee — it's mitigation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Separate technical config from agent instructions&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_PROMPT_PUBLIC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
You are a development assistant. You can use available tools
to answer technical questions.
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// This does NOT go into the system prompt — it lives in a separate config layer&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AGENT_CONFIG_PRIVATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;toolEndpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TOOL_ENDPOINTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;internalSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INTERNAL_SCHEMA&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;h3&gt;
  
  
  LLM07 — Plugin Design Flaws
&lt;/h3&gt;

&lt;p&gt;My MCP tools are essentially plugins. The risk here is that a tool has broader permissions than it actually needs. I reviewed each tool and applied least privilege: a tool that reads files doesn't need write access; a tool that queries an API doesn't need filesystem access.&lt;/p&gt;

&lt;p&gt;This connects directly to what I wrote about &lt;a href="https://juanchi.dev/en/blog/oauth-scope-creep-vercel-incident-audit-integrations" rel="noopener noreferrer"&gt;OAuth scope creep&lt;/a&gt; — the same audit pattern applies to an agent's tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLM08 — Excessive Agency
&lt;/h3&gt;

&lt;p&gt;This is the risk that concerns me most specifically with Cline. The agent has terminal access, can execute commands, can modify files. If the reasoning loop fails, it can cause real damage.&lt;/p&gt;

&lt;p&gt;What I implemented: "confirm before execute" mode for any tool with an irreversible side effect. It's not automatable — it requires deliberate human friction. And that friction is the entire point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Explicit tool classification by impact&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ToolImpact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read-only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reversible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destructive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOOL_IMPACT_MAP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ToolImpact&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read-only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;listDirectory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read-only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reversible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;deleteFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destructive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;runCommand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destructive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;executeTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;impact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TOOL_IMPACT_MAP&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destructive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// safe fallback&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;impact&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destructive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Pause and wait for human confirmation before executing&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;requireHumanApproval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;runTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&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;h3&gt;
  
  
  LLM09 — Overreliance
&lt;/h3&gt;

&lt;p&gt;This isn't a purely technical risk — it's organizational. The problem is trusting the agent's output without external validation. In my pipeline, any output going to production passes through a structural validation layer before it's used as input to another system. The model can be fine, the pipeline can be fine, and the output can still be wrong.&lt;/p&gt;

&lt;p&gt;This risk doesn't close with code. It closes with process and human review at critical nodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLM10 — Model Theft
&lt;/h3&gt;

&lt;p&gt;In my TypeScript agent context, this mainly applies to protecting system prompts. A well-crafted system prompt represents real work — and if it leaks, it can be replicated or used to bypass restrictions.&lt;/p&gt;

&lt;p&gt;What I implemented: system prompts don't live in frontend code. They're served from an authenticated endpoint, they're not logged in plain text, and they don't get exposed in the client bundle.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the OWASP LLM Top 10 Doesn't Tell You (Which Matters Just as Much)
&lt;/h2&gt;

&lt;p&gt;Here's what the list doesn't resolve on its own:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't tell you the priority order for your stack.&lt;/strong&gt; LLM01 (prompt injection) was critical in my case; LLM03 (training data poisoning) is irrelevant from the application layer. Without applying it against your concrete architecture, you don't know which one is urgent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It gives you no criteria for the trust boundary of the base model.&lt;/strong&gt; If you use Claude, GPT-4, or any external API, LLM03 and part of LLM05 are dependencies you take as given. The framework names them, but the mitigation is out of your hands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't distinguish between runtime risks and design risks.&lt;/strong&gt; LLM01 and LLM02 are problems you can detect and mitigate at runtime. LLM08 (excessive agency) is a design problem — if the agent has too many permissions, a runtime patch doesn't fix it.&lt;/p&gt;

&lt;p&gt;I have a post on &lt;a href="https://juanchi.dev/en/blog/opentelemetry-nextjs-traces-edge-server-context" rel="noopener noreferrer"&gt;OpenTelemetry in Next.js&lt;/a&gt; where I talk about traces that survive the edge. That kind of observability helps here too: if you can't see which tools the agent called and with what args, you can't audit LLM08 in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Applied Checklist: The Real State of Each Risk in My Pipeline
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;State Found&lt;/th&gt;
&lt;th&gt;Action Taken&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LLM01 Prompt Injection&lt;/td&gt;
&lt;td&gt;❌ Vulnerable&lt;/td&gt;
&lt;td&gt;Zod schema on external tool output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM02 Insecure Output&lt;/td&gt;
&lt;td&gt;⚠️ Partial&lt;/td&gt;
&lt;td&gt;Explicit escaping before render&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM03 Training Data&lt;/td&gt;
&lt;td&gt;🔵 Out of scope&lt;/td&gt;
&lt;td&gt;Documented as trust boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM04 Model DoS&lt;/td&gt;
&lt;td&gt;⚠️ No limit&lt;/td&gt;
&lt;td&gt;Added max iterations + log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM05 Supply Chain&lt;/td&gt;
&lt;td&gt;⚠️ Invisible&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pnpm audit&lt;/code&gt; in CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM06 Info Disclosure&lt;/td&gt;
&lt;td&gt;❌ Leaky prompts&lt;/td&gt;
&lt;td&gt;Separated config from system prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM07 Plugin Flaws&lt;/td&gt;
&lt;td&gt;⚠️ Partial&lt;/td&gt;
&lt;td&gt;Permission review per tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM08 Excessive Agency&lt;/td&gt;
&lt;td&gt;⚠️ No friction&lt;/td&gt;
&lt;td&gt;Confirm before execute on destructive tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM09 Overreliance&lt;/td&gt;
&lt;td&gt;🔵 Process&lt;/td&gt;
&lt;td&gt;Human validation at critical nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM10 Model Theft&lt;/td&gt;
&lt;td&gt;⚠️ Prompts exposed&lt;/td&gt;
&lt;td&gt;Prompts moved to authenticated endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;❌ = critical finding | ⚠️ = partial mitigation | 🔵 = outside application control&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Does the OWASP LLM Top 10 apply to agents built on Claude or GPT-4 via API?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, with nuance. LLM01, LLM02, LLM06, LLM07, LLM08, and LLM10 are application-layer risks — they apply regardless of which model you use. LLM03 (training data) and part of LLM05 are provider risks: if you use an external API, you take them as a trust boundary. The audit starts with the risks you can actually control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Zod enough to mitigate prompt injection?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Zod validates the structure of external output before it reaches context — that reduces the surface area, but it doesn't eliminate the risk. A well-formed adversarial payload can pass schema validation. Zod is one layer, not a complete solution. Real mitigation combines schema validation, system prompt constraints, and human review at critical points.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Cline safe to use in production as an agent orchestrator?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cline has access to the filesystem, terminal, and other tools with real effects. That's not inherently unsafe — it's the functionality that makes it useful. The risk (LLM08) is in the design: if the agent can execute destructive commands without human confirmation, the risk is real regardless of how well Cline is configured. My rule: any tool with an irreversible effect requires explicit approval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How often should you run this audit?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every time you change the agent's architecture: you add a new tool, change the system prompt, or modify how the agent consumes external outputs. It's not a one-time audit — it's a checklist that runs against every structural change. If you add observability (&lt;a href="https://juanchi.dev/en/blog/opentelemetry-nextjs-traces-edge-server-context" rel="noopener noreferrer"&gt;OpenTelemetry&lt;/a&gt; is one option), you can catch runtime anomalies between audits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the OWASP LLM Top 10 cover multi-agent risks or just single-agent?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The current version (&lt;a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/" rel="noopener noreferrer"&gt;2025&lt;/a&gt;) primarily covers per-agent risk. In multi-agent architectures, LLM01's surface multiplies: each agent can become an injection vector for the others. The framework names the risk, but the mitigation detail for multi-agent pipelines is left to each team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which risk should I tackle first if I have limited time?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LLM01 (prompt injection) if your agent consumes external output — it's the most exploitable and the most overlooked. LLM08 (excessive agency) if the agent has access to tools with irreversible effects — it's the one that can do the most damage when something goes wrong. The rest depend on your specific stack, but these two are the absolute floor.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Difference Between Reading and Auditing
&lt;/h2&gt;

&lt;p&gt;My position is clear: the OWASP LLM Top 10 is not something you read and consider covered. It's something you bring into a review session with the architecture diagram open in front of you, and you ask — for each risk — exactly where in the pipeline that could fail.&lt;/p&gt;

&lt;p&gt;What I don't buy is the idea that "following best practices" is enough. Practices are abstract; the pipeline is concrete. In my case, LLM01 and LLM06 were real problems I wouldn't have found without doing the systematic audit exercise. I would have discovered them when someone motivated enough decided to exploit them.&lt;/p&gt;

&lt;p&gt;If you already have TypeScript agents with MCP tools or elaborate system prompts, do the exercise: open the OWASP LLM Top 10, open the architecture diagram, and ask risk by risk. The result will be more interesting than the list itself.&lt;/p&gt;

&lt;p&gt;Concrete next step: take the checklist from this table, replace the states with your own, and document the findings. An audit that isn't documented doesn't exist.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;OWASP LLM AI Security &amp;amp; Governance Checklist: &lt;a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/" rel="noopener noreferrer"&gt;https://owasp.org/www-project-top-10-for-large-language-model-applications/&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/owasp-llm-top-10-audit-typescript-agent-pipeline" 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>
    <item>
      <title>OWASP LLM Top 10 en producción: cómo audité mi pipeline de agentes TypeScript contra los 10 riesgos y qué encontré</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Sat, 20 Jun 2026 12:02:53 +0000</pubDate>
      <link>https://dev.to/jtorchia/owasp-llm-top-10-en-produccion-como-audite-mi-pipeline-de-agentes-typescript-contra-los-10-riesgos-11fd</link>
      <guid>https://dev.to/jtorchia/owasp-llm-top-10-en-produccion-como-audite-mi-pipeline-de-agentes-typescript-contra-los-10-riesgos-11fd</guid>
      <description>&lt;h1&gt;
  
  
  OWASP LLM Top 10 en producción: cómo audité mi pipeline de agentes TypeScript contra los 10 riesgos y qué encontré
&lt;/h1&gt;

&lt;p&gt;Estaba revisando un system prompt de un agente MCP que había escrito tres semanas antes cuando me di cuenta de algo perturbador: el prompt aceptaba instrucciones de la respuesta de una tool externa. Sin sanitización. Sin validación. Sin ningún límite sobre qué podía hacer con esa salida. La tool llamaba a una API pública, recibía JSON, y ese JSON llegaba directo al contexto del modelo.&lt;/p&gt;

&lt;p&gt;Ahí fue cuando abrí el &lt;a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/" rel="noopener noreferrer"&gt;OWASP LLM Top 10&lt;/a&gt; y paré de leerlo como lista de buenas prácticas para empezar a usarlo como lo que en realidad es: un framework de auditoría.&lt;/p&gt;

&lt;p&gt;Mi tesis es esta: la mayoría de los posts sobre OWASP LLM Top 10 te explican los diez riesgos. Ninguno te muestra cómo correrlos contra tu stack propio y qué encontrás cuando lo hacés en serio. Esa es la diferencia entre "leer el checklist" y "auditar el pipeline". Acá está lo segundo.&lt;/p&gt;




&lt;h2&gt;
  
  
  El stack que audité y por qué importa el contexto
&lt;/h2&gt;

&lt;p&gt;Antes de entrar al checklist, el contexto: tengo un pipeline de agentes en TypeScript con tres capas que interactúan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;System prompts estructurados&lt;/strong&gt; — instrucciones que definen el comportamiento del agente, separadas del contexto de usuario&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP tools&lt;/strong&gt; — tools registradas siguiendo el Model Context Protocol, que el agente puede llamar durante una sesión&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cline como cliente&lt;/strong&gt; — que orquesta la ejecución en el editor y tiene acceso a filesystem, terminal y otras herramientas&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cada capa tiene una superficie de ataque diferente. Eso es lo que el OWASP LLM Top 10 me permitió ver con precisión quirúrgica.&lt;/p&gt;




&lt;h2&gt;
  
  
  Los 10 riesgos: qué encontré en cada uno
&lt;/h2&gt;

&lt;h3&gt;
  
  
  LLM01 — Prompt Injection
&lt;/h3&gt;

&lt;p&gt;Este fue el hallazgo más gordo. Mi agente MCP recibía output de tools externas y lo incorporaba al contexto sin ninguna capa de sanitización. En un escenario adversarial, cualquier API que el agente consultara podría devolver texto diseñado para sobrescribir las instrucciones del system prompt.&lt;/p&gt;

&lt;p&gt;El patrón roto era este:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Patrón inseguro: output externo directo al contexto&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchContextAndInject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// data.content llega sin ningún filtro al contexto del modelo&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lo que cambié:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Validación de estructura antes de incorporar al contexto&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ExternalResponseSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// Solo acepto campos con tipo definido — string libre marcado como sospechoso&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;// Descarto cualquier campo que no esté en el schema&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchContextSafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// Si el schema falla, el agente recibe un error estructurado, no el payload crudo&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ExternalResponseSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Título: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nResumen: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usé Zod — que ya tenía en el stack para validación de API — como primera línea. No es una solución completa al prompt injection, pero reduce la superficie de ataque estructural.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLM02 — Insecure Output Handling
&lt;/h3&gt;

&lt;p&gt;El segundo problema: el output del agente llegaba a la UI sin escaping. En un agente que genera HTML o Markdown, eso es XSS potencial si el output se renderiza directamente.&lt;/p&gt;

&lt;p&gt;Revisé dónde el output del modelo llegaba al DOM y agregué sanitización explícita antes de cualquier render. Si el agente genera código, ese código va a un bloque &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; con escape de caracteres; no a un &lt;code&gt;innerHTML&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLM03 — Training Data Poisoning
&lt;/h3&gt;

&lt;p&gt;Acá el OWASP LLM Top 10 apunta a riesgos del modelo base, no de la aplicación. En mi caso el modelo es Claude vía API — no controlo el fine-tuning ni el dataset. Mi única acción fue documentar esta dependencia explícitamente: &lt;strong&gt;si Anthropic tiene un problema acá, yo tengo un problema acá&lt;/strong&gt;. Ningún sistema prompt lo compensa.&lt;/p&gt;

&lt;p&gt;Límite honesto: no podés auditar esto desde la aplicación. Es una dependencia que tomás como trust boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLM04 — Model Denial of Service
&lt;/h3&gt;

&lt;p&gt;Revisé si tenía rate limiting en los endpoints que disparan llamadas al modelo. No lo tenía en el contexto de pruebas locales. En un escenario de producción esto es crítico: un loop mal diseñado o una tool que llama recursivamente puede generar decenas de requests al modelo en segundos.&lt;/p&gt;

&lt;p&gt;Agregué un límite simple de iteraciones al loop del agente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Control de iteraciones para evitar loops infinitos en el agente&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_ITERATIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;iterations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agentShouldContinue&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;iterations&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_ITERATIONS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;iterations&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runAgentStep&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;agentShouldContinue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;continueLoop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iterations&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_ITERATIONS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Log explícito — quiero saber si esto se dispara&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[agente] Límite de iteraciones alcanzado — revisar loop&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  LLM05 — Supply Chain Vulnerabilities
&lt;/h3&gt;

&lt;p&gt;Este riesgo me hizo revisar dos cosas: los paquetes npm que uso para interactuar con la API del modelo y las dependencias de mis MCP tools. Con pnpm workspaces (tema que ya cubrí en &lt;a href="https://juanchi.dev/es/blog/pnpm-workspaces-monorepo-ci-railway-problemas" rel="noopener noreferrer"&gt;el post de monorepo con Railway&lt;/a&gt;) tenés visibilidad del lockfile, pero eso no es auditoría.&lt;/p&gt;

&lt;p&gt;Lo que agregué: &lt;code&gt;pnpm audit&lt;/code&gt; como paso explícito en CI antes del deploy de cualquier agente. No elimina el riesgo, pero lo hace visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLM06 — Sensitive Information Disclosure
&lt;/h3&gt;

&lt;p&gt;Acá encontré el segundo hallazgo incómodo: en los system prompts tenía contexto de configuración que incluía nombres de tools internas, estructura de datos y algunos defaults del sistema. Ese contexto llega al modelo — y si el modelo lo repite en su output, lo expone.&lt;/p&gt;

&lt;p&gt;La regla que apliqué: &lt;strong&gt;nada que no quieras ver en un log público debería estar en un system prompt sin marcado explícito de confidencialidad&lt;/strong&gt;. Y eso tampoco es garantía — es mitigación.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Separar configuración técnica de instrucciones del agente&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_PROMPT_PUBLIC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
Sos un asistente de desarrollo. Podés usar las herramientas disponibles
para responder preguntas técnicas.
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Esto NO va al system prompt — va a una capa de configuración separada&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AGENT_CONFIG_PRIVATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;toolEndpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TOOL_ENDPOINTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;internalSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INTERNAL_SCHEMA&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;h3&gt;
  
  
  LLM07 — Plugin Design Flaws
&lt;/h3&gt;

&lt;p&gt;Mis MCP tools son básicamente plugins. El riesgo acá es que una tool tenga permisos más amplios de lo necesario. Revisé cada tool y apliqué el principio de mínimo privilegio: una tool que lee archivos no necesita escribir; una tool que consulta una API no necesita acceso al filesystem.&lt;/p&gt;

&lt;p&gt;Esto conecta con lo que escribí sobre &lt;a href="https://juanchi.dev/es/blog/oauth-scope-creep-auditoria-integraciones-terceros-seguridad" rel="noopener noreferrer"&gt;OAuth scope creep&lt;/a&gt; — el mismo patrón de auditoría aplica a las tools de un agente.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLM08 — Excessive Agency
&lt;/h3&gt;

&lt;p&gt;Este es el riesgo que más me preocupa en Cline específicamente. El agente tiene acceso a terminal, puede ejecutar comandos, puede modificar archivos. Si el loop de razonamiento falla, puede hacer daño real.&lt;/p&gt;

&lt;p&gt;Lo que implementé: modo "confirm before execute" para cualquier tool con efecto secundario irreversible. No es automatizable — requiere fricción humana deliberada. Y esa fricción es el punto.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Clasificación explícita de tools por impacto&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ToolImpact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read-only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reversible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destructive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOOL_IMPACT_MAP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ToolImpact&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read-only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;listDirectory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read-only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reversible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;deleteFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destructive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;runCommand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destructive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;executeTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;impact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TOOL_IMPACT_MAP&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destructive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// fallback seguro&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;impact&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destructive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Pausa y espera confirmación humana antes de ejecutar&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;requireHumanApproval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;runTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&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;h3&gt;
  
  
  LLM09 — Overreliance
&lt;/h3&gt;

&lt;p&gt;No es un riesgo técnico puro — es organizacional. El problema es confiar en el output del agente sin validación externa. En mi pipeline, cualquier output que va a producción pasa por una capa de validación estructural antes de ser usado como input de otro sistema. El modelo puede estar seguro, el pipeline puede estar seguro, y el output puede seguir siendo incorrecto.&lt;/p&gt;

&lt;p&gt;Este riesgo no se cierra con código. Se cierra con proceso y revisión humana en los nodos críticos.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLM10 — Model Theft
&lt;/h3&gt;

&lt;p&gt;En mi contexto de agente TypeScript, esto aplica principalmente a la protección de los system prompts. Un system prompt elaborado representa trabajo real — y si se expone, puede ser replicado o usado para evadir restricciones.&lt;/p&gt;

&lt;p&gt;Lo que implementé: los system prompts no viven en el código del frontend. Se sirven desde un endpoint autenticado, no se loguean en texto plano y no se exponen en el bundle del cliente.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lo que el OWASP LLM Top 10 no te dice (y es igual de importante)
&lt;/h2&gt;

&lt;p&gt;Acá está lo que la lista no resuelve sola:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No te dice el orden de prioridad para tu stack.&lt;/strong&gt; LLM01 (prompt injection) fue crítico en mi caso; LLM03 (training data poisoning) es irrelevante desde la aplicación. Sin aplicarlo contra tu arquitectura concreta, no sabés cuál es urgente.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No te da criterio para el trust boundary del modelo base.&lt;/strong&gt; Si usás Claude, GPT-4 o cualquier API externa, LLM03 y parte de LLM05 son dependencias que tomás como dadas. El framework las nombra, pero la mitigación no está en tus manos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No distingue entre riesgos de runtime y riesgos de diseño.&lt;/strong&gt; LLM01 y LLM02 son problemas que podés detectar y mitigar en runtime. LLM08 (excessive agency) es un problema de diseño — si el agente tiene demasiados permisos, un patch de runtime no lo arregla.&lt;/p&gt;

&lt;p&gt;Tengo un post sobre &lt;a href="https://juanchi.dev/es/blog/opentelemetry-nextjs-traces-edge-runtime-contexto" rel="noopener noreferrer"&gt;OpenTelemetry en Next.js&lt;/a&gt; donde hablo de traces que sobreviven el edge. Ese tipo de observabilidad también ayuda acá: si no podés ver qué tools llamó el agente y con qué args, no podés auditar LLM08 en producción.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checklist aplicado: el estado real de cada riesgo en mi pipeline
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Riesgo&lt;/th&gt;
&lt;th&gt;Estado encontrado&lt;/th&gt;
&lt;th&gt;Acción tomada&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LLM01 Prompt Injection&lt;/td&gt;
&lt;td&gt;❌ Vulnerable&lt;/td&gt;
&lt;td&gt;Zod schema en output de tools externas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM02 Insecure Output&lt;/td&gt;
&lt;td&gt;⚠️ Parcial&lt;/td&gt;
&lt;td&gt;Escaping explícito antes de render&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM03 Training Data&lt;/td&gt;
&lt;td&gt;🔵 Fuera de scope&lt;/td&gt;
&lt;td&gt;Documentado como trust boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM04 Model DoS&lt;/td&gt;
&lt;td&gt;⚠️ Sin límite&lt;/td&gt;
&lt;td&gt;Agregué max iterations + log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM05 Supply Chain&lt;/td&gt;
&lt;td&gt;⚠️ Invisible&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pnpm audit&lt;/code&gt; en CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM06 Info Disclosure&lt;/td&gt;
&lt;td&gt;❌ Leaky prompts&lt;/td&gt;
&lt;td&gt;Separé config de system prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM07 Plugin Flaws&lt;/td&gt;
&lt;td&gt;⚠️ Parcial&lt;/td&gt;
&lt;td&gt;Revisión de permisos por tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM08 Excessive Agency&lt;/td&gt;
&lt;td&gt;⚠️ Sin fricción&lt;/td&gt;
&lt;td&gt;Confirm before execute en tools destructivas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM09 Overreliance&lt;/td&gt;
&lt;td&gt;🔵 Proceso&lt;/td&gt;
&lt;td&gt;Validación humana en nodos críticos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM10 Model Theft&lt;/td&gt;
&lt;td&gt;⚠️ Prompts expuestos&lt;/td&gt;
&lt;td&gt;Prompts a endpoint autenticado&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;❌ = hallazgo crítico | ⚠️ = mitigación parcial | 🔵 = fuera del control de la aplicación&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;¿El OWASP LLM Top 10 aplica a agentes basados en Claude o GPT-4 vía API?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sí, con matices. LLM01, LLM02, LLM06, LLM07, LLM08 y LLM10 son riesgos de la aplicación — aplican sin importar qué modelo uses. LLM03 (training data) y parte de LLM05 son riesgos del proveedor: si usás una API externa, los tomás como trust boundary. La auditoría empieza por los riesgos que sí podés controlar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Con Zod alcanza para mitigar prompt injection?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Zod valida la estructura del output externo antes de que llegue al contexto — eso reduce la superficie, pero no elimina el riesgo. Un payload adversarial bien formado puede pasar la validación de schema. Zod es una capa, no una solución completa. La mitigación real combina schema validation, restricciones en el system prompt y revisión humana en puntos críticos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cline es seguro para usar en producción como orquestador de agentes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cline tiene acceso a filesystem, terminal y otras herramientas con efecto real. Eso no es inherentemente inseguro — es la funcionalidad que lo hace útil. El riesgo (LLM08) está en el diseño: si el agente puede ejecutar comandos destructivos sin confirmación humana, el riesgo es real independientemente de qué tan bien esté configurado Cline. La regla que aplico: cualquier tool con efecto irreversible requiere aprobación explícita.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cada cuánto hay que correr esta auditoría?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cada vez que cambiás la arquitectura del agente: agregás una tool nueva, cambiás el system prompt o modificás cómo el agente consume outputs externos. No es una auditoría de una sola vez — es un checklist que corre contra cada cambio estructural. Si agregás observabilidad (&lt;a href="https://juanchi.dev/es/blog/opentelemetry-nextjs-traces-edge-runtime-contexto" rel="noopener noreferrer"&gt;OpenTelemetry&lt;/a&gt; es una opción), podés detectar anomalías en runtime entre auditorías.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿El OWASP LLM Top 10 cubre riesgos de multi-agente o solo agente único?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;La versión actual (&lt;a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/" rel="noopener noreferrer"&gt;2025&lt;/a&gt;) cubre principalmente el riesgo por agente. En arquitecturas multi-agente, la superficie de LLM01 se multiplica: cada agente puede ser un vector de inyección para los demás. El framework nombra el riesgo, pero el detalle de mitigación para pipelines multi-agente queda en manos de cada equipo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué riesgo debería atacar primero si tengo tiempo limitado?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LLM01 (prompt injection) si tu agente consume output externo — es el más explotable y el más ignorado. LLM08 (excessive agency) si el agente tiene acceso a herramientas con efecto irreversible — es el que más daño puede hacer en un fallo. Los demás dependen de tu stack, pero estos dos son el piso mínimo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusión: la diferencia entre leer y auditar
&lt;/h2&gt;

&lt;p&gt;Mi postura es clara: el OWASP LLM Top 10 no sirve para leerlo y darlo por cubierto. Sirve para llevarlo a una sesión de revisión con el diagrama de arquitectura enfrente y preguntar, por cada riesgo, dónde exactamente en el pipeline eso puede fallar.&lt;/p&gt;

&lt;p&gt;Lo que no compro es la idea de que "seguir las buenas prácticas" alcanza. Las prácticas son abstractas; el pipeline es concreto. En mi caso, LLM01 y LLM06 eran problemas reales que no habría encontrado sin hacer el ejercicio de auditoría sistemática. Los habría descubierto cuando alguien con motivación los explotara.&lt;/p&gt;

&lt;p&gt;Si ya tenés agentes en TypeScript con MCP tools o system prompts elaborados, hacé el ejercicio: abrí el OWASP LLM Top 10, abrí el diagrama de arquitectura y preguntá riesgo por riesgo. El resultado va a ser más interesante que el listado.&lt;/p&gt;

&lt;p&gt;Próximo paso concreto: tomá el checklist de esta tabla, reemplazá los estados con los propios y publicá los hallazgos. La auditoría que no se documenta no existe.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;OWASP LLM AI Security &amp;amp; Governance Checklist: &lt;a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/" rel="noopener noreferrer"&gt;https://owasp.org/www-project-top-10-for-large-language-model-applications/&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/owasp-llm-top-10-agentes-produccion-typescript" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>typescript</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
