<?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: Amit Shukla</title>
    <description>The latest articles on DEV Community by Amit Shukla (@amitshuklabag).</description>
    <link>https://dev.to/amitshuklabag</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%2F4119780%2Fb3b9f2eb-017a-4107-b6fa-83139df41a29.png</url>
      <title>DEV Community: Amit Shukla</title>
      <link>https://dev.to/amitshuklabag</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amitshuklabag"/>
    <language>en</language>
    <item>
      <title>RAG evaluation: how to know if your retrieval is actually good</title>
      <dc:creator>Amit Shukla</dc:creator>
      <pubDate>Tue, 29 Sep 2026 13:15:44 +0000</pubDate>
      <link>https://dev.to/amitshuklabag/rag-evaluation-how-to-know-if-your-retrieval-is-actually-good-enk</link>
      <guid>https://dev.to/amitshuklabag/rag-evaluation-how-to-know-if-your-retrieval-is-actually-good-enk</guid>
      <description>&lt;p&gt;You built a RAG app, asked it five questions in the demo, and every answer sounded right. That is not evaluation, that is vibes. The moment a real user asks something slightly off the beaten path, retrieval quietly returns the wrong chunks, the model answers confidently anyway, and nobody notices until a customer does.&lt;/p&gt;

&lt;p&gt;RAG systems fail silently. A bad retrieval does not throw an error, it just hands the model the wrong context, and the model, doing exactly what it is built to do, writes a fluent, convincing, wrong answer on top of it. Without a real evaluation process, you find out about these failures from users, not from tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "it sounded right" is not evaluation
&lt;/h2&gt;

&lt;p&gt;Reading five outputs and nodding is not measurement, it is spot-checking, and spot-checking only catches the failures that happen to land in your five questions. Retrieval quality is not a single number you eyeball, it is a set of properties that need to be checked against a real, repeatable set of queries with known correct answers.&lt;/p&gt;

&lt;p&gt;The two systems working together, retriever and generator, fail differently and need to be evaluated separately before you evaluate them together. A generator can write a great answer from bad context. A retriever can find the perfect chunk and still get a bad answer if the generator ignores it. Blending both into one end-to-end "did it sound good" check hides which half is actually broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build an eval set before you build more features
&lt;/h2&gt;

&lt;p&gt;The single highest leverage thing you can do is create a small, real set of query, expected-answer, expected-source triples. Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What is the default max_connections value in Postgres?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_chunk_ids"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"postgres-pooling-03"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_answer_contains"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"100"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twenty to fifty of these, pulled from real questions your users actually ask or would ask, beats a thousand synthetic ones nobody will trust. Write them once, run them on every change to chunking, embedding model, or prompt, and you have a real signal instead of a feeling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieval metrics: is the right chunk even in the list
&lt;/h2&gt;

&lt;p&gt;Before touching generation quality, check whether the retriever is finding the right source material at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recall@k&lt;/strong&gt; asks: of the chunks you actually needed, how many showed up in the top k results. If the right chunk is buried at position 15 and you only pass the top 5 to the model, recall@5 is 0, and no amount of prompt engineering fixes that. This is usually the first thing to check when answers go wrong, because a generation problem downstream of a retrieval miss looks identical to a bad prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Precision@k&lt;/strong&gt; asks the opposite: of what you retrieved, how much was actually relevant. High recall with low precision means you are burying the right chunk in a pile of noise, which still hurts the generator, since irrelevant context competes for the model's attention and can get woven into a wrong answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MRR (Mean Reciprocal Rank)&lt;/strong&gt; captures how high up the correct result lands, not just whether it appears. A system that ranks the right chunk first every time scores very differently from one that eventually finds it at position 8, even if raw recall looks similar.&lt;/p&gt;

&lt;p&gt;A simple way to compute recall@k against your eval set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recall_at_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retrieved_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;top_k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retrieved_ids&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected_ids&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;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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;expected_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this across your eval set every time you change chunking strategy, embedding model, or index settings, and you get a real before/after number instead of a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generation metrics: did the model actually use what it was given
&lt;/h2&gt;

&lt;p&gt;Once retrieval is solid, check whether the generator is grounded in what it received, not hallucinating on top of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Faithfulness&lt;/strong&gt; checks whether every claim in the answer is actually supported by the retrieved context. An answer can be well written and completely unsupported by the sources it was supposedly built from, this is the specific failure mode that makes RAG dangerous in production, since it looks exactly like a correct answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Answer relevance&lt;/strong&gt; checks whether the response actually addresses the question asked, independent of whether it's grounded, since a model can retrieve the right chunk and still wander off and answer a related but different question.&lt;/p&gt;

&lt;p&gt;A common, practical pattern is using a second LLM call as a judge, asking it to check faithfulness against the retrieved context directly:&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="n"&gt;judge_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Context: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;retrieved_context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
Answer: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;generated_answer&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

Does the answer contain any claim not supported by the context?
Respond with only: SUPPORTED or UNSUPPORTED
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not perfect, an LLM judge has its own blind spots, but it is far better than nothing and it is cheap enough to run against your whole eval set on every change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where teams actually get this wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Testing only the happy path.&lt;/strong&gt; Your eval set should include queries with no good answer in the corpus, ambiguous questions, and queries phrased nothing like your documents. A retriever that only works when the question echoes the document's wording will fail constantly in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never re-running the eval set.&lt;/strong&gt; Building it once and never running it again defeats the purpose. Every change to chunking, embedding model, or prompt needs to run against the same eval set, otherwise you have no idea if you just made things better or worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimizing retrieval and generation together, blindly.&lt;/strong&gt; If an answer is wrong, check retrieval first. Fixing a prompt to compensate for consistently bad retrieval just hides the real problem and makes it harder to fix later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A RAG app that sounds convincing in a five-question demo is not a RAG app you've evaluated, it's one you've gotten lucky with. Build a real eval set from actual questions, measure retrieval with recall@k and precision@k before you touch the prompt, and check faithfulness on generation once retrieval is solid. The failures are silent by default, the only way to catch them before a user does is to actually measure for them.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>llm</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Structured logging: why print statements do not scale past one service</title>
      <dc:creator>Amit Shukla</dc:creator>
      <pubDate>Thu, 24 Sep 2026 13:15:25 +0000</pubDate>
      <link>https://dev.to/amitshuklabag/structured-logging-why-print-statements-do-not-scale-past-one-service-54la</link>
      <guid>https://dev.to/amitshuklabag/structured-logging-why-print-statements-do-not-scale-past-one-service-54la</guid>
      <description>&lt;p&gt;&lt;code&gt;console.log('user logged in', userId)&lt;/code&gt; is fine when there is one process and one terminal. The moment there are two services, three instances of each, and a log aggregator collecting all of it into one place, that same line becomes a string sitting in a pile of other strings, and the only way to find it again is grep and hope.&lt;/p&gt;

&lt;p&gt;Structured logging is not a bigger word for logging more. It is logging in a shape a machine can filter, group, and query, instead of a shape only a human eyeballing a terminal can read.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "unstructured" actually costs you
&lt;/h2&gt;

&lt;p&gt;A typical unstructured log line looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-03-14 09:12:01 INFO user logged in userId=8842 from 203.0.113.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Readable, sure. But ask it a real question: how many logins came from this IP in the last hour, across all three instances of the auth service. There is no field called &lt;code&gt;ip&lt;/code&gt; to filter on, there is a string that happens to contain an IP address somewhere after the word &lt;code&gt;from&lt;/code&gt;. Every query against logs like this becomes a regex, and every regex is one log format change away from silently missing everything.&lt;/p&gt;

&lt;p&gt;Multiply that by every service you run, each with its own slightly different log phrasing, and log aggregation stops being useful right around the time you actually need it, during an incident, at 2 a.m., under pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What structured logging looks like instead
&lt;/h2&gt;

&lt;p&gt;Same event, structured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-14T09:12:01.402Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth-7f9c2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user logged in"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8842&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"203.0.113.4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9c1d2e3f"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now "how many logins from this IP in the last hour" is a real query: filter &lt;code&gt;service:auth AND ip:203.0.113.4 AND message:"user logged in"&lt;/code&gt;, no regex, no guessing at format. Every field is addressable because every field is actually a field, not a substring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fields that matter most
&lt;/h2&gt;

&lt;p&gt;Not every field needs to be there every time, but a few show up in almost every useful log line:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;timestamp&lt;/code&gt;&lt;/strong&gt;, in a consistent format, ideally ISO 8601 with milliseconds. Without it, ordering events across services during an incident is guesswork.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;level&lt;/code&gt;&lt;/strong&gt;, so &lt;code&gt;error&lt;/code&gt; and &lt;code&gt;warn&lt;/code&gt; can be filtered separately from routine &lt;code&gt;info&lt;/code&gt; noise, and alerting can watch one field instead of parsing text for the word "error".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;service&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;instance&lt;/code&gt;&lt;/strong&gt;, so a log aggregator pulling from twenty running processes can tell you which one actually produced a given line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;requestId&lt;/code&gt;&lt;/strong&gt; (or &lt;code&gt;traceId&lt;/code&gt;), generated once at the edge of a request and passed through every service that touches it. This is the single field that turns a pile of unrelated log lines into a story you can follow end to end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;message&lt;/code&gt;&lt;/strong&gt;, a short human-readable description, kept separate from the structured fields around it rather than having the fields baked into the sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing it in code
&lt;/h2&gt;

&lt;p&gt;A raw &lt;code&gt;console.log&lt;/code&gt; gives you none of this by default. A small logging library fixes that with almost no extra code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// logger.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pino&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pino&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;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pino&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;level&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;LOG_LEVEL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;instance&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;HOSTNAME&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="nx"&gt;pino&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdTimeFunctions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isoTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// wherever a request is handled&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./logger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;child&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user logged in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;logger.child()&lt;/code&gt; is the important part. It creates a logger that automatically stamps every line with the request's &lt;code&gt;requestId&lt;/code&gt;, so nothing downstream in that request has to remember to pass it along by hand. Every log call inside that request already carries the field that ties it back to everything else that happened during it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;requestId&lt;/code&gt; is what makes multi-service logs useful
&lt;/h2&gt;

&lt;p&gt;A single request in a real system usually touches more than one service: an API gateway, an auth service, maybe a database proxy, maybe a queue. Without a shared &lt;code&gt;requestId&lt;/code&gt; generated once and forwarded through every hop (usually as a header like &lt;code&gt;x-request-id&lt;/code&gt;), each service's logs are an isolated island. With it, a single filter across your aggregator, &lt;code&gt;requestId:9c1d2e3f&lt;/code&gt;, reconstructs the entire path a request took, across every service, in order, in one view.&lt;/p&gt;

&lt;p&gt;This is the difference structured logging actually buys you. It is not about making individual log lines prettier, it is about making an incident across five services searchable as one story instead of five separate greps that you have to correlate by hand, by timestamp, while something is on fire.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to avoid
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not log secrets.&lt;/strong&gt; Passwords, tokens, full credit card numbers, anything with compliance implications, should never land in a structured field either, it is just as searchable and just as leaked if the log store is ever exposed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not over-nest.&lt;/strong&gt; A field three levels deep in JSON is harder to query in most log tools than a flat field with a longer name. &lt;code&gt;user.address.city&lt;/code&gt; works, but a flatter &lt;code&gt;userCity&lt;/code&gt; is often easier to filter on depending on your aggregator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not log everything at &lt;code&gt;info&lt;/code&gt;.&lt;/strong&gt; If every line is &lt;code&gt;info&lt;/code&gt;, &lt;code&gt;level&lt;/code&gt; stops being a useful filter. Reserve &lt;code&gt;warn&lt;/code&gt; for things that need attention but did not break anything, and &lt;code&gt;error&lt;/code&gt; for things that did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;console.log&lt;/code&gt; is fine for one process on your own machine. The moment logs need to be searched, filtered, or correlated across more than one running instance, they need to be data, not sentences. A structured logger with a consistent &lt;code&gt;service&lt;/code&gt;, &lt;code&gt;level&lt;/code&gt;, &lt;code&gt;timestamp&lt;/code&gt;, and a &lt;code&gt;requestId&lt;/code&gt; that travels with the request costs a few lines of setup and turns "grep and hope" into an actual query, right when you need that query the most.&lt;/p&gt;

</description>
      <category>logging</category>
      <category>observability</category>
      <category>backend</category>
      <category>devops</category>
    </item>
    <item>
      <title>Postgres connection pooling: why your app runs out of connections under load</title>
      <dc:creator>Amit Shukla</dc:creator>
      <pubDate>Tue, 22 Sep 2026 13:17:00 +0000</pubDate>
      <link>https://dev.to/amitshuklabag/postgres-connection-pooling-why-your-app-runs-out-of-connections-under-load-knn</link>
      <guid>https://dev.to/amitshuklabag/postgres-connection-pooling-why-your-app-runs-out-of-connections-under-load-knn</guid>
      <description>&lt;p&gt;It works fine in staging. A handful of people click around, everything responds, nobody notices anything. Then it goes to production, real traffic shows up, and somewhere in your logs: &lt;code&gt;FATAL: sorry, too many clients already&lt;/code&gt;. Nothing about your query logic changed. What changed is how many connections are open at once, and Postgres has a hard ceiling on that number.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a connection actually costs
&lt;/h2&gt;

&lt;p&gt;In most databases, a connection is cheap, a lightweight thread or an entry in a pool. In Postgres, a connection is a dedicated operating system &lt;strong&gt;process&lt;/strong&gt;. Every client that connects gets its own backend process, with its own memory overhead, typically several megabytes just to exist before it runs a single query.&lt;/p&gt;

&lt;p&gt;That process model is part of why Postgres is so stable, an unstable connection can't take down another one's memory space, but it also means connections are not free, and there's a hard limit on how many can exist at once: &lt;code&gt;max_connections&lt;/code&gt;, which defaults to 100.&lt;/p&gt;

&lt;p&gt;A hundred sounds like a lot until you count how many things are trying to hold one open at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why apps blow through it so fast
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Connection-per-request, no pooling.&lt;/strong&gt; Open a new connection at the start of a request, close it at the end. Under low traffic this is invisible. Under real concurrency, you can have hundreds of requests in flight, each holding its own connection, and you hit the ceiling before any single query was slow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Serverless functions.&lt;/strong&gt; Each cold start can open its own connection, and serverless platforms scale by spinning up more instances under load, exactly when you can least afford more connections. A traffic spike creates a connection spike in the worst possible direction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ORMs that don't clean up.&lt;/strong&gt; A pool that never releases idle connections, or a code path that throws before the &lt;code&gt;finally&lt;/code&gt; that returns the connection, leaks connections one request at a time until the ceiling is gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple app instances, each with their own pool.&lt;/strong&gt; If you run ten instances of your app and each opens a pool of twenty connections, that's two hundred connections against a Postgres server that allows one hundred, before a single one of them is even under load.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern that causes it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// a new connection every request, never reused&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="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="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;res&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&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;Fine at ten requests a second. At a few hundred concurrent requests, you're opening and holding that many real Postgres processes at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix inside your app: an actual pool
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// one shared pool, connections are reused, not recreated&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="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="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&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;A pool holds a fixed number of real connections open and hands them out to whichever request needs one, then takes them back when the query finishes. Requests queue briefly for a free connection instead of each opening a new one. This alone fixes the connection-per-request problem for a single, always-on server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix across many instances: an external pooler
&lt;/h2&gt;

&lt;p&gt;An in-process pool doesn't help once you have many instances, each one still opens its own set of real connections, and they all add up against the same &lt;code&gt;max_connections&lt;/code&gt;. The fix here is a pooler that sits between your app and Postgres: &lt;strong&gt;PgBouncer&lt;/strong&gt; is the standard one.&lt;/p&gt;

&lt;p&gt;PgBouncer accepts thousands of client connections and multiplexes them onto a small, fixed number of real Postgres connections, handing out an actual connection only for the duration of a transaction, then returning it to the pool the moment the transaction ends. A hundred app instances can each think they have their own pool of client connections to PgBouncer, while Postgres itself only ever sees a few dozen real ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sizing it, and the tradeoff nobody mentions
&lt;/h2&gt;

&lt;p&gt;More pool slots is not automatically better. If Postgres allows 100 connections and you reserve some for admin and migrations, your actual budget might be 80. Ten app instances each opening a pool of 20 is 200, already over budget, regardless of how idle most of those connections are most of the time. Pool size has to be planned against &lt;code&gt;max_connections&lt;/code&gt; divided across every instance that connects, not set per instance in isolation.&lt;/p&gt;

&lt;p&gt;The tradeoff with PgBouncer specifically: its fastest mode, &lt;strong&gt;transaction pooling&lt;/strong&gt;, breaks anything that depends on session state persisting across queries; prepared statements, session-level settings, advisory locks, &lt;code&gt;LISTEN&lt;/code&gt;/&lt;code&gt;NOTIFY&lt;/code&gt;. If your app relies on those, you either avoid transaction mode and accept fewer effective connections, or restructure the code that depends on session state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Postgres connections are real processes with a hard, low ceiling, not a cheap resource you can hand out per request. Pool inside your app so you reuse connections instead of recreating them, and once you're running more than one instance, put an external pooler like PgBouncer in front of Postgres so the number of real connections stays fixed no matter how many app instances or serverless functions are asking for one.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>backend</category>
      <category>devops</category>
    </item>
    <item>
      <title>React Server Components vs traditional SSR: what changed and why it matters</title>
      <dc:creator>Amit Shukla</dc:creator>
      <pubDate>Thu, 17 Sep 2026 20:01:03 +0000</pubDate>
      <link>https://dev.to/amitshuklabag/react-server-components-vs-traditional-ssr-what-changed-and-why-it-matters-emn</link>
      <guid>https://dev.to/amitshuklabag/react-server-components-vs-traditional-ssr-what-changed-and-why-it-matters-emn</guid>
      <description>&lt;p&gt;People use "SSR" and "Server Components" like they're the same idea wearing a different name. They're not. Traditional server side rendering and React Server Components solve different problems, and mixing them up leads to either overbuilding a simple page or missing out on a real reduction in what you ship to the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  What traditional SSR actually does
&lt;/h2&gt;

&lt;p&gt;Server side rendering, the kind React has done since &lt;code&gt;renderToString&lt;/code&gt; existed, runs your entire component tree on the server for the first request and sends back real HTML. That's the win: the user sees content immediately instead of a blank page waiting for JavaScript to load and run.&lt;/p&gt;

&lt;p&gt;But the server's job isn't done once that HTML ships. The client still downloads the JavaScript for every component in that tree, then re-runs it in the browser to attach event handlers and make the page interactive. That step is hydration, and it means the same render work happens twice, once on the server to produce HTML, once on the client to make it interactive, and the client has to download and execute the code for all of it, including the parts of the page that never do anything interactive at all.&lt;/p&gt;

&lt;p&gt;A list of blog post previews with no buttons, no state, nothing to click, still ships its full component code to the browser and still gets hydrated, purely because it was part of the same tree as something that actually needed interactivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Server Components actually change
&lt;/h2&gt;

&lt;p&gt;A Server Component runs only on the server. Not "server first, then also client." Only the server. Its code, and its dependencies, never reach the browser at all. What gets sent to the client is a special serialized description of the rendered output, not the component's source, not its imports, none of it.&lt;/p&gt;

&lt;p&gt;Interactivity still exists, you opt into it explicitly with &lt;code&gt;'use client'&lt;/code&gt; at the top of a file. That marks a component as one that does need to run and hydrate in the browser, because it holds state, uses a hook, or responds to events. Everything else, by default, stays server-only.&lt;/p&gt;

&lt;p&gt;This is an actual architectural split, not a performance tweak. You're choosing, per component, whether its code ships to the browser at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before and after
&lt;/h2&gt;

&lt;p&gt;A traditional approach: the whole page, including the static list, is one client-rendered (or hydrated) tree.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// everything here ships to the browser and gets hydrated&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PostList&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;posts&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LikeButton&lt;/span&gt; &lt;span class="na"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;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;With Server Components, the list itself, and the data fetching behind it, stays on the server. Only the interactive part opts in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// PostList.jsx — Server Component, never ships to the client&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&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;PostList&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;posts&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// direct backend access, no API route&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LikeButton&lt;/span&gt; &lt;span class="na"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// LikeButton.jsx — Client Component, this is the only part hydrated&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&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="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LikeButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;postId&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;liked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLiked&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setLiked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;liked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;liked&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Liked&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;Like&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same visual result. The list's rendering code, its data fetching logic, and its dependencies never leave the server. The only JavaScript the browser downloads and hydrates is the tiny like button.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually buys you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Smaller client bundles.&lt;/strong&gt; Not marginally, meaningfully, because entire categories of code, data access layers, markdown parsers, formatting libraries, anything only used to produce the server-rendered output, simply never ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direct backend access.&lt;/strong&gt; A Server Component can query a database or call an internal service directly, no separate API route needed just to get data into the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streaming by default.&lt;/strong&gt; Server Components can stream their output as it becomes ready, so slow data doesn't block fast data from rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this gets misused
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It needs framework support.&lt;/strong&gt; This isn't a flag you flip in any React app, it's a rendering convention that Next.js's App Router (and a small number of other frameworks) actually implements. Reaching for it outside that support gets you nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No hooks, no state, no browser APIs inside a Server Component.&lt;/strong&gt; &lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useEffect&lt;/code&gt;, &lt;code&gt;window&lt;/code&gt;, none of it works there, because the component never runs in a browser. That logic has to live in a Client Component.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequential fetches still create waterfalls.&lt;/strong&gt; Moving data fetching to the server doesn't automatically parallelize it. Awaiting one fetch before starting the next is still slow, on the server or the client, fix it the same way you always would, start the requests together and await them together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Traditional SSR answers "how do we get HTML to the browser fast." Server Components answer a different question: "does this component's code need to reach the browser at all." Most pages have far more content that never needed to be there than interactive pieces that do, and that's exactly the part Server Components let you stop shipping.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Kubernetes readiness and liveness probes: what they actually protect you from</title>
      <dc:creator>Amit Shukla</dc:creator>
      <pubDate>Tue, 15 Sep 2026 13:15:41 +0000</pubDate>
      <link>https://dev.to/amitshuklabag/kubernetes-readiness-and-liveness-probes-what-they-actually-protect-you-from-197a</link>
      <guid>https://dev.to/amitshuklabag/kubernetes-readiness-and-liveness-probes-what-they-actually-protect-you-from-197a</guid>
      <description>&lt;p&gt;A pod can be &lt;code&gt;Running&lt;/code&gt; and still be useless. The container process is alive, the port is open, and every request that hits it times out anyway, because the database connection pool hasn't finished warming up, or a downstream dependency is unreachable, or the app is stuck in a loop it will never recover from on its own.&lt;/p&gt;

&lt;p&gt;Kubernetes has two checks for exactly this, &lt;code&gt;livenessProbe&lt;/code&gt; and &lt;code&gt;readinessProbe&lt;/code&gt;. They sit next to each other in the same YAML block, take the same shape, and do almost nothing alike.&lt;/p&gt;

&lt;h2&gt;
  
  
  What liveness actually does
&lt;/h2&gt;

&lt;p&gt;A liveness probe answers one question: is this process still working, or should it be killed and restarted.&lt;/p&gt;

&lt;p&gt;If the probe fails enough times in a row, Kubernetes kills the container and starts a fresh one. That's the entire mechanism. It exists for the case where your process is technically running but permanently stuck, a deadlock, a hung thread, a memory leak that's wedged the event loop. Restarting is the only fix, and liveness is what triggers it automatically instead of you finding out from an alert three hours later.&lt;/p&gt;

&lt;p&gt;What it does not do: protect you from slow startup, temporary unavailability, or a dependency being down. If your liveness check fails because the database is briefly unreachable, Kubernetes restarts a perfectly healthy process, which does nothing to fix the database and just adds a few seconds of extra downtime while the container comes back up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What readiness actually does
&lt;/h2&gt;

&lt;p&gt;A readiness probe answers a different question: should this pod currently receive traffic.&lt;/p&gt;

&lt;p&gt;If it fails, the pod is pulled out of the Service's list of endpoints. Nothing gets restarted. The container keeps running, Kubernetes just stops routing requests to it until the probe passes again. This is what you want during startup, while dependencies are still initializing, and during temporary trouble, when a downstream service is degraded and you'd rather drain traffic away than serve errors.&lt;/p&gt;

&lt;p&gt;Readiness is safe to make strict. Liveness is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake that causes the most damage
&lt;/h2&gt;

&lt;p&gt;Pointing both probes at the same endpoint, or writing a liveness check that depends on downstream services.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// dangerous as a liveness check&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/healthz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment the database is slow or Redis hiccups, every pod running this as its liveness check starts failing and getting restarted, all at once, across the whole deployment. You just turned a downstream blip into a full outage of your own service, and a restart storm on top of it, since the new containers boot up, immediately fail the same check because the dependency is still down, and get killed again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each check should actually look like
&lt;/h2&gt;

&lt;p&gt;Liveness should only answer "is the process itself alive," nothing more.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// liveness: just prove the event loop is responsive&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/healthz&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Readiness should check the things that actually determine whether this pod can serve a request correctly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// readiness: are the things we depend on actually up&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&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;And the pod spec pointing at each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/healthz&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
  &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

&lt;span class="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/ready&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
  &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tuning that actually matters
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;initialDelaySeconds&lt;/strong&gt; on liveness should be longer than your worst-case startup time. If your app can take 20 seconds to boot under load, and liveness starts checking at 10 seconds with a low failure threshold, you'll get restart loops on your slowest starts, which makes the slow start even slower.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;failureThreshold and periodSeconds together&lt;/strong&gt; define how long a probe has to fail before something happens. Three failures at ten second intervals gives you thirty seconds of grace. Too short and normal jitter triggers unnecessary restarts or traffic pulls. Too long and a genuinely stuck pod keeps eating requests for a while before anyone notices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never let liveness depend on anything outside the process.&lt;/strong&gt; If it can fail because of a network call, a database, another service, it will eventually cause a restart storm during exactly the incident where you can least afford one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Readiness can and should depend on those things.&lt;/strong&gt; That's the entire point of having it separate. Use it to pull a pod out of rotation the moment it can't correctly serve a request, and let it rejoin automatically the moment it can again, with no restart, no lost in-flight work on the other healthy pods.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Liveness restarts a stuck process. Readiness controls whether a pod gets traffic. Keep liveness dumb and self-contained so it only fires when the process itself is actually broken, and keep readiness honest about your real dependencies so Kubernetes can route around trouble instead of amplifying it.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>cloudnative</category>
      <category>containers</category>
    </item>
    <item>
      <title>Self-hosting open source is easy. Running it in production is not.</title>
      <dc:creator>Amit Shukla</dc:creator>
      <pubDate>Thu, 10 Sep 2026 19:24:53 +0000</pubDate>
      <link>https://dev.to/amitshuklabag/self-hosting-open-source-is-easy-running-it-in-production-is-not-i72</link>
      <guid>https://dev.to/amitshuklabag/self-hosting-open-source-is-easy-running-it-in-production-is-not-i72</guid>
      <description>&lt;p&gt;You picked an open source tool on purpose. Maybe it was n8n instead of a per-task automation SaaS, Keycloak instead of paying per active user for auth, Postgres and Grafana and Mattermost instead of three separate subscriptions that each want a seat license. The pitch is good: own your data, no per-seat bill, no vendor deciding your roadmap.&lt;/p&gt;

&lt;p&gt;You run &lt;code&gt;docker compose up&lt;/code&gt;. It works. You show the team. Everyone is happy.&lt;/p&gt;

&lt;p&gt;Then it goes to production, and the actual job starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The work that is not in the README
&lt;/h2&gt;

&lt;p&gt;The README gets you a running process. Keeping that process healthy, safe, and recoverable for the next two years is a different list: sizing and provisioning, OS patching, application updates that don't break your config or schema, TLS that renews itself, backups that are off-host and actually tested, monitoring that warns you before the disk fills, email that gets delivered instead of spam-filtered, a firewall and some answer for volumetric attacks, secret and log rotation, and tracking CVE disclosures for every app you run, forever.&lt;/p&gt;

&lt;p&gt;None of this is hard in isolation. All of it together, across five or six self-hosted apps, is a part time job. And it usually lives in one engineer's head.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two options most teams settle for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do it yourself on a VPS.&lt;/strong&gt; Works right up until the person who set it up is on leave during an incident, or the one undocumented upgrade step gets skipped, or a restore is needed for the first time and nobody has tested one. The software is free. The operational knowledge is expensive and fragile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pay for the vendor's hosted version.&lt;/strong&gt; Faster to start, but often back to per-seat pricing, your data on their infrastructure, and you've given up the control that made you choose open source. For some tools the managed tier costs several times the raw compute.&lt;/p&gt;

&lt;p&gt;There is a third option: a managed layer on top of the open source software you already chose, one that leaves you with full root access and your own data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Elestio actually does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://elest.io/" rel="noopener noreferrer"&gt;Elestio&lt;/a&gt; runs that layer. Pick an app from a catalog of &lt;strong&gt;400+ open source templates&lt;/strong&gt;, pick where it runs, a major cloud provider and region of your choice, your own virtual machines, or your own hardware on premise, and it deploys onto a dedicated instance, not a shared box.&lt;/p&gt;

&lt;p&gt;From there, everything on that operational list is already handled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automated encrypted backups, off host, with retention, and restore that's actually tested&lt;/li&gt;
&lt;li&gt;Monitoring with alerting on the things that actually page you&lt;/li&gt;
&lt;li&gt;Automatic OS patching and application updates, with a rollback path&lt;/li&gt;
&lt;li&gt;Managed SMTP so outbound mail is set up and deliverable&lt;/li&gt;
&lt;li&gt;TLS certificates that issue and renew on their own&lt;/li&gt;
&lt;li&gt;A firewall and DDoS protection in front of the instance&lt;/li&gt;
&lt;li&gt;Built-in CI/CD if you're deploying your own code alongside&lt;/li&gt;
&lt;li&gt;Support that covers the whole stack, not just the VM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You keep full root access. It's your instance and your data, exportable any time. Billing is a flat fee on top of compute, not per user, so growing your team doesn't grow your bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started takes minutes, not a sprint
&lt;/h2&gt;

&lt;p&gt;Pick a template, choose a region, and Elestio provisions the instance with backups, monitoring, updates, TLS, and a firewall already configured before your first request even hits it. No YAML to write first, no runbook to build before you're allowed to go live.&lt;/p&gt;

&lt;p&gt;Most teams have their first app running in under ten minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://elest.io/" rel="noopener noreferrer"&gt;Try Elestio free →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Open source being free to license doesn't make it free to operate. Backups, patching, updates, monitoring, mail, and security tracking are the real cost, and they show up the moment you're in production. Elestio is the fastest way to get all of that handled from day one, so your team spends its time on the product, not the platform underneath it.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Multi-stage Docker builds: ship the artifact, not the build shop</title>
      <dc:creator>Amit Shukla</dc:creator>
      <pubDate>Thu, 10 Sep 2026 19:14:54 +0000</pubDate>
      <link>https://dev.to/amitshuklabag/multi-stage-docker-builds-ship-the-artifact-not-the-build-shop-3o6l</link>
      <guid>https://dev.to/amitshuklabag/multi-stage-docker-builds-ship-the-artifact-not-the-build-shop-3o6l</guid>
      <description>&lt;p&gt;Most Node images I inherit are somewhere north of a gigabyte. The app inside is maybe 40 MB. The other gigabyte is the build shop: compilers, dev dependencies, the npm cache, a full copy of the source, and whatever the base image shipped with.&lt;/p&gt;

&lt;p&gt;That gigabyte costs you every day. Slower pulls on every deploy and every autoscale event. More layers for your scanner to chew through. A bigger surface for a CVE to land on. And a longer gap between "push" and "running in production".&lt;/p&gt;

&lt;p&gt;Multi-stage builds fix this without touching your application code. Here is how they work and how to get the details right.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ends up in a single stage image
&lt;/h2&gt;

&lt;p&gt;When you write a normal Dockerfile, every instruction adds a layer, and every layer stays in the final image. A typical Node build looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final image now contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;node:20&lt;/code&gt;, which is Debian plus the full Node toolchain, around 1.1 GB before you add anything&lt;/li&gt;
&lt;li&gt;every dependency, including the dev ones you only need to compile&lt;/li&gt;
&lt;li&gt;the npm cache left behind by &lt;code&gt;npm ci&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;your entire source tree, not just the compiled output&lt;/li&gt;
&lt;li&gt;any build artifacts you generated on the way&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You run &lt;code&gt;node dist/server.js&lt;/code&gt;. Everything else is dead weight that ships anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The multi-stage pattern
&lt;/h2&gt;

&lt;p&gt;A multi-stage Dockerfile has more than one &lt;code&gt;FROM&lt;/code&gt;. Each &lt;code&gt;FROM&lt;/code&gt; starts a new stage with its own filesystem. You do the messy work in an early stage, then start a clean final stage and copy in only what you need with &lt;code&gt;COPY --from&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things changed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The first stage is named &lt;code&gt;build&lt;/code&gt; with &lt;code&gt;AS build&lt;/code&gt;. It installs everything and compiles. None of its layers reach the final image.&lt;/li&gt;
&lt;li&gt;The second stage starts from &lt;code&gt;node:20-slim&lt;/code&gt;, a much smaller base. It installs production dependencies only, then copies the compiled &lt;code&gt;dist&lt;/code&gt; folder out of the &lt;code&gt;build&lt;/code&gt; stage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The compilers, the dev dependencies, and the source tree never make it into the image you ship. Same build, a fraction of the size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get these four details right
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Name your stages
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;AS &amp;lt;name&amp;gt;&lt;/code&gt; on stages you copy from, then reference them in &lt;code&gt;COPY --from=&amp;lt;name&amp;gt;&lt;/code&gt;. You can also copy from an external image directly, for example &lt;code&gt;COPY --from=nginx:latest /etc/nginx/nginx.conf ./&lt;/code&gt;, which is handy for grabbing a single binary or config file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not copy node_modules across stages
&lt;/h3&gt;

&lt;p&gt;It is tempting to &lt;code&gt;COPY --from=build /app/node_modules ./node_modules&lt;/code&gt; and skip a second install. Do not. The build stage has dev dependencies mixed in, and native modules may have compiled against a different base image. Run &lt;code&gt;npm ci --omit=dev&lt;/code&gt; in the final stage so you get a clean production tree that matches the runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Order instructions so the cache survives
&lt;/h3&gt;

&lt;p&gt;Docker caches each layer and reuses it until something upstream changes. Copy your lockfiles and install before you copy the rest of the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a change to your application code invalidates only the &lt;code&gt;COPY . .&lt;/code&gt; layer and everything after it. The dependency install, which is the slow part, stays cached. Reverse those lines and every one line code change reinstalls everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Go smaller on the final base
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;node:20-slim&lt;/code&gt; drops most of the Debian extras. For a bigger cut, &lt;code&gt;gcr.io/distroless/nodejs20&lt;/code&gt; ships Node and nothing else, no shell, no package manager. It is stricter to debug but the attack surface is tiny. Whatever you pick, add a non-root user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; node&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The same feature, other uses
&lt;/h2&gt;

&lt;p&gt;Multi-stage is not only about size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One Dockerfile, several targets.&lt;/strong&gt; Add a &lt;code&gt;test&lt;/code&gt; stage and build just that stage in CI with &lt;code&gt;docker build --target test .&lt;/code&gt;. Your test image and your production image come from the same file and the same base layers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="c"&gt;# ... install and compile ...&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run lint &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;
&lt;span class="c"&gt;# ... copy artifact ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pulling a tool without installing it.&lt;/strong&gt; Need &lt;code&gt;dockerize&lt;/code&gt; or a specific CLI at runtime? Copy the single binary from its published image in a stage instead of running a package manager in your final image.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in numbers
&lt;/h2&gt;

&lt;p&gt;On a plain Express and TypeScript service:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single stage on &lt;code&gt;node:20&lt;/code&gt;: about 1.1 GB&lt;/li&gt;
&lt;li&gt;Multi-stage with &lt;code&gt;node:20-slim&lt;/code&gt; and &lt;code&gt;--omit=dev&lt;/code&gt;: about 180 MB&lt;/li&gt;
&lt;li&gt;Multi-stage with distroless: about 130 MB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pull time on a cold node drops from tens of seconds to a few. Your scanner has less to report. And nothing in the image can compile code or run a shell that does not need to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Build with the full toolchain in an early stage. Start the final stage from the smallest base you can debug, install production dependencies only, and copy in just the artifact. Name your stages, keep your install layer above your source copy, and run as a non-root user. It is a ten line change to your Dockerfile and it pays off on every deploy.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>node</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
