gRPC and SignalR solve different problems — efficient service-to-service RPC versus pushing updates to connected clients — but interviewers pair them because both break the request/response mental model most engineers default to, and both punish teams that treat them like REST with a different serializer. A candidate who has only read the getting-started docs can describe a unary call or a chat hub; a candidate who has run either in production can explain why gRPC traffic piles up on one Kubernetes pod, or why a SignalR app suddenly needs sticky sessions the moment it scales past one instance. The ten questions below cover streaming types, deadlines, HTTP/2 load balancing, SignalR scale-out and delivery guarantees.

Q1 When do you actually choose gRPC over a JSON REST API, and when is that choice a mistake?#

Short answer: Choose gRPC for internal, service-to-service calls where you control both ends, need low latency or streaming, and want a strongly typed contract shared across languages. It's usually a mistake for public-facing APIs, where REST's browser support, HTTP caching and human-readable payloads matter more than the performance gap.

gRPC's advantage comes from Protobuf's compact binary encoding and HTTP/2 multiplexing, which cuts serialization cost and per-call connection overhead compared to JSON over HTTP/1.1 — real gains for high-volume internal traffic, but easy to overstate for network-bound payloads where the wire format isn't the bottleneck. The cost is real too: browsers can't speak native gRPC at all, requiring gRPC-Web or JSON transcoding as a translation layer, and a .proto contract adds a build step and versioning discipline that a REST team without one won't have. Many production systems use both deliberately: REST or JSON transcoding at the public edge, gRPC between internal services.

FactorgRPCREST with JSON
PayloadBinary ProtobufText JSON
StreamingServer, client, bidirectionalResponse streaming or SSE only
Browser supportNeeds gRPC-Web or transcodingNative
HTTP cachingNot applicableMature

What interviewers look for: a decision driven by who calls the API (internal service vs. public client) rather than "gRPC is just faster," and awareness of the browser and caching gaps.

Common mistakes: picking gRPC for a public API and then rebuilding REST-like transcoding anyway; assuming the performance gap matters for a payload that's small or infrequent enough that it never will.

Follow-up questions:

  • How does JSON transcoding change this trade-off?
  • When would you run both a REST and a gRPC surface for the same service?
  • What does a .proto contract give you that an OpenAPI document doesn't?

Q2 Walk through gRPC's four call types with a concrete scenario for each. What changes about how you write the server and client code?#

Short answer: Unary is a normal request/response, used for queries and commands. Server streaming returns a sequence of messages over one call, suited to change feeds or progress updates. Client streaming lets a caller push many messages before getting one response, suited to batched ingestion. Bidirectional streaming allows both sides to read and write independently, suited to interactive sessions such as a live reservation flow.

Protobuf
service InventoryService {
  rpc GetStock (GetStockRequest) returns (StockLevel);                     // Unary
  rpc WatchStock (WatchStockRequest) returns (stream StockLevel);          // Server streaming
  rpc ImportAdjustments (stream StockAdjustment) returns (ImportSummary);  // Client streaming
  rpc Reserve (stream ReserveRequest) returns (stream ReserveResult);      // Bidirectional
}

The generated code shape follows the contract: a unary method returns a Task<TResponse>, a server- streaming method takes an IServerStreamWriter<TResponse>, a client-streaming method takes an IAsyncStreamReader<TRequest>, and bidirectional methods take both. The threading rule that trips people up is that a single stream reader or writer allows only one caller at a time — concurrent producers must funnel through something like a bounded Channel<T> with one writing loop — while reading and writing concurrently on a bidirectional call is fine, since they're independent directions. Always await WriteAsync: HTTP/2 flow control applies backpressure, and awaiting is how that backpressure reaches your producer instead of silently buffering unbounded memory.

What interviewers look for: the four types matched to realistic scenarios, not just recited, and the single-writer-per-stream threading rule stated without prompting.

Common mistakes: writing to a stream from two threads at once; forgetting to await WriteAsync and discovering a slow consumer causes unbounded memory growth on the producer side.

Follow-up questions:

  • How would you implement a heartbeat over a long-lived server-streaming call?
  • What happens if a bidirectional call's handler returns while a background task is still writing?
  • How do you test a client-streaming method without a live network connection?

Q3 How do gRPC deadlines and cancellation actually propagate across a call chain, and what's the most common mistake teams make with them?#

Short answer: gRPC has no default deadline, so an outbound call can hang forever unless you set one explicitly; the deadline is an absolute UTC time carried in a grpc-timeout header, and when it passes, the client fails with DeadlineExceeded and the server's context.CancellationToken fires. The most common mistake is setting a deadline on the outermost call but never passing context.CancellationToken into the database calls and downstream calls it triggers, so the server keeps doing wasted work after the caller has already given up.

C#
var level = await client.GetStockAsync(
    new GetStockRequest { Sku = sku },
    deadline: DateTime.UtcNow.AddSeconds(2),
    cancellationToken: cancellationToken);

In a call chain, a downstream service should inherit whatever remains of the caller's budget rather than starting a fresh deadline of its own; EnableCallContextPropagation on the client factory flows the deadline and cancellation token into outbound calls automatically, and if a child call sets a shorter deadline of its own, the smaller one wins. Because DeadlineExceeded only means the caller stopped waiting, not that the server rolled back its work, operations triggered by a call that might time out need to be safe to retry or already committed — idempotency is what makes a deadline safe rather than merely fast.

What interviewers look for: the absolute-UTC-time detail, context.CancellationToken propagated into every downstream I/O call, and idempotency named as the reason deadlines don't imply safe retries.

Common mistakes: setting a deadline without threading the resulting cancellation token into database or downstream calls; assuming DeadlineExceeded means the server's work was rolled back.

Follow-up questions:

  • What happens if a downstream call sets a longer deadline than the budget it inherited?
  • How would you distinguish a client that cancelled from one that hit its deadline?
  • Why is EnableCallContextPropagation dangerous to enable without also setting a deadline upstream?

Q4 Why does gRPC traffic sometimes pile onto a single instance in Kubernetes even though a load balancer sits in front of the service, and how do you fix it?#

Short answer: A standard Kubernetes service and most layer-4 load balancers balance TCP connections, not individual calls, and a gRPC client multiplexes every call over one long-lived HTTP/2 connection — so once that connection is established, all of its traffic lands on whichever pod it connected to, and newly scaled pods sit idle until existing connections happen to churn.

There are two real fixes. The first is a layer-7 proxy that understands HTTP/2 and balances at the call level — a service mesh like Istio or Linkerd, Envoy, or a YARP gateway — at the cost of an extra hop. The second is client-side load balancing, built into the managed gRPC client since version 2.45.0: point it at a headless Kubernetes service, which returns one DNS record per ready pod, resolve it with the dns:/// scheme, and configure RoundRobinConfig instead of the default pick_first policy, which otherwise sends everything to one address.

C#
builder.Services.AddGrpcClient<InventoryService.InventoryServiceClient>(o =>
        o.Address = new Uri("dns:///inventory-headless.shop.svc.cluster.local:8080"))
    .ConfigureChannel(o => o.ServiceConfig = new ServiceConfig
    {
        LoadBalancingConfigs = { new RoundRobinConfig() }
    });

What interviewers look for: the connection-vs-call distinction stated precisely, and both fixes named — not just "add a service mesh" without the client-side alternative.

Common mistakes: assuming a Kubernetes service load-balances gRPC calls the way it does HTTP/1.1 requests; enabling client-side balancing but leaving the default pick_first policy in place.

Follow-up questions:

  • What is EnableMultipleHttp2Connections for, and when would you need it alongside load balancing?
  • How does a headless service's DNS resolution interact with pod restarts?
  • What are the trade-offs of an L7 proxy versus client-side balancing for this problem?

Q5 How does SignalR scale out across multiple server instances, and what's the practical difference between the Azure SignalR Service and a Redis backplane?#

Short answer: A single SignalR server only knows about the connections it holds directly, so scaling to more than one instance needs a way to fan a message out to clients connected to other instances. A Redis backplane does this with publish/subscribe: every server publishes outbound messages to Redis, and every server relays matching messages to its own connected clients. The Azure SignalR Service instead acts as a proxy in front of your app, holding every client connection itself so your servers only need a small, constant number of connections to the service.

The practical trade-offs run in opposite directions. With a Redis backplane, your app still holds every client connection directly, so it must scale out based on connection count even when message volume is low, and it still needs sticky sessions except in a narrow WebSockets-only configuration. The Azure SignalR Service removes both constraints — sticky sessions aren't needed because clients are redirected to the service on connect, and your app scales based on message throughput, not connection count — at the cost of depending on an external managed service and its network path. Other backplane providers exist, including SQL Server, Orleans and NCache, for teams that can't add Redis but already run one of those.

What interviewers look for: the fan-out mechanism explained specifically (pub/sub vs. proxy), and the sticky-session and scaling-trigger trade-offs stated as consequences of that mechanism, not memorized facts.

Common mistakes: assuming a Redis backplane removes the need for sticky sessions, the way the Azure service does; not accounting for connection-count-driven scaling when sizing a self-hosted deployment.

Follow-up questions:

  • Why does the Azure SignalR Service change what metric you scale on?
  • What happens to in-flight messages if the Redis backplane itself becomes unavailable?
  • When would a team choose a Redis backplane over the Azure SignalR Service on purpose?

Q6 Why does SignalR require sticky sessions when scaled out, and in what specific situations can you avoid them?#

Short answer: SignalR requires that the same server process handle every HTTP request for a given connection, because negotiation, transport upgrade and the connection's in-memory state all happen on one process; without sticky sessions, a server farm can route a client's follow-up requests to a different instance than the one that accepted the connection, which breaks it.

There are exactly three situations where sticky sessions aren't required: a single server in a single process, where there's nothing to route between; the Azure SignalR Service, which enables affinity at the service layer rather than the app layer; and clients configured to use WebSockets exclusively with SkipNegotiation enabled on the client, which removes the initial negotiation request that sticky sessions would otherwise need to route consistently. Every other scenario, including one using a Redis backplane, still needs affinity configured at the load balancer — Application Request Routing affinity on Azure App Service, ip_hash or a cookie-based sticky directive on Nginx, or the equivalent on any other proxy in front of the app.

What interviewers look for: the underlying reason (connection state lives on one process) rather than a memorized rule, and all three exceptions named accurately, including the WebSockets-plus-SkipNegotiation combination most people forget.

Common mistakes: believing a message backplane by itself removes the need for sticky sessions; forgetting that SkipNegotiation alone isn't enough — clients must also be WebSockets-only.

Follow-up questions:

  • What actually breaks, mechanically, if a request lands on the wrong server mid-connection?
  • Why doesn't a Redis backplane make sticky sessions unnecessary the way the Azure service does?
  • How would you configure sticky sessions for a SignalR app behind Nginx?

Q7 What does SignalR actually guarantee about message delivery, and how do you build something reliable on top of a system that, by default, doesn't guarantee it?#

Short answer: By default, SignalR gives no delivery guarantee at all: a hub method call is fire-and- forget from the transport's perspective, and a message in flight when a connection drops is simply lost, with no automatic retry or acknowledgment. .NET 8's opt-in stateful reconnect narrows that gap for brief interruptions by buffering unacknowledged messages on both sides and replaying them once the connection resumes, but it's still bounded, not a durability guarantee.

C#
app.MapHub<TradeHub>("/trades", options => options.AllowStatefulReconnects = true);
builder.Services.AddSignalR(o => o.StatefulReconnectBufferSize = 100_000); // bytes, the default

Stateful reconnect acknowledges messages between client and server and replays anything sent while the connection was briefly down, and unlike a plain reconnect, the client resumes with the same connection ID instead of negotiating a new one. It only helps within the configured buffer window, though: an outage longer than the client can buffer, or a buffer that fills faster than it drains, still loses messages. For anything that must not be lost — an order confirmation, a payment status — put a durable, ordered store behind SignalR: write the event first, then push it, and have the client reconcile its view against a "fetch what I missed since sequence N" API on reconnect rather than trusting the push channel alone.

What interviewers look for: "no guarantee by default" stated plainly, stateful reconnect correctly scoped as reducing loss during brief blips rather than eliminating it, and a concrete reconciliation pattern for anything that truly can't be lost.

Common mistakes: treating SignalR as reliable messaging out of the box; assuming stateful reconnect is a durability guarantee rather than a bounded buffer for short interruptions.

Follow-up questions:

  • What happens if the stateful reconnect buffer fills before the connection resumes?
  • How would a client know it missed messages that fell outside the buffer window?
  • Where would you put a message queue in this design, and what would it change?

Q8 Walk through what happens, step by step, when a SignalR .NET client's connection drops and reconnects with WithAutomaticReconnect(). What can silently go wrong for the application?#

Short answer: The client isn't automatically resilient by default — WithAutomaticReconnect() must be opted into. Once enabled, a dropped connection moves the HubConnection to Reconnecting, firing that event so the app can warn the user or disable input; the client then retries after the configured delays, 0, 2, 10 and 30 seconds by default, stopping after four failed attempts and moving to Disconnected if none succeed.

C#
connection.Reconnecting += error => { /* warn the user, disable send buttons */ return Task.CompletedTask; };
connection.Reconnected += connectionId => { /* dequeue anything the app buffered while offline */ return Task.CompletedTask; };

The detail that silently breaks applications: unless stateful reconnect is also enabled, a successful automatic reconnect gets a brand-new ConnectionId, because the connection looks entirely new to the server. Any server-side state keyed by the old connection ID — group membership, a per-connection cache entry, an in-flight operation tracked by connection — is now orphaned, and the app must re-join groups and re-establish that state in the Reconnected handler rather than assuming continuity. A custom IRetryPolicy can replace the fixed delay list with one driven by PreviousRetryCount and ElapsedTime for backoff tuned to the app's tolerance for staleness.

What interviewers look for: automatic reconnect named as opt-in, not default, and the new-connection- ID consequence for server-side group membership stated as the main operational gotcha.

Common mistakes: assuming reconnection is automatic without calling WithAutomaticReconnect(); forgetting to re-join SignalR groups in the Reconnected handler after a plain reconnect.

Follow-up questions:

  • How does stateful reconnect change what happens to the connection ID?
  • What would you do differently for a client that must not silently give up after four attempts?
  • How would a server detect that a "new" connection is actually the same user reconnecting?

Q9 You're building a real-time trading dashboard that must broadcast price ticks to thousands of concurrently connected browsers. Would you reach for SignalR, gRPC streaming, or something else? Justify it.#

Short answer: SignalR, because the requirement is browser clients and one-to-many broadcast, which is exactly what SignalR's transport negotiation (WebSockets with graceful fallback) and group broadcast model are built for. gRPC server streaming could deliver the same ticks technically, but browsers can't consume native gRPC at all, so you'd need gRPC-Web as a translation layer to get something SignalR already does natively.

The scale question then becomes an infrastructure decision, not a SignalR-versus-gRPC one: at "thousands" of connections, a single instance's connection and memory limits push you toward scaling out, which means choosing between a Redis backplane (cheaper, but sticky sessions and connection-count-driven scaling) and the Azure SignalR Service (no sticky sessions, scales on message volume, external dependency). For pure price ticks, group broadcast — putting every subscriber to a symbol in a SignalR group — avoids sending symbol-specific updates to clients that never asked for them. If the same platform also needs a high-throughput, service-to-service feed of the same ticks into other backend systems, that's the point where gRPC server streaming becomes the right tool for that specific, non-browser leg, run alongside SignalR rather than instead of it.

What interviewers look for: SignalR chosen for the browser requirement specifically, not gRPC's throughput used to justify a wrong-tool choice; the scale-out decision separated cleanly from the initial protocol choice; willingness to use both technologies for their respective legs of the same system.

Common mistakes: picking gRPC because it's "faster" without accounting for browsers not speaking it natively; not considering SignalR groups and broadcasting every tick to every connected client regardless of what they subscribed to.

Follow-up questions:

  • How would you keep a slow consumer from blocking broadcast to every other subscriber?
  • What would push you toward the Azure SignalR Service over a self-hosted Redis backplane here?
  • How would you replay missed ticks to a client that reconnects after a gap?

Q10 A gRPC service and a SignalR hub both need to report an error to their caller. How does error handling differ between the two, and what does that mean for client code?#

Short answer: gRPC has a structured status model: a service throws RpcException with a specific StatusCode such as NotFound or FailedPrecondition, which travels in HTTP trailers alongside an optional message and, through Grpc.StatusProto, typed detail objects the client can deserialize and act on programmatically. SignalR has no equivalent status vocabulary; a hub method that throws sends a generic error to the caller by default, and anything more specific has to be modeled explicitly by the app, either as a typed message the client method understands or as a separate invocation the client is built to expect.

C#
// gRPC: the client can branch on a specific, protocol-level status code.
catch (RpcException ex) when (ex.StatusCode == StatusCode.FailedPrecondition) { /* retry after state changes */ }

That difference changes how defensively client code has to be written. A gRPC client can safely branch on StatusCode because the protocol guarantees the vocabulary is consistent across every service that follows it; a SignalR client has no such guarantee and must trust an application-level contract the team defined itself, which means that contract needs the same versioning discipline as any other API surface, or older clients silently stop understanding new error shapes. A senior answer also flags the trap of trusting HTTP status codes for gRPC: because gRPC usually returns HTTP 200 with the real outcome in a trailer, any proxy, dashboard or generic retry middleware that only reads the HTTP status will treat every failed gRPC call as a success.

What interviewers look for: the structured-status-versus-app-defined-contract distinction stated clearly, and the HTTP-200-hides-gRPC-failures trap named as a concrete operational risk.

Common mistakes: building generic HTTP retry or alerting logic that reads only the HTTP status code of a gRPC call; assuming a SignalR error message has a stable, versioned shape without the team having defined one.

Follow-up questions:

  • How would you version an application-level error contract on a SignalR hub over time?
  • What does EnableDetailedErrors change about what a gRPC client can see, and why is it dev-only?
  • How would you build alerting that correctly detects failed gRPC calls behind a proxy?

Quick-Fire Round#

QuestionAnswer
Default gRPC deadline if you set none?None — the call can wait forever.
Where does a gRPC call's real status usually travel?HTTP trailers (grpc-status), not the HTTP status code.
Why does gRPC traffic pile onto one pod?A load balancer balances the connection, not each multiplexed call.
What removes the need for a SignalR backplane?Nothing — multiple instances always need one to fan out messages.
Does a Redis backplane remove the need for sticky sessions?No; only the Azure SignalR Service or a single instance do.
Default SignalR delivery guarantee?None; a message in flight when the connection drops is lost.
Does a plain automatic reconnect keep the same connection ID?No — only stateful reconnect does.
What is EnableMultipleHttp2Connections for?Exceeding the ~100-stream limit on one HTTP/2 connection.

How to Prepare#

  • Be ready to justify gRPC versus REST by who calls the API, not by a blanket performance claim.
  • Know all four gRPC call types with a realistic scenario each, and the single-writer-per-stream rule.
  • Explain deadline propagation end to end, including why it requires threading the cancellation token into every downstream call, not just setting a deadline on the outer call.
  • Be able to state precisely why gRPC clients concentrate on one pod, and both fixes for it.
  • Know the mechanism difference between a Redis backplane and the Azure SignalR Service, and how each changes the sticky-session and scaling story.
  • Have a precise, unhedged answer for what SignalR guarantees about delivery — by default, nothing.