<?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: Daniil R</title>
    <description>The latest articles on DEV Community by Daniil R (@youngpib0dy).</description>
    <link>https://dev.to/youngpib0dy</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%2F4049599%2F98bb7f92-c183-4809-b539-741dfc963f42.jpg</url>
      <title>DEV Community: Daniil R</title>
      <link>https://dev.to/youngpib0dy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/youngpib0dy"/>
    <language>en</language>
    <item>
      <title>Your gRPC stream is "healthy" and serving nothing: synthetic monitoring for server-side streams</title>
      <dc:creator>Daniil R</dc:creator>
      <pubDate>Mon, 27 Jul 2026 13:26:37 +0000</pubDate>
      <link>https://dev.to/youngpib0dy/your-grpc-stream-is-healthy-and-serving-nothing-synthetic-monitoring-for-server-side-streams-5gcn</link>
      <guid>https://dev.to/youngpib0dy/your-grpc-stream-is-healthy-and-serving-nothing-synthetic-monitoring-for-server-side-streams-5gcn</guid>
      <description>&lt;p&gt;&lt;em&gt;By Daniil Romashov — SRE/DevOps engineer. The tool described here is open source:&lt;br&gt;
&lt;a href="https://github.com/youngpabl0/grpc-streams-checker" rel="noopener noreferrer"&gt;github.com/youngpabl0/grpc-streams-checker&lt;/a&gt; (Apache-2.0).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Uptime checks are a solved problem for request/response APIs: hit the endpoint, read the status code, done. &lt;br&gt;
Server-side streaming RPCs break that model completely. A gRPC stream isn't a request and a response — it's a channel the server holds open and pushes messages into over time. The&lt;br&gt;
connection can be up, the handshake can succeed, the health endpoint can be green — and yet the&lt;br&gt;
only thing that matters, &lt;strong&gt;frames arriving with valid data&lt;/strong&gt;, can be silently broken. &lt;br&gt;
Your probe says "healthy" while consumers get nothing.&lt;/p&gt;

&lt;p&gt;I hit this running real-time market-data streams behind a gRPC-web/Envoy stack. In a realistic&lt;br&gt;
production topology the stream crosses several hops before anyone consumes it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client ──▶ LB ──▶ Envoy (grpc-web / HTTP/2) ──▶ backend ──▶ upstream feed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every hop is a place where the stream dies while everything still looks healthy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an &lt;strong&gt;idle timeout on the LB or proxy&lt;/strong&gt; quietly kills long-lived connections;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Envoy / grpc-web translation&lt;/strong&gt; buffers or drops frames while the TCP session stays up;&lt;/li&gt;
&lt;li&gt;the backend keeps the stream open but its &lt;strong&gt;upstream feed went quiet&lt;/strong&gt; — nothing to push;&lt;/li&gt;
&lt;li&gt;frames arrive, but they're &lt;strong&gt;garbage&lt;/strong&gt; — empty payloads, missing fields — after a bad deploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is visible to an HTTP 200 probe or a standard gRPC health check. &lt;br&gt;
"The stream stopped&lt;br&gt;
producing" was simultaneously our worst failure mode and the one nothing caught — customers reported it before monitoring did. So I built a synthetic checker for it, ran it in production for&lt;br&gt;
a year across six streaming methods, and have now open-sourced a clean rewrite.&lt;/p&gt;
&lt;h2&gt;
  
  
  The core idea: consume the stream like a real client
&lt;/h2&gt;

&lt;p&gt;The only probe that answers "is this stream actually serving valid data?" is a real client. &lt;br&gt;
For each configured stream, on a infinite cycle, grpc-streams-checker doing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;loads the &lt;code&gt;.proto&lt;/code&gt; at runtime&lt;/strong&gt; (via &lt;code&gt;@grpc/proto-loader&lt;/code&gt; — no codegen, no stale stubs;
adding a stream to monitoring = pointing at a file and naming a method);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;opens the server-side streaming RPC&lt;/strong&gt; — TLS or plaintext, auth metadata if needed;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;collects N frames&lt;/strong&gt; (not just the first!) within a time budget, measuring time-to-first-frame
and the largest gap between consecutive frames;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;validates every frame's payload&lt;/strong&gt; — required fields present, values matching exact strings or
regexes — because "frames are flowing" and "frames are correct" are different failures;&lt;/li&gt;
&lt;li&gt;cancels the stream and exports the outcome as Prometheus metrics.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Point 3 deserves emphasis. A stream that emits one frame and hangs is a different (and sneakier) failure than one that never starts. Demanding, say, &lt;code&gt;minFrames: 3&lt;/code&gt; per check verifies &lt;em&gt;sustained&lt;/em&gt; flow. &lt;br&gt;
And the largest inter-frame gap turns out to be a great early-warning signal: a stuttering&lt;br&gt;
producer degrades there long before it goes fully dark.&lt;/p&gt;

&lt;p&gt;Point 4 catches the failure nobody instruments for: the deploy that keeps the stream up but breaks the payload. &lt;br&gt;
An empty &lt;code&gt;symbol&lt;/code&gt; field in a price tick is an outage for the consumer even though every transport-level signal is green.&lt;/p&gt;
&lt;h2&gt;
  
  
  Metrics designed for alerting, not just dashboards
&lt;/h2&gt;

&lt;p&gt;Every failure mode gets its own result label, and — the key design decision — every per-stream metric carries a &lt;strong&gt;&lt;code&gt;method&lt;/code&gt; label&lt;/strong&gt;, so per-method dashboards and ownership-routed alerts fall out of the label set for couple of minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight prometheus"&gt;&lt;code&gt;&lt;span class="n"&gt;grpc_stream_up&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;dc&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;                      &lt;span class="c"&gt;# 1 = frames arrived AND payloads valid&lt;/span&gt;
&lt;span class="n"&gt;grpc_stream_check_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;                        &lt;span class="c"&gt;# ok|timeout|insufficient_frames|validation_failed|error&lt;/span&gt;
&lt;span class="n"&gt;grpc_stream_error_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;                          &lt;span class="c"&gt;# UNAVAILABLE, DEADLINE_EXCEEDED, UNAUTHENTICATED…&lt;/span&gt;
&lt;span class="n"&gt;grpc_stream_first_frame_ms&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;_duration_seconds&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;histogram&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;# degrades before it breaks&lt;/span&gt;
&lt;span class="n"&gt;grpc_stream_max_inter_frame_ms&lt;/span&gt;                             &lt;span class="c"&gt;# sustained-flow health&lt;/span&gt;
&lt;span class="n"&gt;grpc_stream_frames_last_check&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;_total&lt;/span&gt;                     &lt;span class="c"&gt;# did we get the 3-5 we demanded?&lt;/span&gt;
&lt;span class="n"&gt;grpc_stream_validation_fail_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;                &lt;span class="c"&gt;# missing_field | mismatch | empty_frame&lt;/span&gt;
&lt;span class="n"&gt;grpc_stream_last_success_timestamp_seconds&lt;/span&gt;                 &lt;span class="c"&gt;# staleness alerting&lt;/span&gt;
&lt;span class="n"&gt;grpc_stream_last_run_timestamp_seconds&lt;/span&gt;                     &lt;span class="c"&gt;# a dead checker must never look like health&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Success rate by method — one query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sum by (method) (rate(grpc_stream_check_total{result="ok"}[5m]))
/
sum by (method) (rate(grpc_stream_check_total[5m]))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repo ships &lt;a href="https://github.com/youngpabl0/grpc-streams-checker/blob/main/alerts/grpc-streams-checker.rules.yml" rel="noopener noreferrer"&gt;ready-made Prometheus alert rules&lt;/a&gt;&lt;br&gt;
— stream down, slow first frame, stuttering producer, invalid payloads, repeated gRPC errors, and&lt;br&gt;
"the checker itself died" (silence must never masquerade as health) — plus&lt;br&gt;
&lt;a href="https://github.com/youngpabl0/grpc-streams-checker/tree/main/deploy/kubernetes" rel="noopener noreferrer"&gt;Kubernetes manifests&lt;/a&gt;&lt;br&gt;
with probes, resource limits, and a ServiceMonitor.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it in 32 seconds
&lt;/h2&gt;

&lt;p&gt;The repo includes a demo gRPC server with intentional failure modes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/youngpabl0/grpc-streams-checker.git
&lt;span class="nb"&gt;cd &lt;/span&gt;grpc-streams-checker &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm ci

node examples/demo-server.js &amp;amp;        &lt;span class="c"&gt;# a PriceStream on :50051&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;streams.example.json streams.json
node src/index.js &lt;span class="nt"&gt;--once&lt;/span&gt;
&lt;span class="c"&gt;# → {"stream":"price_stream","result":"ok","frames":3,"firstFrameMs":223,...}&lt;/span&gt;

node examples/demo-server.js &lt;span class="nt"&gt;--silent&lt;/span&gt;   &lt;span class="c"&gt;# opens, never sends → result: timeout&lt;/span&gt;
node examples/demo-server.js &lt;span class="nt"&gt;--broken&lt;/span&gt;   &lt;span class="c"&gt;# empty fields       → result: validation_failed&lt;/span&gt;
node examples/demo-server.js &lt;span class="nt"&gt;--slow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000 &lt;span class="c"&gt;# stuttering        → max_inter_frame_ms grows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--once&lt;/code&gt; exits non-zero on any failure, so the same binary doubles as a CI gate or cron probe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a use in production taught me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-method labels pay for themselves.&lt;/strong&gt; With &lt;code&gt;team&lt;/code&gt;-routed alerting on top, each squad gets
paged only for its own streams — one checker serves the whole org.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-to-first-frame is the canary.&lt;/strong&gt; Every full outage we had was preceded by visible
first-frame degradation. Alert on the trend, not just the corpse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate payloads.&lt;/strong&gt; Two of our worst incidents were "stream up, data wrong." Transport
metrics can't see that class of failure at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor the monitor.&lt;/strong&gt; &lt;code&gt;last_run_timestamp&lt;/code&gt; with a "checker is dead" alert is non-negotiable;
a crashed checker otherwise reads as a fleet of healthy streams.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern generalizes to any long-lived push transport — WebSocket, SSE, queue consumers. The&lt;br&gt;
value is turning &lt;em&gt;"is data actually flowing, and is it valid?"&lt;/em&gt; into labelled metrics you page&lt;br&gt;
yourself about, instead of something a customer reports.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Daniil Romashov is a Site Reliability / DevOps engineer specializing in reliability and&lt;br&gt;
observability for high-load systems. &lt;br&gt;
Code and manifests: &lt;a href="https://github.com/youngpabl0/grpc-streams-checker" rel="noopener noreferrer"&gt;github.com/youngpabl0/grpc-streams-checker&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>sre</category>
      <category>devops</category>
      <category>grpc</category>
    </item>
  </channel>
</rss>
