<?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: Abdul Wahab</title>
    <description>The latest articles on DEV Community by Abdul Wahab (@awahaab).</description>
    <link>https://dev.to/awahaab</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%2F4192647%2Fc99d7d81-48c7-4eb5-936c-d72cd781847a.png</url>
      <title>DEV Community: Abdul Wahab</title>
      <link>https://dev.to/awahaab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/awahaab"/>
    <language>en</language>
    <item>
      <title>gRPC: how a local call becomes a network request</title>
      <dc:creator>Abdul Wahab</dc:creator>
      <pubDate>Tue, 03 Nov 2026 20:42:31 +0000</pubDate>
      <link>https://dev.to/awahaab/grpc-how-a-local-call-becomes-a-network-request-l20</link>
      <guid>https://dev.to/awahaab/grpc-how-a-local-call-becomes-a-network-request-l20</guid>
      <description>&lt;p&gt;Suppose you run a ride-hailing app with five services: trip, dispatch, location, pricing, and payments.&lt;/p&gt;

&lt;p&gt;The trip service owns each ride. Dispatch matches riders to nearby drivers. Location tracks driver positions.&lt;/p&gt;

&lt;p&gt;Pricing quotes the fare. Payments charges the card.&lt;/p&gt;

&lt;p&gt;When a rider taps Find a ride, the trip service calls dispatch to find a driver. The first version of that call is hand-written JSON. Build a URL, encode JSON, set a content type, parse the response, and repeat that code in every service that calls dispatch.&lt;/p&gt;

&lt;p&gt;Then the dispatch service renames driver_id to driver_ref. The trip service still compiles. At runtime the field is missing, dispatch checks the field, rejects the request, and the ride fails.&lt;/p&gt;

&lt;p&gt;gRPC starts from the contract instead of the HTTP call. You define the service and its messages in one file. From that file, a compiler generates the client and server code, and the generated client makes the call look like a local method.&lt;/p&gt;

&lt;p&gt;The gRPC introduction describes this contract-first workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  A call that crosses the network
&lt;/h2&gt;

&lt;p&gt;gRPC is a remote procedure call system. The client invokes a method on a stub, a generated local object that stands in for the remote service. The stub serializes the request, sends it to the server, waits for the reply, and returns the result to the caller.&lt;/p&gt;

&lt;p&gt;The method name and the message types come from a service definition. The server implements that definition. The client never hand-writes the URL or the JSON body.&lt;/p&gt;

&lt;p&gt;Suppose the trip service calls &lt;code&gt;FindDriver&lt;/code&gt; with &lt;code&gt;pickup_zone = "airport"&lt;/code&gt;. The call site reads like a function call.&lt;/p&gt;

&lt;p&gt;The network is still between the two processes. The stub hides how the message is encoded and sent. It does not hide the latency, the failure, or the version of the contract on the other side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The .proto file is the contract
&lt;/h2&gt;

&lt;p&gt;A gRPC service lives in a &lt;code&gt;.proto&lt;/code&gt; file. The file defines messages, which are typed records, and a service, which lists the methods that can be called.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;&lt;span class="kd"&gt;service&lt;/span&gt; &lt;span class="n"&gt;Dispatch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;FindDriver&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RideRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DriverReply&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;WatchDrivers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AreaRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="n"&gt;DriverLocation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;ReportRoute&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="n"&gt;RoutePoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RouteSummary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;TrackTrip&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="n"&gt;TripEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="n"&gt;TripUpdate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;RideRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;pickup_zone&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;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;rider_id&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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;DriverReply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;driver_id&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;
  &lt;span class="kt"&gt;int32&lt;/span&gt; &lt;span class="na"&gt;eta_seconds&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each message field has a name and a number. The number is the field's identity on the wire, and the binary encoding uses the number, not the name.&lt;/p&gt;

&lt;p&gt;Changing the name can break generated source code or a JSON client, but it does not change the field number. Reusing a number for a different field is not safe, which is why deleted numbers should be reserved (&lt;a href="https://protobuf.dev/overview/" rel="noopener noreferrer"&gt;protobuf overview&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The service block is the contract the compiler consumes. It says &lt;code&gt;FindDriver&lt;/code&gt; takes a &lt;code&gt;RideRequest&lt;/code&gt; and returns a &lt;code&gt;DriverReply&lt;/code&gt;, and the other three methods stream. It does not say which programming language either side uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The compiler writes the plumbing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;protoc&lt;/code&gt; reads the &lt;code&gt;.proto&lt;/code&gt; file. A gRPC plugin adds the client stub and server skeleton to the normal message classes.&lt;/p&gt;

&lt;p&gt;The client gets a stub with a &lt;code&gt;FindDriver&lt;/code&gt; method. The server gets an interface it must implement. The generated code serializes the request, dispatches the method, and makes the transport call.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3s3fojq4fig4m2d3mh2a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3s3fojq4fig4m2d3mh2a.png" alt="A .proto file generates a client stub and a server skeleton." width="799" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A .proto file generates a client stub and a server skeleton.&lt;/p&gt;

&lt;p&gt;This is a build step, not a runtime library you can skip. Change the &lt;code&gt;.proto&lt;/code&gt; file and you regenerate both sides. That keeps the contract honest, and it adds a toolchain every contributor must run.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the stub sends
&lt;/h2&gt;

&lt;p&gt;Suppose the client calls &lt;code&gt;FindDriver&lt;/code&gt; with &lt;code&gt;pickup_zone = "airport"&lt;/code&gt;. The stub produces a request message and encodes it with protobuf, a schema-based binary format. It opens or reuses an HTTP/2 stream, a multiplexed connection that carries several calls at once.&lt;/p&gt;

&lt;p&gt;The request carries the method path, the encoded message, and optional metadata, key-value headers attached to the call, such as an authorization token. HTTP/2 multiplexes several calls over one connection. The client does not need a new TCP connection for every request (&lt;a href="https://www.rfc-editor.org/rfc/rfc9113.html" rel="noopener noreferrer"&gt;RFC 9113&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The server decodes the message, dispatches to &lt;code&gt;FindDriver&lt;/code&gt;, and sends back an encoded reply with a gRPC status. A non-OK status is not an HTTP 500. It is a gRPC status such as &lt;code&gt;NOT_FOUND&lt;/code&gt; or &lt;code&gt;DEADLINE_EXCEEDED&lt;/code&gt;. The status travels in the response trailers, the headers sent after the response body (&lt;a href="https://grpc.io/docs/guides/status-codes/" rel="noopener noreferrer"&gt;gRPC status codes&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The client can set a deadline. When the deadline passes, the call ends with &lt;code&gt;DEADLINE_EXCEEDED&lt;/code&gt;, even if the server is still working. The server can query the remaining time. It can stop work that no client is waiting for (&lt;a href="https://grpc.io/docs/what-is-grpc/core-concepts/" rel="noopener noreferrer"&gt;gRPC core concepts&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ways to stream
&lt;/h2&gt;

&lt;p&gt;A unary call sends one request and gets one reply. That is the shape most people mean by RPC.&lt;/p&gt;

&lt;p&gt;gRPC also defines three streaming shapes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server streaming: one request, then a stream of replies. &lt;code&gt;WatchDrivers&lt;/code&gt; uses this shape to send live driver locations.&lt;/li&gt;
&lt;li&gt;Client streaming: a stream of requests, then one reply. &lt;code&gt;ReportRoute&lt;/code&gt; uses this shape to send GPS points and receive one route summary.&lt;/li&gt;
&lt;li&gt;Bidirectional streaming: both sides send independent streams. &lt;code&gt;TrackTrip&lt;/code&gt; uses this shape to exchange trip events and status updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shapes matter because the method signature carries the direction. &lt;code&gt;returns (stream ItemReply)&lt;/code&gt; is a server stream. &lt;code&gt;(stream ItemRequest)&lt;/code&gt; is a client stream. Both sides are visible in the &lt;code&gt;.proto&lt;/code&gt; file before any code is generated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa8zwbw6vp2z93ozi74pv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa8zwbw6vp2z93ozi74pv.png" alt="The four call shapes differ in which side sends one message and which side sends a stream." width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The four call shapes differ in which side sends one message and which side sends a stream.&lt;/p&gt;

&lt;p&gt;Streaming fits a live driver feed, a GPS upload, or a trip-status channel. It does not make a slow query faster. The same database work still happens behind the stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the contract buys
&lt;/h2&gt;

&lt;p&gt;First, the client is typed. The compiler catches a wrong argument type before the request leaves the process. A hand-written JSON client catches that at runtime, if it catches it at all. The same &lt;code&gt;.proto&lt;/code&gt; file can generate a Go client and a Java server (&lt;a href="https://grpc.io/docs/what-is-grpc/introduction/" rel="noopener noreferrer"&gt;gRPC introduction&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The second gain is one contract across languages. A Go client and a Java server both generate from the same file. Neither team hand-writes a serializer.&lt;/p&gt;

&lt;p&gt;The third gain is streaming without a second protocol. The four call shapes use the same service definition and the same generated code path.&lt;/p&gt;

&lt;p&gt;Fourth, a deadline travels with the call. The client sets it, the server sees it, and a cancelled call can stop work instead of finishing for a client that has gone.&lt;/p&gt;

&lt;p&gt;None of this makes gRPC automatically faster than JSON over HTTP. The network can dominate a small call. Measure the call path before assuming the binary format is the bottleneck.&lt;/p&gt;

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

&lt;p&gt;Every contributor must run a build step for the contract. A new contributor needs &lt;code&gt;protoc&lt;/code&gt;, the language plugin, and a generated-code step in CI. A stale generated file can compile while disagreeing with the service, so CI should regenerate it.&lt;/p&gt;

&lt;p&gt;When a message is binary, &lt;code&gt;curl&lt;/code&gt; shows bytes, not a readable JSON object. You need a decoder, a proxy, or a logging path that understands protobuf.&lt;/p&gt;

&lt;p&gt;Browsers cannot call a normal gRPC service directly. You need gRPC-Web and a proxy, or a JSON gateway (&lt;a href="https://grpc.io/docs/platforms/web/" rel="noopener noreferrer"&gt;gRPC-Web&lt;/a&gt;). That is extra infrastructure for a public web client.&lt;/p&gt;

&lt;p&gt;HTTP/2 changes connection behavior. A long-lived connection can pin a client to one backend. The balancer then has to balance requests or streams, not only connections. A layer 4 balancer, which balances whole connections, can leave one server busy while another idles (&lt;a href="https://grpc.io/docs/guides/load-balancing/" rel="noopener noreferrer"&gt;gRPC load balancing&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;When you change the schema, take care. Adding a field is usually safe. Renaming a field, changing its type, or reusing a field number can break old clients (&lt;a href="https://protobuf.dev/programming-guides/proto3/#updating" rel="noopener noreferrer"&gt;protobuf update guide&lt;/a&gt;). The compiler checks types, not whether old clients can read the new field.&lt;/p&gt;

&lt;h2&gt;
  
  
  When JSON is the better fit
&lt;/h2&gt;

&lt;p&gt;Use a JSON API when the client is a browser you do not control. A browser needs gRPC-Web, a browser-compatible variant, plus a proxy. It can also use a gateway that translates between gRPC and JSON (&lt;a href="https://grpc.io/docs/platforms/web/" rel="noopener noreferrer"&gt;gRPC-Web&lt;/a&gt;). Use JSON when the API is public, or when the payload is small and the team values readable requests over generated code.&lt;/p&gt;

&lt;p&gt;Use gRPC when both sides are services you build. Use it when the contract is shared across languages, when you need streaming, or when a deadline and typed errors remove repeated plumbing.&lt;/p&gt;

&lt;p&gt;You can keep a public JSON edge and call internal gRPC services. The edge translates once. The internal services get the typed contract.&lt;/p&gt;

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

&lt;p&gt;gRPC replaces hand-written request plumbing with a typed contract and generated stubs; the contract and streaming are the gain, while code generation, binary payloads, and HTTP/2 are the cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is gRPC faster than REST?
&lt;/h3&gt;

&lt;p&gt;Not by definition. It often uses a compact binary format and one multiplexed connection, but latency, payload size, connection reuse, and server work decide the result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does gRPC replace HTTP?
&lt;/h3&gt;

&lt;p&gt;No. It uses HTTP/2 as its transport (&lt;a href="https://www.rfc-editor.org/rfc/rfc9113.html" rel="noopener noreferrer"&gt;RFC 9113&lt;/a&gt;). The method call is mapped onto an HTTP/2 request and response, with gRPC status in the trailers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a browser call gRPC directly?
&lt;/h3&gt;

&lt;p&gt;Not a normal gRPC service. Browsers need gRPC-Web plus a proxy, or a JSON gateway.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is protobuf?
&lt;/h3&gt;

&lt;p&gt;A schema language and binary encoding for typed messages. gRPC uses it by default for the service definition and the payload.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need HTTP/2?
&lt;/h3&gt;

&lt;p&gt;gRPC is designed around HTTP/2 features such as multiplexed streams. A proxy or gateway may expose a different protocol at the edge.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do errors work?
&lt;/h3&gt;

&lt;p&gt;The server returns a gRPC status code and message in the trailers. A deadline or cancellation ends the call even if the server would have succeeded later. If either side ignores the deadline, work continues for a client that has already given up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://grpc.io/docs/what-is-grpc/introduction/" rel="noopener noreferrer"&gt;gRPC: Introduction to gRPC&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://grpc.io/docs/what-is-grpc/core-concepts/" rel="noopener noreferrer"&gt;gRPC: Core concepts, architecture and lifecycle&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://protobuf.dev/overview/" rel="noopener noreferrer"&gt;Protocol Buffers: Overview&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://protobuf.dev/programming-guides/proto3/" rel="noopener noreferrer"&gt;Protocol Buffers: proto3 language guide&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9113.html" rel="noopener noreferrer"&gt;RFC 9113: HTTP/2&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://grpc.io/docs/guides/status-codes/" rel="noopener noreferrer"&gt;gRPC: Status codes&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://grpc.io/docs/platforms/web/" rel="noopener noreferrer"&gt;gRPC-Web&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.awahab.dev/blog/grpc-remote-calls" rel="noopener noreferrer"&gt;awahab.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>microservices</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Load balancing: what a load balancer does and costs</title>
      <dc:creator>Abdul Wahab</dc:creator>
      <pubDate>Mon, 02 Nov 2026 20:07:45 +0000</pubDate>
      <link>https://dev.to/awahaab/load-balancing-what-a-load-balancer-does-and-costs-1md7</link>
      <guid>https://dev.to/awahaab/load-balancing-what-a-load-balancer-does-and-costs-1md7</guid>
      <description>&lt;p&gt;Suppose one of three order servers runs out of database connections on a Friday afternoon. It still accepts connections.&lt;/p&gt;

&lt;p&gt;A load balancer sits in front of the three and hands each request to one of them. It keeps choosing the broken server. Every order that lands there fails.&lt;/p&gt;

&lt;p&gt;The load balancer gave clients one address. It did not remove the failing server. An address alone is not enough if the balancer keeps using a broken server.&lt;/p&gt;

&lt;h2&gt;
  
  
  One address in front of many servers
&lt;/h2&gt;

&lt;p&gt;A load balancer sits in front of a group of servers. That group is the pool. A client sends every request to the balancer's address, and the balancer forwards the request to one server in the pool.&lt;/p&gt;

&lt;p&gt;This role has a name in the HTTP specification: a gateway. A gateway is also called a reverse proxy, which is a server that forwards requests to other servers. RFC 9110 section 3.7 says gateways "enable partitioning or load balancing of HTTP services across multiple machines." The same section calls a gateway a reverse proxy (&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-3.7" rel="noopener noreferrer"&gt;RFC 9110, section 3.7&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Suppose the service has servers at &lt;code&gt;10.0.0.1:8080&lt;/code&gt;, &lt;code&gt;10.0.0.2:8080&lt;/code&gt;, and &lt;code&gt;10.0.0.3:8080&lt;/code&gt;. Clients use one name, &lt;code&gt;api.example.com&lt;/code&gt;. The balancer owns that name and sends each request to one of the three addresses.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft2cme8gr3axv67juv6tc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft2cme8gr3axv67juv6tc.png" alt="Clients send every request to one address. The load balancer chooses a server from the pool." width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clients send every request to one address. The load balancer chooses a server from the pool.&lt;/p&gt;

&lt;p&gt;The client does not need to know how many servers exist, what their addresses are, or which one is busy. It needs the balancer's address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Without one, every client keeps the list
&lt;/h2&gt;

&lt;p&gt;Suppose one API has three servers and three client apps. Without a load balancer, every app needs the list of three addresses. Each app must pick one, notice when one dies, and try another. That logic then lives in every client you ship.&lt;/p&gt;

&lt;p&gt;You must change the address list in every client when you add a server, or leave the new server idle. You must change the list again when you remove a server to maintain it.&lt;/p&gt;

&lt;p&gt;A load balancer chooses in one place. Clients get one address. You can add, remove, or replace servers behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One address, more room, fewer visible failures
&lt;/h2&gt;

&lt;p&gt;Suppose traffic doubles. You add a server and tell the balancer. Clients keep the same address, so no client changes.&lt;/p&gt;

&lt;p&gt;Suppose a server dies. The balancer stops sending it traffic. The next request goes to another server, and the client may not notice.&lt;/p&gt;

&lt;p&gt;Suppose you need to upgrade a server. Take it out of the pool, upgrade it, and put it back. The other servers keep serving.&lt;/p&gt;

&lt;p&gt;The cost is that the balancer now sits in the request path. The rest of the post explains what that means.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the request moves
&lt;/h2&gt;

&lt;p&gt;A request for &lt;code&gt;/images/logo.png&lt;/code&gt; arrives at the balancer at &lt;code&gt;203.0.113.10:443&lt;/code&gt;. The balancer picks &lt;code&gt;10.0.0.2:8080&lt;/code&gt; and forwards the request. The response comes back through the balancer.&lt;/p&gt;

&lt;p&gt;The balancer can choose once for a connection, or once for every request. Layer 4 and layer 7 differ in what they can see. Layer 4 is the transport layer, where connections live. Layer 7 is the application layer, where HTTP requests live.&lt;/p&gt;

&lt;h2&gt;
  
  
  What layer 4 sees, and what layer 7 sees
&lt;/h2&gt;

&lt;p&gt;A layer 4 load balancer works with connections. It sees IP addresses and ports on TCP (Transmission Control Protocol) connections (&lt;a href="https://www.rfc-editor.org/rfc/rfc9293.html" rel="noopener noreferrer"&gt;RFC 9293&lt;/a&gt;). It does not see HTTP paths or headers. It chooses a server when a connection opens and sends every request on that connection to the same server.&lt;/p&gt;

&lt;p&gt;Layer 4 does not parse the request, so it can forward protocols it does not understand. It is also blind to the request. If one request is a slow search and the next is a quick order, the balancer cannot move the quick one to another server. Both sit on the same connection.&lt;/p&gt;

&lt;p&gt;A layer 7 load balancer reads the request. It can see the method, path, and headers. That means it can choose a server for each request, and it can send &lt;code&gt;/images&lt;/code&gt; to one pool and &lt;code&gt;/checkout&lt;/code&gt; to another.&lt;/p&gt;

&lt;p&gt;Layer 7 also depends on the protocol. It is useful for HTTP and less useful for a protocol it cannot parse.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6nyn55icokg2eke0yzwz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6nyn55icokg2eke0yzwz.png" alt="At layer 4, one connection stays on one server. At layer 7, the balancer can choose a server for each request." width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At layer 4, one connection stays on one server. At layer 7, the balancer can choose a server for each request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unequal requests break round robin
&lt;/h2&gt;

&lt;p&gt;The simplest method is round robin. Server A gets the first request, B gets the second, C gets the third, then A again. Round robin keeps one counter for the next turn. It does not use request cost.&lt;/p&gt;

&lt;p&gt;Round robin gives each server one turn. The rule has no input for request cost. One request may finish quickly, while another holds its server for much longer. Equal turns then become unequal work.&lt;/p&gt;

&lt;p&gt;Suppose two slow searches and one quick order arrive together. Round robin sends the first search to A, the order to B, and the second search to A. A stays busy while B finishes and sits idle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwc2bsju1i78hi7s3lmhb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwc2bsju1i78hi7s3lmhb.png" alt="Round robin sends the second search to the busy instance and leaves it working until 1.8 seconds. Least connections sends it to the idle instance." width="800" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Round robin sends the second search to the busy instance and leaves it working until 1.8 seconds. Least connections sends it to the idle instance.&lt;/p&gt;

&lt;p&gt;Round robin has one weakness: it ignores active work. This method sends the next request to the server with the fewest active connections. A busy server gets skipped until it catches up.&lt;/p&gt;

&lt;p&gt;Least connections looks at the number of active connections. It does not use request cost. A slow request holds one connection for a long time. A fast request holds one for a short time.&lt;/p&gt;

&lt;h2&gt;
  
  
  A shallow check can miss a failed order
&lt;/h2&gt;

&lt;p&gt;A load balancer needs to know which servers can take traffic. It can send a small request to each server on a schedule. That is an active check.&lt;/p&gt;

&lt;p&gt;Another option is to watch real requests fail. That is a passive check.&lt;/p&gt;

&lt;p&gt;The balancer sends a health check as a request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/health&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10.0.0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The balancer reads the status code. Status 200 means the path answered. It does not prove that the server can serve an order.&lt;/p&gt;

&lt;p&gt;After enough checks fail, the server leaves the pool. After enough checks succeed, the balancer puts it back.&lt;/p&gt;

&lt;p&gt;A check that tests only the process can pass while orders fail. A check that queries the database can fail when the database fails. If every server checks the same dependency, the dependency can fail every server at once.&lt;/p&gt;

&lt;p&gt;This is why the check should match the problem you care about. Check that the process can answer, then watch the error rate for the work that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The extra network step has a price
&lt;/h2&gt;

&lt;p&gt;A load balancer adds a hop, which is one network step. The request goes to the balancer, then to a server. The balancer is another machine to run and monitor.&lt;/p&gt;

&lt;p&gt;The balancer is now part of the request path. If it fails, clients cannot reach the pool. If you run a second balancer, it can take over when the first fails.&lt;/p&gt;

&lt;p&gt;The balancer can keep a client on one server. That is a sticky session.&lt;/p&gt;

&lt;p&gt;The server must then keep the client's state. If it dies, the state goes with it. If the state lives in a store both servers can reach, any server can serve the next request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sometimes the hop buys nothing
&lt;/h2&gt;

&lt;p&gt;The first case is a service with one server and steady traffic. One server handles the whole load, so a second server would sit idle.&lt;/p&gt;

&lt;p&gt;The second case is a static site with no dynamic requests. A cache close to the client already serves the files.&lt;/p&gt;

&lt;p&gt;A service whose database is the bottleneck does not get faster by adding servers. Fix the database or the query first. The database still does the same work.&lt;/p&gt;

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

&lt;p&gt;A load balancer does not make one server faster. It lets clients share a pool of servers without knowing which server answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is a load balancer a reverse proxy?
&lt;/h3&gt;

&lt;p&gt;RFC 9110 calls a gateway a reverse proxy. See &lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-3.7" rel="noopener noreferrer"&gt;RFC 9110, section 3.7&lt;/a&gt;. A reverse proxy forwards requests to servers. A load balancer is a reverse proxy that also chooses among servers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it make my app faster?
&lt;/h3&gt;

&lt;p&gt;The balancer receives the request, then forwards it to a server. The balancer helps with capacity and failures, not with the speed of one request.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if the balancer fails?
&lt;/h3&gt;

&lt;p&gt;If there is only one, the service is down. Run more than one and give clients a way to reach a working balancer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need sticky sessions?
&lt;/h3&gt;

&lt;p&gt;Only when a server holds state the next request needs, such as an in-memory cart. A store both servers can reach lets any server serve the next request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-3.7" rel="noopener noreferrer"&gt;RFC 9110: HTTP Semantics, section 3.7&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9293.html" rel="noopener noreferrer"&gt;RFC 9293: Transmission Control Protocol&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc6265.html" rel="noopener noreferrer"&gt;RFC 6265: HTTP State Management Mechanism&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/userguide/how-elastic-load-balancing-works.html" rel="noopener noreferrer"&gt;AWS: How Elastic Load Balancing works&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/target-group-health-checks.html" rel="noopener noreferrer"&gt;AWS: Health checks for Application Load Balancer target groups&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/" rel="noopener noreferrer"&gt;NGINX: HTTP load balancing&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.nginx.com/nginx/admin-guide/load-balancer/http-health-check/" rel="noopener noreferrer"&gt;NGINX: HTTP health checks&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers" rel="noopener noreferrer"&gt;Envoy: Supported load balancers&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/outlier" rel="noopener noreferrer"&gt;Envoy: Outlier detection&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.awahab.dev/blog/load-balancing" rel="noopener noreferrer"&gt;awahab.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>devops</category>
      <category>infrastructure</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Text embeddings and cosine similarity</title>
      <dc:creator>Abdul Wahab</dc:creator>
      <pubDate>Mon, 26 Oct 2026 15:20:34 +0000</pubDate>
      <link>https://dev.to/awahaab/text-embeddings-and-cosine-similarity-5639</link>
      <guid>https://dev.to/awahaab/text-embeddings-and-cosine-similarity-5639</guid>
      <description>&lt;p&gt;Text embeddings represent sentences as vectors; cosine similarity compares their directions to find related candidates. A high score still needs checking against the task. For example, "I forgot my password" and "How can I reset my login?" use different words to ask about a similar problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give each sentence a list of numbers
&lt;/h2&gt;

&lt;p&gt;An embedding is a vector: an ordered list of numbers that represents an item. For text, a trained model produces that list from a word, sentence, or passage. The geometry of these vectors can capture useful relationships for a task. &lt;a href="https://developers.google.com/machine-learning/crash-course/embeddings/embedding-space" rel="noopener noreferrer"&gt;Google's introduction to embedding spaces&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Picture a map of help articles. Password-reset instructions and login troubleshooting might sit near one another. A recipe for banana bread should be somewhere else. The map is an analogy for comparing those representations.&lt;/p&gt;

&lt;p&gt;The model learns its representation during training. You do not usually assign one coordinate to "password" and another to "bread." Real embeddings have many dimensions. Individual coordinates need not have an obvious human label.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a small, deliberately invented map
&lt;/h2&gt;

&lt;p&gt;The sketch above uses three hand-written, two-dimensional vectors to illustrate the comparison. The coordinates are invented for teaching, and the scores come from those coordinates. Your browser does not run an embedding model.&lt;/p&gt;

&lt;p&gt;Our query is &lt;code&gt;[1, 0.1]&lt;/code&gt;. A password-reset answer points in almost the same direction, while a recipe points elsewhere. Select a different answer to see its direction and similarity score.&lt;/p&gt;

&lt;p&gt;This toy map makes the arithmetic visible. With a real model, the numbers come from encoding text. A visualization in two dimensions simplifies the full space.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The sketch calculates its score from invented vectors. The score does not measure how a particular model ranks these sentences.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Compare directions with cosine similarity
&lt;/h2&gt;

&lt;p&gt;Cosine similarity divides the dot product of two vectors by the product of their lengths. Its value ranges from −1 to 1 for nonzero vectors. A value near 1 means the vectors point in almost the same direction. The choice of similarity measure depends on the model and task. &lt;a href="https://sbert.net/docs/sentence_transformer/usage/semantic_textual_similarity.html" rel="noopener noreferrer"&gt;Sentence Transformers documents its similarity measures&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;These are example vectors. Real searches use numbers from an embedding model.&lt;/p&gt;

&lt;p&gt;save as similarity.mjs, then run node similarity.mjs&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cosine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;dot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;0&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;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hypot&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hypot&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dot&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;length&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;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&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;answers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reset your password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.92&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Troubleshoot signing in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.43&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;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bake banana bread&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;answers&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;answer&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;cosine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vector&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;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example assumes the vectors have the same dimensions. Its zero-length check returns zero as a fallback. Mathematically, cosine similarity is undefined for a zero vector. The reset answer should rank first and the recipe last.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the comparison into a search
&lt;/h2&gt;

&lt;p&gt;For a real experiment, encode a small set of support articles with a sentence-embedding model. Encode a question with the same model. Calculate the similarities. Return the closest candidates. &lt;a href="https://sbert.net/docs/sentence_transformer/usage/semantic_textual_similarity.html" rel="noopener noreferrer"&gt;Sentence Transformers shows text encoding and comparison&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Try ten short questions before building a larger system. Include alternate wording, spelling mistakes, and an unrelated question. Record which answer appears first. Read that answer to decide whether it helps.&lt;/p&gt;

&lt;p&gt;Keep the question list as a repeatable check. If you change the model or split articles into smaller passages, run the same questions again. This lets you compare results across changes instead of relying on one impressive result.&lt;/p&gt;

&lt;h2&gt;
  
  
  A high score still needs interpretation
&lt;/h2&gt;

&lt;p&gt;"I can sign in" and "I cannot sign in" discuss almost the same subject while describing different states. Include pairs like these in your experiments. They help distinguish topic similarity from the exact relationship your application needs.&lt;/p&gt;

&lt;p&gt;Similarity scores depend on the representation and comparison method. Even familiar cosine scores can behave unexpectedly across models and training choices. &lt;a href="https://arxiv.org/abs/2403.05440" rel="noopener noreferrer"&gt;Research on the limits of cosine similarity&lt;/a&gt; explains why the score needs context.&lt;/p&gt;

&lt;p&gt;Retrieval gives you candidates to inspect. In a support search, a good candidate should also match the product, version, and situation of the user. Ranking alone does not make that decision.&lt;/p&gt;

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

&lt;p&gt;Embeddings turn text into vectors you can compare. Similarity helps find candidates, but you still need to check whether they suit the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://developers.google.com/machine-learning/crash-course/embeddings/embedding-space" rel="noopener noreferrer"&gt;Google: Embedding spaces&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://sbert.net/docs/sentence_transformer/usage/semantic_textual_similarity.html" rel="noopener noreferrer"&gt;Sentence Transformers: Semantic textual similarity&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://arxiv.org/abs/2403.05440" rel="noopener noreferrer"&gt;Research: Is cosine similarity really about similarity?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.awahab.dev/blog/embeddings-and-sentence-similarity" rel="noopener noreferrer"&gt;awahab.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>datascience</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Idempotency and duplicate requests</title>
      <dc:creator>Abdul Wahab</dc:creator>
      <pubDate>Mon, 26 Oct 2026 15:20:33 +0000</pubDate>
      <link>https://dev.to/awahaab/idempotency-and-duplicate-requests-4gbj</link>
      <guid>https://dev.to/awahaab/idempotency-and-duplicate-requests-4gbj</guid>
      <description>&lt;p&gt;An idempotency key identifies retries of the same operation. A database uniqueness constraint can prevent duplicate records, while other side effects need their own coordination. This matters when an order succeeds but its reply is lost: the client must be able to retry without creating a second order.&lt;/p&gt;

&lt;h2&gt;
  
  
  A repeated request is an ordinary situation
&lt;/h2&gt;

&lt;p&gt;Imagine the server receives an order and saves it successfully. The connection drops before the reply reaches your browser, so the browser cannot tell whether the order succeeded. A retry might be sensible, but creating another order can duplicate the work.&lt;/p&gt;

&lt;p&gt;Repeating an idempotent operation gives the same intended effect. APIs can use an idempotency key to identify retries and return a previously recorded result. &lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe documents one practical implementation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The sketch above models two requests for one action. Disable protection. Submit twice. Then reset the example. Enable protection. Submit twice again.&lt;/p&gt;

&lt;p&gt;The request count stays at two while the number of orders changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the action an identity before sending it
&lt;/h2&gt;

&lt;p&gt;two attempts, one intended order&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;/orders&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-7f3a&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notebook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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;Create a key when the user starts an operation. Preserve it across retries. A fresh key for every request makes those requests look like different operations. A new order needs a new key.&lt;/p&gt;

&lt;p&gt;A server should associate the key with the caller and operation. It should check that repeated uses have the same request data. If an order changes, the server should reject reuse of its key. Otherwise, it could return an unrelated result. &lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe's idempotency behavior&lt;/a&gt; shows how it checks repeated parameters.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disabling the Submit button improves the interface. The server also needs to handle repeats from retries, multiple tabs, and clients that do not use that interface.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Make the database enforce uniqueness
&lt;/h2&gt;

&lt;p&gt;Consider two requests arriving together. Both ask whether a key exists. Each sees that it does not exist and tries to create an order. An application-level check followed by a separate insert leaves room for that race.&lt;/p&gt;

&lt;p&gt;A database uniqueness constraint can enforce that a key appears only once within a chosen scope. &lt;a href="https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-UNIQUE-CONSTRAINTS" rel="noopener noreferrer"&gt;PostgreSQL documents uniqueness across groups of columns&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this database example, retries with the same customer and key return the existing order.&lt;/p&gt;

&lt;p&gt;a PostgreSQL schema for this example&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;idempotency_key&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;request_body&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&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;For an operation that only creates this row, the winning insert records the order. A conflicting attempt can read the existing row, compare the request, and reuse the result. More complicated workflows must also coordinate their side effects and recorded results.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small experiment you can run
&lt;/h2&gt;

&lt;p&gt;This Python example uses an in-memory SQLite database, so it needs no database server. It models one customer ordering one item. The uniqueness constraint rejects the repeated key, and the function retrieves the saved result. &lt;a href="https://docs.python.org/3/library/sqlite3.html" rel="noopener noreferrer"&gt;Python's sqlite3 documentation&lt;/a&gt; covers parameter binding and transaction handling.&lt;/p&gt;

&lt;p&gt;save as orders.py, then run python3 orders.py&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;

&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;:memory:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="s"&gt;
  CREATE TABLE orders (
    id INTEGER PRIMARY KEY,
    customer TEXT NOT NULL,
    key TEXT NOT NULL,
    item TEXT NOT NULL,
    UNIQUE(customer, key)
  )
&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&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;with&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO orders(customer, key, item) VALUES (?, ?, ?)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IntegrityError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SELECT id, item FROM orders WHERE customer = ? AND key = ?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Key reused with different request data&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;alice&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;checkout-1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;notebook&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;alice&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;checkout-1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;notebook&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SELECT COUNT(*) FROM orders&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# Expected output: 1, 1, 1 (on separate lines)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change the item in the second call while keeping its key. You should get an error. Change the key instead. You should get a second order. These variations show which requests count as the same operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the guarantee within its boundaries
&lt;/h2&gt;

&lt;p&gt;The example only records database rows. It does not send emails, reserve inventory, or contact another service. A unique order row alone cannot prevent those separate effects from happening twice. A real design must coordinate their identities and recovery paths.&lt;/p&gt;

&lt;p&gt;Decide how long keys remain valid. Define what a second request sees while the first still runs. Keeping keys forever and expiring them quickly have different consequences. Document that contract so clients know when a retry remains safe.&lt;/p&gt;

&lt;p&gt;Draw three moments: before the insert, after the insert, and after the reply. Imagine a failure at each point. Record what a retry should return. Include any work that might already be complete.&lt;/p&gt;

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

&lt;p&gt;Use the same identity for retries of an operation. Enforce that identity where you record the operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe: Idempotent requests&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-UNIQUE-CONSTRAINTS" rel="noopener noreferrer"&gt;PostgreSQL: Unique constraints&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.python.org/3/library/sqlite3.html" rel="noopener noreferrer"&gt;Python: sqlite3&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.awahab.dev/blog/duplicate-requests-and-idempotency" rel="noopener noreferrer"&gt;awahab.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>backend</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Request timeouts: how long to wait, and when to retry</title>
      <dc:creator>Abdul Wahab</dc:creator>
      <pubDate>Mon, 26 Oct 2026 15:16:38 +0000</pubDate>
      <link>https://dev.to/awahaab/request-timeouts-how-long-to-wait-and-when-to-retry-1oko</link>
      <guid>https://dev.to/awahaab/request-timeouts-how-long-to-wait-and-when-to-retry-1oko</guid>
      <description>&lt;p&gt;A client sends a request at 0 seconds. The service answers at 30 seconds. Nothing in the client says stop. For those 30 seconds it holds a connection, a worker, and memory for an answer that has not arrived.&lt;/p&gt;

&lt;p&gt;A request timeout sets that limit. It decides how long your app waits before it gives up.&lt;/p&gt;

&lt;h2&gt;
  
  
  How long do you wait?
&lt;/h2&gt;

&lt;p&gt;A timeout is the maximum time a client waits for a request to complete. Amazon Web Services (AWS) sets one on every remote call, and it sets two: a connection timeout and a request timeout. The Requests library ships without a default, and its docs say why: "Failure to do so can cause your program to hang indefinitely."&lt;/p&gt;

&lt;p&gt;Example: a client with a 2-second timeout sends a request at 0 seconds. The service answers at 30 seconds. The client leaves at 2 seconds, and the answer arrives to nobody.&lt;/p&gt;

&lt;p&gt;Those resources stay reserved until the answer arrives or the wait ends. AWS puts the risk this way. "If a large number of requests hold on to resources for a long time, the server can run out of those resources."&lt;/p&gt;

&lt;p&gt;The limit is narrower than the name suggests. In Requests it does not cover the whole download. It fires when no bytes arrive for that many seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pool fills up
&lt;/h2&gt;

&lt;p&gt;Example: 50 requests arrive each second, and each call hangs for 30 seconds. That is 50 × 30 = 1,500 calls in flight. Workers and connections are fixed, so new requests queue behind the stuck ones.&lt;/p&gt;

&lt;p&gt;In the same setup, a pool of 100 workers faces 1,500 calls. That is 15 times more work than it can run. The queue grows, callers give up, and the app appears down while the code is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the number
&lt;/h2&gt;

&lt;p&gt;Measure how long the downstream service takes. AWS chooses an acceptable false-timeout rate, 0.1% for example.&lt;/p&gt;

&lt;p&gt;AWS takes the matching percentile from the dependency. In this example it is p99.9. 99.9% of calls finish faster than that value. A limit near that value fails about one call in a thousand on a healthy day.&lt;/p&gt;

&lt;p&gt;If the limit is too high, resources stay pinned. If it is too low, healthy calls fail. Those calls get retried, and the retries add traffic.&lt;/p&gt;

&lt;p&gt;The limit splits in two: a connection timeout and a request timeout.&lt;/p&gt;

&lt;p&gt;The AWS Builders' Library describes one team that traced rare timeouts to connection setup. A new secure connection took longer than 20 milliseconds, so a few requests failed right after a deploy. Opening connections at startup removed the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries make it worse
&lt;/h2&gt;

&lt;p&gt;When a service is busy, "frequent client retries can prevent the service from recovering and worsen the problem." Azure's guide calls that a retry storm. When the retries arrive together, it is a thundering herd.&lt;/p&gt;

&lt;p&gt;The AWS Builders' Library says the same. Retries "can amplify the load on a dependent system," and against an overloaded service they can "make the overload worse instead of better." AWS's client libraries retry only when the dependency looks healthy. They stop when retries stop helping.&lt;/p&gt;

&lt;p&gt;Retries also multiply work. When N clients retry together, the total work grows with N squared, according to AWS's jitter analysis.&lt;/p&gt;

&lt;p&gt;Backoff, waiting longer after each failure, helps a little. Jitter, a random delay before each retry, spreads the spikes. In AWS's 100-client simulation, jitter cut call counts by more than half.&lt;/p&gt;

&lt;p&gt;Cap the attempts and the total retry time. Azure's guide puts the cap at seconds or minutes, not forever.&lt;/p&gt;

&lt;p&gt;Retry only calls you can repeat. A write that timed out may still have happened, so retries need idempotent operations, which can run twice without harm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deadlines for nested calls
&lt;/h2&gt;

&lt;p&gt;Three calls with 2-second limits can burn 6 seconds before the first error reaches the user.&lt;/p&gt;

&lt;p&gt;Give the whole request one deadline and pass what remains to each step. A 2-second budget spends 300 milliseconds on the first call. The rest of the chain gets 1.7 seconds and fails before the user does.&lt;/p&gt;

&lt;p&gt;a 2-second limit in a browser&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;// The fetch aborts with a TimeoutError DOMException after 2 seconds.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&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 signal aborts with a TimeoutError when the time passes, as MDN documents. A caller can catch the error and tell it apart from a network failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Abort closes your side
&lt;/h2&gt;

&lt;p&gt;Aborting a call closes your side of the connection. The remote service may keep working unless that service watches for the client to cancel. AWS is blunt about what that leaves open: "A timeout or failure doesn't necessarily mean that side effects haven't happened."&lt;/p&gt;

&lt;p&gt;The timeout buys back your resources. It does not buy back theirs.&lt;/p&gt;

&lt;p&gt;Without that number, a service has no limit on how long it can take. Its slowest dependency decides.&lt;/p&gt;

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

&lt;p&gt;Every request needs a limit. Without one, the slowest dependency sets the pace of your app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is a timeout the same as a retry?
&lt;/h3&gt;

&lt;p&gt;No. A timeout ends one attempt. A retry starts another, and it needs a safe call, a capped delay, and a limit on attempts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a timeout stop the server work?
&lt;/h3&gt;

&lt;p&gt;No. It ends your wait. The remote service keeps running unless that service watches for the client to cancel, and a 30-second call can finish long after your 2-second limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/" rel="noopener noreferrer"&gt;AWS Builders' Library: Timeouts, retries, and backoff with jitter&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static" rel="noopener noreferrer"&gt;MDN: AbortSignal.timeout()&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://requests.readthedocs.io/en/latest/user/quickstart/#timeouts" rel="noopener noreferrer"&gt;Requests: Quickstart (timeouts)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/architecture/antipatterns/retry-storm/" rel="noopener noreferrer"&gt;Azure Architecture Center: Retry Storm antipattern&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/" rel="noopener noreferrer"&gt;AWS Architecture Blog: Exponential Backoff and Jitter&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.awahab.dev/blog/request-timeouts" rel="noopener noreferrer"&gt;awahab.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>networking</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Circuit breakers: when to stop calling a service</title>
      <dc:creator>Abdul Wahab</dc:creator>
      <pubDate>Mon, 26 Oct 2026 15:16:37 +0000</pubDate>
      <link>https://dev.to/awahaab/circuit-breakers-when-to-stop-calling-a-service-2ji</link>
      <guid>https://dev.to/awahaab/circuit-breakers-when-to-stop-calling-a-service-2ji</guid>
      <description>&lt;p&gt;Imagine a payments service that starts timing out at 14:02. Your app keeps sending requests. Each one waits two seconds and fails. By 14:04 the worker pool is full, and checkout is down because one dependency is sick.&lt;/p&gt;

&lt;p&gt;A circuit breaker stops the sending. After a set number of failures it returns an error without making the call. Your app keeps the capacity it would have spent on calls that cannot succeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The calls that cannot succeed
&lt;/h2&gt;

&lt;p&gt;A breaker wraps a remote call and watches the result. Past a threshold it stops calling. Azure's architecture guide states the purpose: the pattern "helps prevent an application from repeatedly trying to run an operation that's likely to fail."&lt;/p&gt;

&lt;p&gt;Martin Fowler credits Michael Nygard's book Release It! with popularizing the pattern. In Fowler's account, the breaker trips once failures reach a threshold. Then "all further calls to the circuit breaker return with an error, without the protected call being made at all."&lt;/p&gt;

&lt;p&gt;You save the wait, not the error. A failing call still holds a connection and a worker until its timeout fires. Azure warns that blocked requests "might hold critical system resources, such as memory, threads, and database connections." Enough of them and unrelated parts of the app fail too.&lt;/p&gt;

&lt;p&gt;Example: a checkout service calls payments with a 2-second timeout. Payments hangs. At 50 checkout requests a second, 100 workers sit waiting on the same broken dependency, and the pool is gone in under a minute.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.awahab.dev%2Fagent-12-c1bb6ee9a786c2a4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.awahab.dev%2Fagent-12-c1bb6ee9a786c2a4.png" alt="Call-path diagram: your app calls the breaker, which forwards the call to payments while closed. When the breaker is open, the forward call is crossed out and an error returns to the app at once." width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The breaker sits between the caller and the dependency; when it opens, the call stops there and the error comes back at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closed, open, half-open
&lt;/h2&gt;

&lt;p&gt;The breaker is a small state machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Closed.&lt;/strong&gt; Calls pass. The breaker counts failures inside a rolling window, a moving slice of time such as the last 10 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open.&lt;/strong&gt; Calls fail at once. Nothing reaches the dependency, and a timer runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Half-open.&lt;/strong&gt; When the timer ends, a few trial calls pass. A success closes the breaker and resets the count. A failure reopens it and restarts the timer.&lt;/p&gt;

&lt;p&gt;Azure explains why the third state earns its place. A recovering service "might be able to support a limited volume of requests until the recovery is complete." A flood of work can knock it over again.&lt;/p&gt;

&lt;p&gt;Failures open the breaker, the cooldown leaves it half-open, and the trial call decides the next state.&lt;/p&gt;

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

&lt;p&gt;Start with a count of failures in a window. Azure describes a counter that resets at intervals, so the occasional error does not open the circuit. The threshold trips only when enough failures land inside the period.&lt;/p&gt;

&lt;p&gt;Fowler suggests a rate instead: open at a 50% failure rate. Different errors can have different limits, such as "a threshold of 10 for timeouts but 3 for connection failures."&lt;/p&gt;

&lt;p&gt;Count only errors that say the dependency is unhealthy. A 404 for a missing record, or a rejected input, is a normal answer. Fowler is direct: "Not all errors should trip the circuit."&lt;/p&gt;

&lt;p&gt;Scope the breaker to one dependency, often to one operation. Wide scope causes a problem: a single breaker over every call to a service will open for a fault that affects one endpoint.&lt;/p&gt;

&lt;p&gt;a breaker with three states&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;// A breaker for one dependency. The numbers are examples.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;breaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;threshold&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; &lt;span class="nx"&gt;cooldown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt; &lt;span class="cm"&gt;/* ms */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;openedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&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;args&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;open&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;openedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;openedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cooldown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;circuit open&lt;/span&gt;&lt;span class="dl"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;openedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;openedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next call after the cooldown is the probe. A success resets the breaker. A failure starts the cooldown again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the caller sees
&lt;/h2&gt;

&lt;p&gt;An open breaker returns an error at once. The caller needs a plan for that error. Fowler lists the choices: fail the operation, use a workaround, show stale data, or put the work on a queue for later.&lt;/p&gt;

&lt;p&gt;Example: a product page calls recommendations and inventory. With the recommendations breaker open, the page renders without the recommendations strip and keeps the buy button. The inventory call still runs. One broken dependency costs a strip, not the sale.&lt;/p&gt;

&lt;p&gt;Hystrix, Netflix's breaker library, calls this fallback and graceful degradation. It also rejects a call when its pool of threads is full, instead of queueing work that will wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the breaker lives
&lt;/h2&gt;

&lt;p&gt;You can put the breaker in the client. Hystrix, Netflix's library, and resilience4j, a Java library, wrap each call in process. The breaker knows which operation it protects, so per-endpoint thresholds are possible.&lt;/p&gt;

&lt;p&gt;You can put it in the network. Envoy's outlier detection is passive health checking: it removes hosts that fail or slow down from the load-balancing set. Envoy caps how many hosts it ejects, and each repeat lengthens the time a host stays out.&lt;/p&gt;

&lt;p&gt;The two see different things. A proxy sees host health, not what the operation means. A client library sees the operation, but each instance keeps its own count unless you share state.&lt;/p&gt;

&lt;h2&gt;
  
  
  It does not fix the service
&lt;/h2&gt;

&lt;p&gt;A breaker protects your capacity. It does not repair the dependency, and it does not make a failed call succeed. The half-open probe exists to notice recovery, nothing more.&lt;/p&gt;

&lt;p&gt;Traffic returns when the breaker closes. If every instance probes at the same moment, the recovering service gets a wave. Stagger the probes, or let one instance probe first.&lt;/p&gt;

&lt;p&gt;Order matters with retries. Retries must respect the breaker. Azure says retry logic must watch what the breaker returns. Retries should stop "if the circuit breaker indicates that a fault isn't transient."&lt;/p&gt;

&lt;p&gt;Log every state change with the failure count that caused it. An open breaker is a signal about the dependency. Fowler argues that operations staff should be able to trip or reset a breaker by hand.&lt;/p&gt;

&lt;p&gt;Without a breaker, the slowest dependency decides how much of your capacity it gets. With one, you decide how many calls to waste before you stop.&lt;/p&gt;

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

&lt;p&gt;A timeout bounds one call. A breaker bounds the number of calls, and protects your capacity, not the dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is a circuit breaker the same as a timeout?
&lt;/h3&gt;

&lt;p&gt;No. A timeout bounds one call. A breaker stops new calls to a dependency that keeps failing. Use both: the timeout ends the call, and the breaker ends the attempts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which errors should open it?
&lt;/h3&gt;

&lt;p&gt;Only errors that mean the dependency is unhealthy: timeouts, connection errors, and 5xx responses. A 404 or a rejected input is a normal result and should not count.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens to calls already in flight?
&lt;/h3&gt;

&lt;p&gt;They finish or hit their timeout. The breaker only blocks new calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker" rel="noopener noreferrer"&gt;Azure Architecture Center: Circuit Breaker pattern&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://martinfowler.com/bliki/CircuitBreaker.html" rel="noopener noreferrer"&gt;Martin Fowler: CircuitBreaker&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/Netflix/Hystrix/wiki" rel="noopener noreferrer"&gt;Netflix Hystrix wiki&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/outlier" rel="noopener noreferrer"&gt;Envoy: Outlier detection&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.awahab.dev/blog/circuit-breakers" rel="noopener noreferrer"&gt;awahab.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>microservices</category>
    </item>
    <item>
      <title>HTTP caching and stale responses</title>
      <dc:creator>Abdul Wahab</dc:creator>
      <pubDate>Sun, 18 Oct 2026 19:40:26 +0000</pubDate>
      <link>https://dev.to/awahaab/why-clearing-cache-fixes-things-2g7a</link>
      <guid>https://dev.to/awahaab/why-clearing-cache-fixes-things-2g7a</guid>
      <description>&lt;p&gt;Clearing your browser cache removes saved responses, so the next request can fetch a newer version. It does not change the server or clear a CDN's cache. If your browser shows an old page while a friend sees the update, the difference may be in which cached response each browser can reuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  A saved copy with permission to reuse it
&lt;/h2&gt;

&lt;p&gt;Imagine copying a café menu into your notebook. Checking the notebook is quicker than walking back to the counter. The trade-off is that the café might change its menu while your copy stays the same.&lt;/p&gt;

&lt;p&gt;An HTTP (Hypertext Transfer Protocol) cache keeps a response so a later request can reuse it. A browser cache lives on your device. A shared cache, such as a content delivery network (CDN), can serve many visitors. Reuse can avoid a request to the origin server. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching" rel="noopener noreferrer"&gt;MDN explains the different cache layers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The cache needs a rule for how long it can reuse a saved copy. Without one, visitors can receive confusing answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fresh response can differ from the server
&lt;/h2&gt;

&lt;p&gt;an example response&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;Cache-Control&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;max-age=60&lt;/span&gt;
&lt;span class="na"&gt;ETag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"menu-v1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;max-age=60&lt;/code&gt; gives the response a freshness lifetime of 60 seconds. During that time, an ordinary request can reuse the stored response. If the café changes its menu after ten seconds, the saved copy can still count as fresh under this rule.&lt;/p&gt;

&lt;p&gt;Your browser can reuse the old page until its cache timer expires.&lt;/p&gt;

&lt;p&gt;Once the response is stale, the cache needs another way to decide whether it can reuse it. In this example, it asks the server to validate its copy. The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control" rel="noopener noreferrer"&gt;Cache-Control reference&lt;/a&gt; explains the headers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In the sketch above, publish a new version. Then request the page. Advance time past 60 seconds. Request it again. The server changes immediately, but the cached copy follows its freshness rule.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Revalidating a saved response
&lt;/h2&gt;

&lt;p&gt;The server supplies a version identifier called an &lt;code&gt;ETag&lt;/code&gt;. The client can send its saved identifier in an &lt;code&gt;If-None-Match&lt;/code&gt; request header. If the identifier still matches, the server can answer &lt;code&gt;304 Not Modified&lt;/code&gt; without sending the response body again. If it differs, the server returns the new representation. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag" rel="noopener noreferrer"&gt;ETag reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;the version check&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/menu&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;If-None-Match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"menu-v1"&lt;/span&gt;

# If the server still has menu-v1:
HTTP/1.1 304 Not Modified
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cache checks for an update and can still reuse the bytes it already downloaded. To see this in the sketch, request an expired copy without publishing an update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clearing the browser cache
&lt;/h2&gt;

&lt;p&gt;After you remove the saved response from your browser, its next request cannot use that local copy. This can reveal a newer version. It does not automatically empty the cache in a CDN or an application. Those are separate layers.&lt;/p&gt;

&lt;p&gt;Which cache supplied the response? Inspect the request in the Network panel of your browser. Check the response headers. Compare a normal load with one that bypasses the browser cache. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching#reload_and_force_reload" rel="noopener noreferrer"&gt;MDN covers reloads and stored responses&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Suppose three colleagues see different versions. Record who saw each version and when. Then inspect the cache headers. That timeline can make the freshness rule easier to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three rules worth recognizing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Directive&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;max-age=60&lt;/td&gt;
&lt;td&gt;Allow reuse while the response is fresh for 60 seconds.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no-cache&lt;/td&gt;
&lt;td&gt;Allow storage. Validate before reuse.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no-store&lt;/td&gt;
&lt;td&gt;Do not store the response in an HTTP cache.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Despite its name, &lt;code&gt;no-cache&lt;/code&gt; permits storage. It requires validation before reuse. &lt;code&gt;no-store&lt;/code&gt; prevents storage. These directives control HTTP caching. An application can also manage separate caches. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control" rel="noopener noreferrer"&gt;Cache-Control reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For practice, open the Network panel on any public page. Select one document and one image. Compare their caching headers. Would you accept the same freshness lifetime for both resources? Explain your choice.&lt;/p&gt;

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

&lt;p&gt;A cache can treat a response as fresh even after the server changes. Choose its freshness rules deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching" rel="noopener noreferrer"&gt;MDN: HTTP caching&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control" rel="noopener noreferrer"&gt;MDN: Cache-Control&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag" rel="noopener noreferrer"&gt;MDN: ETag&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.awahab.dev/blog/why-clearing-cache-fixes-things" rel="noopener noreferrer"&gt;awahab.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
