TL;DR: I built Loom, a transparent gRPC proxy that decodes traffic in real-time using Server Reflection. No .proto files needed. It gives you a browser UI to watch, inspect, and replay calls.
joshuabvarghese
/
Loom
gRPC L7 Debugging Proxy
Loom
A gRPC debugging proxy. Point it at your backend, point your client at Loom, and watch every call decoded in a browser tab.
Your gRPC Client → Loom (:9999) → Your Backend (:50051)
↓
Web Inspector
http://localhost:9998
Why
gRPC traffic is binary. Wireshark can't read it. grpcurl is great for one-off calls but you can't watch a flow. I kept running it over and over trying to understand what was happening between services.
Loom sits transparently between your client and backend. It uses Server Reflection to decode every frame on the fly — no .proto files required — and streams the results into a browser UI. You see the JSON payloads, the status codes, how long each call took, and a ready-to-copy grpcurl command to replay any of them.
What it does
- Intercepts all four gRPC stream types — unary, server-streaming, client-streaming, bidi
- Auto-decodes using Server Reflection (no proto…
The problem that wouldn't go away
I was working on a side project with gRPC between two services, and I kept hitting the same wall.
The traffic is binary. You can't just open DevTools and watch what's happening.
My workflow became:
- Run
grpcurlover and over - Copy-paste the same commands
- Try to piece together what went wrong
- Repeat
It was tedious. And I kept thinking — why isn't there just something I can WATCH?
So I built one.
What Loom does
Loom sits transparently between your gRPC client and backend.
Point your client at Loom instead of your backend, and it proxies everything through while decoding each frame on the fly.
What you get:
- 🔴 Real-time call inspection — every request/response appears in your browser
- 📦 JSON payloads — no more squinting at binary
- ✅ Status codes & latency — at a glance
- 📋 Copyable
grpcurlcommands — replay any call instantly
No .proto files needed. It uses Server Reflection automatically.
The part that surprised me most
I thought the hardest part would be the HTTP/2 plumbing.
It wasn't.
The thing that took me the longest was the reflection cache.
Here's the problem: when multiple goroutines all need the same method descriptor at the same time, the naive approach makes ALL of them hit the backend simultaneously. That's wasteful and slow.
I needed what's called stampede protection — only one goroutine does the fetch, the rest wait.
Getting the mutex logic right without deadlocking took me an embarrassing amount of time. Like, staring at the screen at 11 PM embarrassing.
But I got there.
What I'd do differently
1. Split up the web UI earlier
The web UI is one 40KB file. It works. But if I was starting again, I'd split it up properly.
I kept telling myself "I'll refactor it later".
Later never came. 😅
2. Write tests FIRST for the circuit breaker
I wrote them after implementing it. Found two edge cases I'd missed in the half-open state.
Lesson learned. Tests first. Always.
What I learned about Go specifically
Coming from other languages, I underestimated how much Go's concurrency model would change my thinking.
Once I stopped fighting it and just thought in goroutines and channels, everything clicked:
- The circuit breaker
- The recorder fanning out to three sinks simultaneously
- The SSE hub
All of it felt natural in a way it never would have in other languages.
Go didn't just let me build this — it guided how I thought about the problem.
Try it yourself
MIT licensed. Single binary. Has a -demo mode that needs nothing to get started:
go run -mod=vendor . -demo
# open http://localhost:9998
I'd love your feedback
This is my first real Go project. I'm still learning.
If you have thoughts on:
The code architecture
The idea itself
Things I missed
Or just want to say hi
Subscribe to get notified when Part 2 will talk more on Deep dive into the reflection cache & stampede protection when it drops 👇
Top comments (0)