<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>devops on Saurabh Kumar</title><link>https://saurabh-kumar.com/tags/devops/</link><description>Recent content in devops on Saurabh Kumar</description><generator>Hugo</generator><language>en-US</language><copyright>Copyright © 2025, Saurabh Kumar.</copyright><lastBuildDate>Fri, 24 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://saurabh-kumar.com/tags/devops/index.xml" rel="self" type="application/rss+xml"/><item><title>Three Things That Bite You When You Ship gRPC to Production</title><link>https://saurabh-kumar.com/articles/2026/07/three-things-that-bite-you-when-you-ship-grpc-to-production/</link><pubDate>Fri, 24 Jul 2026 00:00:00 +0000</pubDate><guid>https://saurabh-kumar.com/articles/2026/07/three-things-that-bite-you-when-you-ship-grpc-to-production/</guid><description>&lt;p&gt;I recently spent some time going deeper into gRPC, past the quickstart and into HTTP/2 framing, Protobuf encoding, and the parts of the system that sit around the service itself. I came away liking it. Contract-first APIs are useful, generated clients remove an entire class of mistakes, and streaming is part of the protocol rather than something attached later.&lt;/p&gt;&#10;&lt;p&gt;I also came away with a list of things I wish the quickstart had made harder to miss. They are not obscure bugs, and they are not arguments against gRPC. They are direct consequences of the choices that make it good.&lt;/p&gt;&#10;&lt;h2 id="connections-are-not-requests"&gt;Connections Are Not Requests&lt;/h2&gt;&#10;&lt;p&gt;For a long time I carried a simple model of load balancing: a request arrives, the load balancer picks a backend, and the backend handles it. That model is often close enough for HTTP/1.1 systems, even though the machinery underneath is balancing TCP connections rather than individual requests.&lt;/p&gt;&#10;&lt;p&gt;gRPC makes the difference visible. It usually keeps an HTTP/2 connection open and carries many concurrent RPCs over that connection as streams. Reusing the connection avoids handshakes and lets several calls make progress at once. This is one of the reasons gRPC performs well.&lt;/p&gt;&#10;&lt;p&gt;An L4 load balancer cannot see those streams. It sees one TCP connection, chooses one backend when that connection opens, and keeps sending the connection there for its lifetime. If a client resolves a single virtual IP and holds one long-lived connection to it, hundreds or thousands of calls can remain pinned to one backend while other instances have very little to do.&lt;/p&gt;&#10;&lt;p&gt;The metrics look absurd: one pod is hot, several pods are quiet, and adding more pods changes almost nothing. The cluster has capacity, but the client has no path to it.&lt;/p&gt;&#10;&lt;p&gt;It is like assigning a delivery truck to a warehouse when it leaves the depot, then insisting that every package on that truck go to the same warehouse. Adding warehouses does not help. The decision was already made at the wrong level.&lt;/p&gt;&#10;&lt;p&gt;The fix is to balance one level higher. A gRPC client can resolve individual backends, keep subchannels to them, and choose one for each RPC with a policy such as &lt;code&gt;round_robin&lt;/code&gt;. In Kubernetes that often means a headless service rather than a normal ClusterIP service. An HTTP/2-aware proxy such as Envoy, or a service mesh built around one, can also distribute streams across backend connections.&lt;/p&gt;&#10;&lt;p&gt;Both approaches work, and both add something I now have to operate: endpoint discovery and health checking in the client, or another proxy in the request path. That may be a good trade. It is still a trade.&lt;/p&gt;&#10;&lt;p&gt;A load balancer that understands connections does not necessarily understand gRPC.&lt;/p&gt;&#10;&lt;h2 id="browser-http2-is-not-grpc"&gt;Browser HTTP/2 Is Not gRPC&lt;/h2&gt;&#10;&lt;p&gt;I made what seemed like an obvious inference: gRPC uses HTTP/2, browsers support HTTP/2, therefore browser JavaScript can call a gRPC service.&lt;/p&gt;&#10;&lt;p&gt;It cannot, at least not as native gRPC through the browser APIs available to application code.&lt;/p&gt;&#10;&lt;p&gt;The browser may speak HTTP/2 on the network, but JavaScript does not control that connection as an HTTP/2 peer. gRPC places its final status and error details in trailing headers after the response body, and &lt;code&gt;fetch&lt;/code&gt; and &lt;code&gt;XMLHttpRequest&lt;/code&gt; do not give application code the trailer behavior native gRPC expects. Client streaming and bidirectional streaming also need the request and response to remain active together, with the client writing while it reads. Browser request APIs have historically not offered that form of full-duplex transport in a portable way.&lt;/p&gt;&#10;&lt;p&gt;The browser owns the connection. My code gets a window into it, not the keys.&lt;/p&gt;&#10;&lt;p&gt;grpc-web exists to bridge that boundary. It adapts the protocol so trailers can travel inside the response body as a marked frame that browser code can read. A proxy such as Envoy commonly translates between grpc-web on the browser side and native gRPC on the service side, although some servers support grpc-web directly. Unary calls work, and server streaming can work, but client and bidirectional streaming remain constrained by the browser transport.&lt;/p&gt;&#10;&lt;p&gt;Connect takes a different approach. A Connect server can expose browser-friendly HTTP alongside gRPC and grpc-web, with JSON or Protobuf payloads, and the browser can call it with ordinary &lt;code&gt;fetch&lt;/code&gt;. That removes a dedicated translation proxy in many deployments and makes a service much easier to inspect with tools such as &lt;code&gt;curl&lt;/code&gt;. It does not grant JavaScript arbitrary control over the browser&amp;rsquo;s HTTP/2 connection; available streaming modes still depend on the browser transport.&lt;/p&gt;&#10;&lt;p&gt;I do not consider this a flaw in gRPC. It was designed primarily for environments where both ends are programs with control over their network stack, and server-to-server RPC is where its tradeoffs make the most sense. A browser is someone else&amp;rsquo;s runtime, with someone else&amp;rsquo;s security model and someone else&amp;rsquo;s API surface.&lt;/p&gt;&#10;&lt;p&gt;The mistake is not that gRPC has a boundary. The mistake is drawing an architecture diagram that pretends the boundary is not there.&lt;/p&gt;&#10;&lt;h2 id="field-numbers-never-forget"&gt;Field Numbers Never Forget&lt;/h2&gt;&#10;&lt;p&gt;Protobuf field names are for people. Field numbers are for the wire.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-protobuf" data-lang="protobuf"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The encoded message does not contain the words &lt;code&gt;name&lt;/code&gt; or &lt;code&gt;email&lt;/code&gt;. It contains field numbers, wire types, and values. Renaming &lt;code&gt;name&lt;/code&gt; to &lt;code&gt;full_name&lt;/code&gt; while keeping field number 1 does not change the wire contract. Changing the number does.&lt;/p&gt;&#10;&lt;p&gt;This is easy to understand while looking at one version of a &lt;code&gt;.proto&lt;/code&gt; file. It becomes harder after field 2 was deleted two years ago, the person who deleted it has moved to another team, and the current file makes number 2 look pleasantly unused.&lt;/p&gt;&#10;&lt;p&gt;It is not unused. It is retired.&lt;/p&gt;&#10;&lt;p&gt;Suppose a later version reuses field 2 for another length-delimited value:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-protobuf" data-lang="protobuf"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;bytes&lt;/span&gt; &lt;span class="n"&gt;avatar_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// reused field number&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;An old client can still send an email address in field 2. The new server sees the same field number and a compatible wire type, and accepts those bytes as &lt;code&gt;avatar_hash&lt;/code&gt;. Protobuf cannot object because the old meaning is not present on the wire. Unless validation catches it, perfectly valid bytes can acquire the wrong meaning and travel further into the system.&lt;/p&gt;&#10;&lt;p&gt;That is worse than a failed decode.&lt;/p&gt;&#10;&lt;p&gt;The fix is a tombstone:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-protobuf" data-lang="protobuf"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;reserved&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;reserved&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;email&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;bytes&lt;/span&gt; &lt;span class="n"&gt;avatar_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;reserved&lt;/code&gt; tells the compiler that the number and name may not be used again. It turns historical knowledge into a rule the tooling can enforce. Without it, the only thing protecting the schema is memory, and organizational memory is not a compatibility mechanism.&lt;/p&gt;&#10;&lt;p&gt;The rest of the discipline follows from the same fact. I do not renumber fields. I reserve every removed number and name. I prefer additive changes while old and new binaries must coexist, and I introduce a new message or RPC when the meaning genuinely needs to break. Proto3&amp;rsquo;s removal of &lt;code&gt;required&lt;/code&gt; fields came from the same hard-earned lesson: one service should not make every older peer invalid merely by evolving its schema.&lt;/p&gt;&#10;&lt;p&gt;A field number is small, but it is permanent.&lt;/p&gt;&#10;&lt;h2 id="the-abstraction-ends-somewhere"&gt;The Abstraction Ends Somewhere&lt;/h2&gt;&#10;&lt;p&gt;These three problems looked unrelated to me at first. One was about Kubernetes, one about browsers, and one about schema files. The common thread is that gRPC&amp;rsquo;s guarantees stop at the layer that understands them.&lt;/p&gt;&#10;&lt;p&gt;A TCP load balancer does not understand HTTP/2 streams. Browser JavaScript does not control the browser&amp;rsquo;s HTTP/2 implementation. Protobuf understands field numbers and wire types, but it does not understand that some bytes used to mean an email address and now mean an avatar hash.&lt;/p&gt;&#10;&lt;p&gt;Put those boundaries together and gRPC becomes less forgiving than its clean interface suggests. The generated method call may look like a local function, but it still crosses a network, a load balancer, a runtime, and a schema history. Each layer has constraints the service design needs to account for.&lt;/p&gt;&#10;&lt;p&gt;I still like gRPC, especially for internal communication across services written in different languages. The contract is real, the tooling is mature, and native streaming is valuable. I do not want to give those things up merely because the surrounding system needs care.&lt;/p&gt;&#10;&lt;p&gt;I need to account for those boundaries when choosing clients, load balancers, and schema changes.&lt;/p&gt;&#10;</description></item><item><title>Lobste.rs moved to SQLite</title><link>https://saurabh-kumar.com/til/1784376000/</link><pubDate>Sat, 18 Jul 2026 12:00:00 +0000</pubDate><guid>https://saurabh-kumar.com/til/1784376000/</guid><description>&lt;p&gt;&lt;a href="https://lobste.rs/s/ko1ji1" rel="noopener external"&gt;Lobste.rs moved to SQLite&lt;/a&gt;, and the &lt;a href="https://news.ycombinator.com/item?id=48899847" rel="noopener external"&gt;HN thread&lt;/a&gt; had sharper lessons than the post itself:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;WAL checkpoints only run during &amp;ldquo;reader gaps.&amp;rdquo; Keep reads/writes continuously overlapping and the WAL file grows unbounded, a DoS vector kgeist reproduced with short-lived reads/writes, not just long-lived readers as the docs imply.&lt;/li&gt;&#10;&lt;li&gt;Writes are fully serialized, one fsync each, yet app-layer batching still gets 100k+ writes/sec.&lt;/li&gt;&#10;&lt;li&gt;Plain rsync on a live .db file can silently corrupt it since it doesn&amp;rsquo;t understand SQLite transactions. Use &lt;a href="https://sqlite.org/rsync.html" rel="noopener external"&gt;sqlite3_rsync&lt;/a&gt; instead.&lt;/li&gt;&#10;&lt;li&gt;The real weak point is schema migrations: treat SQLite like a KV store, or reach for CQRS, to keep the churn low.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>SecretSpec</title><link>https://saurabh-kumar.com/til/1784116800/</link><pubDate>Wed, 15 Jul 2026 12:00:00 +0000</pubDate><guid>https://saurabh-kumar.com/til/1784116800/</guid><description>&lt;p&gt;&lt;a href="https://secretspec.dev/" rel="noopener external"&gt;SecretSpec&lt;/a&gt; declares secrets once in &lt;code&gt;secretspec.toml&lt;/code&gt; (names + descriptions, never values) and resolves them from any of 11 backends: keyring, 1Password, Vault, AWS, GCP, .env. Same command everywhere: &lt;code&gt;secretspec run --profile production -- npm start&lt;/code&gt;. Per-secret fallback chains (&lt;code&gt;providers = [&amp;quot;vault&amp;quot;, &amp;quot;keyring&amp;quot;, &amp;quot;env&amp;quot;]&lt;/code&gt;) and a &lt;code&gt;ref = { item, field }&lt;/code&gt; form points at a secret you already have elsewhere, no renaming. Sharpest bit: agents get held to a stricter bar by default. &lt;code&gt;require_reason = &amp;quot;agents&amp;quot;&lt;/code&gt; forces &lt;code&gt;secretspec run --reason &amp;quot;...&amp;quot;&lt;/code&gt; before an AI agent can read a secret, appended to a local audit log; humans running interactively are unaffected. Rust core, SDKs for Python/Go/Node/Ruby/PHP/Haskell. Worth piloting on a project drowning in &lt;code&gt;.env&lt;/code&gt; sprawl.&lt;/p&gt;&#10;</description></item><item><title>Dynamic workflows</title><link>https://saurabh-kumar.com/til/1780056000/</link><pubDate>Fri, 29 May 2026 12:00:00 +0000</pubDate><guid>https://saurabh-kumar.com/til/1780056000/</guid><description>&lt;p&gt;&lt;a href="https://claude.com/blog/introducing-dynamic-workflows-in-claude-code" rel="noopener external"&gt;Dynamic workflows&lt;/a&gt; in Claude Code. Claude dynamically writes orchestration scripts that spawn tens to hundreds of parallel subagents. It plans, decomposes into subtasks, fans out agents, and checks results before returning a coordinated answer. Adversarial agents try to refute findings, iterating until convergence. Enable via &lt;code&gt;ultracode&lt;/code&gt; setting or ask Claude to &amp;ldquo;create a workflow&amp;rdquo;. Notable: Jarred Sumner used it to port Bun from Zig to Rust (~750K lines, 11 days). Available on Max, Team, Enterprise plans. Consumes significantly more tokens than typical sessions.&lt;/p&gt;&#10;</description></item><item><title>mise</title><link>https://saurabh-kumar.com/til/1775304000/</link><pubDate>Sat, 04 Apr 2026 12:00:00 +0000</pubDate><guid>https://saurabh-kumar.com/til/1775304000/</guid><description>&lt;p&gt;&lt;a href="https://github.com/jdx/mise" rel="noopener external"&gt;mise&lt;/a&gt; let&amp;rsquo;s you manage dependencies at OS level and project level. Thinking to manage my dev machine. The hard part is documenting what I have installed.&lt;/p&gt;&#10;</description></item><item><title>Hono</title><link>https://saurabh-kumar.com/til/1770724800/</link><pubDate>Tue, 10 Feb 2026 12:00:00 +0000</pubDate><guid>https://saurabh-kumar.com/til/1770724800/</guid><description>&lt;p&gt;&lt;a href="https://hono.dev/" rel="noopener external"&gt;Hono&lt;/a&gt; based on web standards is a great option for js runtimes esp. if you plan to deploy your code to cloudflare workers, bun or deno. You get faster startup time and cross-platform compatibility.&lt;/p&gt;&#10;</description></item><item><title>Free-threaded Python in uv and GitHub Actions</title><link>https://saurabh-kumar.com/til/1768219200/</link><pubDate>Mon, 12 Jan 2026 12:00:00 +0000</pubDate><guid>https://saurabh-kumar.com/til/1768219200/</guid><description>&lt;p&gt;The free-threaded version of python can be added to uv and github actions with &lt;code&gt;t&lt;/code&gt; suffix. e.g. &lt;code&gt;3.14t&lt;/code&gt; and &lt;code&gt;3.13t&lt;/code&gt; &lt;a href="https://github.com/actions/setup-python?tab=readme-ov-file#basic-usage" rel="noopener external"&gt;source&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>New tools</title><link>https://saurabh-kumar.com/til/1766232000/</link><pubDate>Sat, 20 Dec 2025 12:00:00 +0000</pubDate><guid>https://saurabh-kumar.com/til/1766232000/</guid><description>&lt;p&gt;New tools:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/gjtorikian/gh-actions-lockfile" rel="noopener external"&gt;gh-actions-lockfile&lt;/a&gt; to generate and verify lockfiles for gh-actions&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://proxyman.com/" rel="noopener external"&gt;Proxyman&lt;/a&gt; is more user-friendly http debugging proxy than Charles Proxy on MacOS&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>actions/checkout</title><link>https://saurabh-kumar.com/til/1733832000/</link><pubDate>Tue, 10 Dec 2024 12:00:00 +0000</pubDate><guid>https://saurabh-kumar.com/til/1733832000/</guid><description>&lt;p&gt;&lt;a href="https://github.com/actions/checkout" rel="noopener external"&gt;&lt;code&gt;actions/checkout&lt;/code&gt;&lt;/a&gt; saves the workflow token (&lt;code&gt;GITHUB_TOKEN&lt;/code&gt; or a custom one) in the checked-out repo&amp;rsquo;s &lt;code&gt;.git/config&lt;/code&gt; by default. Workflows that upload the repo as an artifact leak it, and &lt;a href="https://unit42.paloaltonetworks.com/github-repo-artifacts-leak-tokens/" rel="noopener external"&gt;Unit 42 found many projects doing this&lt;/a&gt;. GitHub calls it intended behaviour. Turn it off, and scan workflows with &lt;a href="https://github.com/woodruffw/zizmor" rel="noopener external"&gt;zizmor&lt;/a&gt;, which &lt;a href="https://blog.yossarian.net/2024/12/06/zizmor-ultralytics-injection" rel="noopener external"&gt;would have caught the Ultralytics injection&lt;/a&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v4&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;persist-credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Efficiently Managing Mass URL Redirects with Nginx</title><link>https://saurabh-kumar.com/articles/2017/09/efficiently-managing-mass-url-redirects-with-nginx/</link><pubDate>Mon, 25 Sep 2017 00:00:00 +0000</pubDate><guid>https://saurabh-kumar.com/articles/2017/09/efficiently-managing-mass-url-redirects-with-nginx/</guid><description>&lt;p&gt;I use Nginx&amp;rsquo;s &lt;code&gt;map&lt;/code&gt; directive to keep URL redirects in a separate configuration file. It creates hash tables for fast lookups.&lt;/p&gt;&#10;&lt;h2 id="creating-your-redirect-configuration"&gt;Creating Your Redirect Configuration&lt;/h2&gt;&#10;&lt;p&gt;First, create a file called &lt;code&gt;redirects-map.conf&lt;/code&gt; where each line represents a single redirect rule in this format:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;old_location&amp;gt; &amp;lt;new_url&amp;gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can use two types of location specifications:&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Simple path redirects&lt;/strong&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/contact-success/ /contact;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Regex-based redirects&lt;/strong&gt; (prefixed with &lt;code&gt;~&lt;/code&gt;):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;~^/showcase/(.*)?$ /projects/;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here&amp;rsquo;s an example of a &lt;code&gt;redirects-map.conf&lt;/code&gt; file:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /etc/nginx/redirects-map.conf&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Simple redirects&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;/contact-success/&lt;/span&gt; &lt;span class="s"&gt;/contact&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;/old-page/&lt;/span&gt; &lt;span class="s"&gt;/new-page&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;/outdated-content/&lt;/span&gt; &lt;span class="s"&gt;https://newdomain.com/content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Pattern-based redirects&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;~^/showcase/(.*)?$&lt;/span&gt; &lt;span class="s"&gt;/projects/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;~^/articles/(\d+)/(.*)$&lt;/span&gt; &lt;span class="s"&gt;/blog/&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="configuring-nginx"&gt;Configuring Nginx&lt;/h2&gt;&#10;&lt;h3 id="step-1-upload-the-configuration"&gt;Step 1: Upload the configuration&lt;/h3&gt;&#10;&lt;p&gt;Upload your &lt;code&gt;redirects-map.conf&lt;/code&gt; to your server at &lt;code&gt;/etc/nginx/&lt;/code&gt;&lt;/p&gt;&#10;&lt;h3 id="step-2-set-up-the-map-directive"&gt;Step 2: Set up the map directive&lt;/h3&gt;&#10;&lt;p&gt;Add the following at the top of your nginx virtualhost file (outside any server blocks):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;map&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$redirected_url&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;default&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;none&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/redirects-map.conf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="step-3-add-the-rewrite-rule"&gt;Step 3: Add the rewrite rule&lt;/h3&gt;&#10;&lt;p&gt;Inside your &lt;code&gt;server&lt;/code&gt; block, add this redirection rule:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Handle redirects defined in the map&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$redirected_url&lt;/span&gt; &lt;span class="s"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;none&amp;#34;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;rewrite&lt;/span&gt; &lt;span class="s"&gt;^&lt;/span&gt; &lt;span class="nv"&gt;$redirected_url&lt;/span&gt; &lt;span class="s"&gt;permanent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Your complete virtualhost configuration should look something like this:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;map&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$redirected_url&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;default&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;none&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/redirects-map.conf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;example.org&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Handle redirects defined in the map&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$redirected_url&lt;/span&gt; &lt;span class="s"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;none&amp;#34;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;rewrite&lt;/span&gt; &lt;span class="s"&gt;^&lt;/span&gt; &lt;span class="nv"&gt;$redirected_url&lt;/span&gt; &lt;span class="s"&gt;permanent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Your other server configuration directives&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="step-4-testing"&gt;Step 4: Testing&lt;/h3&gt;&#10;&lt;p&gt;After saving your changes, test your Nginx configuration:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nginx -t&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If successful, reload Nginx:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nginx -s reload&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="handling-large-redirect-sets"&gt;Handling Large Redirect Sets&lt;/h2&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Nginx has memory limitations for map files, controlled by the &lt;code&gt;map_hash_bucket_size&lt;/code&gt; and &lt;code&gt;map_hash_max_size&lt;/code&gt; directives. If you encounter an error like &lt;code&gt;[emerg]: could not build the map_hash&lt;/code&gt;, you&amp;rsquo;ll need to increase these values.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Add these directives to the &lt;code&gt;http&lt;/code&gt; block in your &lt;code&gt;nginx.conf&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Increase these values for large redirect sets&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;map_hash_bucket_size&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;# Default is 32/64 depending on architecture&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;map_hash_max_size&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;# Default is 2048&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Rest of your http configuration&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For very large redirect sets (e.g., 30KB file), you might need values like:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;map_hash_bucket_size&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;map_hash_max_size&lt;/span&gt; &lt;span class="mi"&gt;30720&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="performance-considerations"&gt;Performance Considerations&lt;/h2&gt;&#10;&lt;p&gt;The map module is highly efficient because:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;It uses a hash table for O(1) lookups regardless of how many redirects you have&lt;/li&gt;&#10;&lt;li&gt;It loads the entire redirect configuration into memory once during startup&lt;/li&gt;&#10;&lt;li&gt;It avoids the performance penalties of using multiple &lt;code&gt;if&lt;/code&gt; or &lt;code&gt;location&lt;/code&gt; blocks&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;This approach is ideal for sites migrating to new URL structures or consolidating content from multiple domains.&lt;/p&gt;&#10;</description></item></channel></rss>