gRPC StreamAlerts: Low-Latency Risk Signals for Trading Systems
Executive Summary
Webhooks deliver events reliably and asynchronously, but the round-trip including DNS, TLS handshake, and HTTP overhead means typical delivery latencies are 200-800ms. For high-frequency MM strategies that re-quote on tier changes, that is too slow. SettleRisk's gRPC StreamAlerts RPC opens a long-lived bidirectional connection over HTTP/2 and pushes events with sub-100ms median latency. This post explains the protocol, walks through a Go consumer, and covers the operational gotchas.
Core Concept
StreamAlerts is a server-streaming RPC defined in the SettleRisk protobuf:
service VelloxService {
rpc StreamAlerts (StreamAlertsRequest) returns (stream Alert);
}
message StreamAlertsRequest {
repeated string platforms = 1; // ["polymarket", "kalshi"]
repeated string event_types = 2; // ["score.tier_changed", ...]
string tenant_id = 3;
}
message Alert {
string event_id = 1;
string event_type = 2;
string market_id = 3;
google.protobuf.Timestamp timestamp = 4;
google.protobuf.Struct payload = 5;
}
Connect, send a StreamAlertsRequest describing which platforms and event types you care about, and receive Alert messages as the events fire. The connection multiplexes over a single HTTP/2 stream so server push is essentially zero-cost per event.
| Layer | Webhook | gRPC StreamAlerts |
|-------|---------|------------------|
| Connection | One per event | One per session |
| Median latency | 200-800 ms | 30-100 ms |
| Plan tier requirement | Builder+ | Fund+ |
| Authentication | HMAC per request | mTLS or token, per-session |
| Replay | Yes, via /v1/webhook-deliveries | No (use webhooks for replay) |
Worked Example
A Go consumer wired up against the production gRPC endpoint:
package main
import (
"context"
"log"
"time"
pb "github.com/settlerisk/vellox-proto/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
)
func main() {
creds := credentials.NewTLS(nil)
conn, err := grpc.Dial("api.settlerisk.com:50051", grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatalf("dial: %v", err)
}
defer conn.Close()
client := pb.NewVelloxServiceClient(conn)
ctx := metadata.AppendToOutgoingContext(
context.Background(),
"authorization", "Bearer " + apiKey,
"x-vellox-key-id", keyID,
)
stream, err := client.StreamAlerts(ctx, &pb.StreamAlertsRequest{
Platforms: []string{"polymarket", "kalshi"},
EventTypes: []string{"score.tier_changed", "rules.changed"},
TenantId: tenantID,
})
if err != nil {
log.Fatalf("stream: %v", err)
}
for {
alert, err := stream.Recv()
if err != nil {
log.Printf("recv error: %v — reconnecting in 2s", err)
time.Sleep(2 * time.Second)
return
}
log.Printf("[%s] %s -> %s", alert.EventId, alert.MarketId, alert.EventType)
handleAlert(alert)
}
}
Python equivalent using grpcio:
import grpc
import vellox_pb2 as pb
import vellox_pb2_grpc
def main():
creds = grpc.ssl_channel_credentials()
channel = grpc.secure_channel("api.settlerisk.com:50051", creds)
stub = vellox_pb2_grpc.VelloxServiceStub(channel)
metadata = (("authorization", f"Bearer {API_KEY}"), ("x-vellox-key-id", KEY_ID))
req = pb.StreamAlertsRequest(
platforms=["polymarket", "kalshi"],
event_types=["score.tier_changed"],
tenant_id=TENANT_ID,
)
for alert in stub.StreamAlerts(req, metadata=metadata):
print(f"[{alert.event_id}] {alert.market_id} -> {alert.event_type}")
handle_alert(alert)
Implementation Notes
Always reconnect on stream error. Long-lived streams will drop. TCP RST, server restarts, network blips — they all happen. Wrap the receive loop with an exponential backoff reconnect.
Use HTTP/2 keepalive. Set grpc.WithKeepaliveParams (Go) or keepalive_time_ms (Python) to 30s so idle streams don't get killed by intermediate proxies.
Don't process inline. Push alerts to a queue and process them in worker goroutines or async tasks. A slow consumer can stall the entire stream.
Subscribe minimally. Each platform + event type filter is server-side. Don't subscribe to * and filter client-side; subscribe only to what you need.
| Concern | Recommendation |
|---------|----------------|
| Reconnection | Exponential backoff, max 30s delay |
| Keepalive | 30s, permit-without-stream |
| Backpressure | Async queue, never block stream.Recv |
| Auth refresh | Re-resolve token before re-dial |
| Tenant isolation | Set tenant_id explicitly |
Failure Modes
1. Blocking the receive loop. If your handler runs synchronously and takes > 1s, you fall behind. Symptoms: rising lag, eventual disconnect.
2. Skipping reconnect logic. First disconnect kills the consumer. Always assume the stream will drop and handle it.
3. Forgetting idempotency. A reconnect may replay a small number of events that were in flight. Dedupe on event_id exactly as you would with webhooks.
4. Wrong proto schema version. The proto file evolves additively but new fields require regenerated stubs. Pin a version in your build.
5. Using StreamAlerts as a replay mechanism. It's not. Use the webhook delivery store for replay. StreamAlerts is for live signal.
Checklist
- [ ] Exponential backoff reconnect, capped at 30s
- [ ] HTTP/2 keepalive at 30s
- [ ] Async handler — never block the receive loop
- [ ] Dedupe on
event_idwith 7-day TTL - [ ] Subscribe only to platforms and event types you care about
- [ ] Pin a proto version in your build
Sources + Further Reading
- SettleRisk docs — gRPC service definition
- Webhooks post — webhook alternative path
- gRPC documentation — server-streaming RPCs
- HTTP/2 keepalive RFC 7540
- Designing Streaming APIs (Newman 2017)
Fund tier and above include StreamAlerts: /pricing.
Get weekly risk analysis in your inbox
Market risk scores, emerging dispute patterns, and settlement delay trends — delivered every Monday.