📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

Explain HTTP from a backend engineer's point of view: methods and idempotency, status codes, important headers, and the practical differences between HTTP/1.1, HTTP/2 and HTTP/3.

HTTP semantics for API design and operations: safe and idempotent methods, status code classes, caching and content headers, and the evolution from HTTP/1.1 to HTTP/2 and HTTP/3.

Mid-Level Theory Intermediate aspnet-core http http2 http3 networking rest

🎯 Key points

  • Safe methods (GET, HEAD, OPTIONS) do not change state; idempotent methods (GET, HEAD, OPTIONS, PUT, DELETE) have the same effect when repeated; POST is not idempotent.
  • PATCH is not guaranteed to be idempotent (RFC 5789): relative changes such as 'add 1 to quantity' or a JSON Patch 'add' to an array apply twice, while a patch that sets absolute values or is guarded by If-Match can be designed to be idempotent.
  • Idempotency is what makes retries safe; for POST operations clients will retry, use an Idempotency-Key header, store the response, and enforce a unique constraint so concurrent duplicates return the original result instead of a 500.
  • Status classes: 2xx success, 3xx redirect or 304 Not Modified, 4xx client errors (401 unauthenticated versus 403 forbidden, 404, 409, 412, 422, 429 with Retry-After), 5xx server errors, with 502, 503 and 504 usually from proxies or gateways.
  • Cache-Control, ETag with If-None-Match (304) and If-Match (412) give caching and optimistic concurrency; Vary tells caches which request headers change the response.
  • HTTP/1.1 is text with one request in flight per connection, so clients open several connections per host and suffer HTTP-level head-of-line blocking.
  • HTTP/2 uses binary frames, multiplexed streams on one TCP connection and HPACK header compression; gRPC needs it, a single lost packet stalls every stream (TCP head-of-line blocking), and long-lived connections can pin traffic to one backend behind an L4 load balancer.
  • HTTP/3 keeps the same semantics on QUIC over UDP with QPACK, recovers loss per stream, sets up faster and survives network changes through connection migration; it helps most for mobile and lossy networks and needs UDP 443 plus an HTTP/2 fallback.

⏱ 30-second answer

HTTP is a request/response protocol: method + path + headers (+ body) in, status + headers (+ body) out.

  • Methods: GET/HEAD/OPTIONS are safe; GET, PUT, DELETE are idempotent (repeating has the same effect); POST and usually PATCH are not, so retries need an idempotency key.
  • Status codes: 2xx success, 3xx redirect/not modified, 4xx client error (400, 401 vs 403, 404, 409, 422, 429), 5xx server error (500, and 502/503/504 usually from proxies or gateways).
  • Headers: Content-Type, Accept, Authorization, Cache-Control, ETag/If-None-Match, Retry-After, traceparent.
  • Versions: HTTP/1.1 is text with one request at a time per connection; HTTP/2 is binary with multiplexed streams and header compression on one TCP connection; HTTP/3 does the same over QUIC/UDP, removing TCP head-of-line blocking.

🗣 Full answer (senior)

Methods

Method Safe Idempotent Typical use
GET yes yes Read a resource
HEAD yes yes Headers only (existence, size)
OPTIONS yes yes CORS preflight, capabilities
PUT no yes Replace a resource at a known URI
DELETE no yes Remove a resource
POST no no Create, or run a command
PATCH no not guaranteed Partial update

Idempotency is what makes retries safe. Retrying a timed-out POST /payments can charge twice unless the server deduplicates with an Idempotency-Key header.

Status codes that matter in practice

Code Meaning Note
200 / 201 / 204 OK / Created (+ Location) / No Content
304 Not Modified Response to If-None-Match with matching ETag
400 / 422 Bad request / validation failed Return ProblemDetails
401 Not authenticated Missing or invalid credentials
403 Authenticated but not allowed
404 / 409 Not found / Conflict (e.g. concurrency)
429 Too many requests Include Retry-After
500 Unhandled server error
502 / 503 / 504 Bad gateway / unavailable / gateway timeout Usually from a load balancer or proxy

Versions

HTTP/1.1 HTTP/2 HTTP/3
Format Text Binary frames Binary frames
Transport TCP TCP (+TLS in practice) QUIC over UDP (TLS 1.3 built in)
Concurrency One request at a time per connection; clients open ~6 per host Many streams on one connection Many streams, independent loss recovery
Header compression No HPACK QPACK
HOL blocking At HTTP level At TCP level Removed (per stream)

gRPC requires HTTP/2 (or HTTP/3).

Production example

A mobile client retried POST /orders on timeouts. When the API was slow, some customers got two orders. We added an Idempotency-Key header generated by the client per checkout, stored keys with the response for 24 hours, and returned the stored response on repeats. Duplicate orders stopped, and we also returned 429 with Retry-After during overload so clients backed off instead of hammering.

🔍 What the interviewer is checking

Checks practical HTTP knowledge that shapes API design and production behaviour.

Green flags

  • Knows safe vs idempotent and why it matters for retries.
  • Uses status codes correctly (401 vs 403, 409, 429, 502/503/504).
  • Mentions caching and conditional headers.
  • Explains multiplexing and head-of-line blocking across versions.

Red flags

  • Says PUT and POST are interchangeable.
  • Returns 200 for errors.
  • Cannot explain what HTTP/2 changed.

📚 Learn in depth

1. Anatomy of a request and response

GET /orders/42 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJ...
If-None-Match: "v7"
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

HTTP/1.1 304 Not Modified
ETag: "v7"
Cache-Control: private, max-age=60

2. Caching and conditional requests

  • Cache-Control: public, max-age=300 lets CDNs and browsers cache; private limits to the client; no-store forbids caching.
  • ETag + If-None-Match lets a client revalidate cheaply (304, no body).
  • ETag + If-Match on PUT/PATCH gives optimistic concurrency: 412 Precondition Failed if someone else changed it.
  • Vary: Accept-Encoding, Authorization tells caches which request headers change the response.

3. HTTP/1.1 limits

connection 1: [req A ───── resp A][req B ── resp B]
connection 2: [req C ─── resp C]
... browsers open ~6 per host; slow responses block the rest

Pipelining exists but is effectively unused because responses must return in order.

4. HTTP/2

one TCP connection
├─ stream 1: HEADERS GET /a  ... DATA
├─ stream 3: HEADERS GET /b  ... DATA     frames interleaved
└─ stream 5: HEADERS POST /c ... DATA
  • Multiplexing removes the need for many connections; HPACK shrinks repeated headers (cookies, auth).
  • In .NET, HttpClient uses HTTP/2 when the server negotiates it via ALPN over TLS and you set Version = 2.0 (default request version is 1.1). SocketsHttpHandler.EnableMultipleHttp2Connections opens more connections when stream limits are reached.
  • Long-lived HTTP/2 connections can unbalance L4 load balancers (all streams go to one backend).

5. HTTP/3

Same semantics, carried on QUIC. Faster connection setup, better on lossy mobile networks, connection migration. Requires UDP 443 open; negotiated via Alt-Svc.

6. Semantics are the contract

Proxies, caches, SDKs and retry policies all rely on method and status semantics. Returning 200 with {"error": ...} breaks retries, monitoring and caching. Use correct codes and ProblemDetails (RFC 9457) bodies.

7. Try it yourself

  1. curl -v https://example.com and read every header.
  2. curl -sI --http2 https://example.com vs --http1.1.
  3. Add app.UseOutputCache() or ETag support to an endpoint and test If-None-Match with curl.
  4. Simulate a retry of POST with and without an idempotency key and count created rows.

💻 Code

// Idempotent POST using an Idempotency-Key header.
// The unique key in IdempotencyRecords is the real guard: if two retries race,
// the second insert fails and we return the response the first one stored.
app.MapPost("/orders", async (
    [FromHeader(Name = "Idempotency-Key")] string? key,
    CreateOrder cmd, OrdersDb db, CancellationToken ct) =>
{
    if (string.IsNullOrWhiteSpace(key))
        return Results.Problem("Idempotency-Key header is required", statusCode: 400);

    var existing = await db.IdempotencyRecords.AsNoTracking()
        .SingleOrDefaultAsync(r => r.Key == key, ct);
    if (existing is not null)
        return Results.Json(existing.ResponseBody, statusCode: existing.StatusCode);

    var order = Order.Create(cmd);
    db.Orders.Add(order);
    db.IdempotencyRecords.Add(new IdempotencyRecord(key, 201, order.ToDto()));

    try
    {
        // Order and key are saved in one transaction: both or neither.
        await db.SaveChangesAsync(ct);
    }
    catch (DbUpdateException ex) when (ex.IsUniqueViolation())
    {
        // A concurrent request with the same key won the race.
        db.ChangeTracker.Clear();
        var winner = await db.IdempotencyRecords.AsNoTracking()
            .SingleAsync(r => r.Key == key, ct);
        return Results.Json(winner.ResponseBody, statusCode: winner.StatusCode);
    }

    return Results.Created($"/orders/{order.Id}", order.ToDto());
});

// IsUniqueViolation is a small extension: for SQL Server it checks
// SqlException.Number 2627 or 2601; for PostgreSQL, SqlState "23505".
// Overload protection: the built-in rate limiter returns 429 with Retry-After
builder.Services.AddRateLimiter(o =>
{
    o.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    o.AddFixedWindowLimiter("reports", w => { w.PermitLimit = 10; w.Window = TimeSpan.FromSeconds(30); });
    o.OnRejected = (ctx, _) =>
    {
        if (ctx.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
            ctx.HttpContext.Response.Headers.RetryAfter = ((int)retryAfter.TotalSeconds).ToString();
        return ValueTask.CompletedTask;
    };
});

app.UseRateLimiter();
app.MapGet("/reports", GetReports).RequireRateLimiting("reports");
// Client: prefer HTTP/2 to an internal API, fall back to HTTP/1.1 if not negotiated
using var http = new HttpClient(new SocketsHttpHandler
{
    EnableMultipleHttp2Connections = true,
    PooledConnectionLifetime = TimeSpan.FromMinutes(5) // pick up DNS and scale-out changes
})
{
    BaseAddress = new Uri("https://orders.internal/"),
    DefaultRequestVersion = HttpVersion.Version20,
    DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower
};

⚠ Common mistakes

Mistake: Retrying POST automatically without idempotency. Why it is wrong: A timeout does not mean the server did nothing, so a retry can create a second order or charge. Instead: Retry only idempotent methods by default, and require an idempotency key for POST operations that must be retried.

Mistake: Assuming every PATCH is safe to retry. Why it is wrong: Relative updates such as "increment stock" or appending to an array apply twice on retry. Instead: Design patches to set absolute values, guard them with If-Match and an ETag, or treat them like POST with an idempotency key.

Mistake: Returning 200 with an error payload. Why it is wrong: Monitoring, caches, SDKs and retry policies all treat it as success. Instead: Use correct status codes with ProblemDetails (RFC 9457) bodies.

Mistake: Mixing up 401 and 403. Why it is wrong: Clients react differently: 401 triggers re-authentication, 403 means stop. Instead: Return 401 for missing or invalid credentials and 403 for insufficient permissions.

Mistake: Thinking HTTP/2 always makes things faster. Why it is wrong: All streams share one TCP connection, so packet loss stalls them all, and long-lived connections can send all traffic to one backend. Instead: Measure, and use L7 load balancing or connection limits for gRPC and HTTP/2.

🏛 Senior insight: trade-offs

HTTP semantics are a contract with every intermediary: CDNs cache by method and Cache-Control, retry policies rely on idempotency, load balancers report 5xx, and SDKs map status codes to exceptions. Respecting them makes resilience and caching almost free.

Design principle Example Benefit
Idempotent writes PUT /orders/42, or POST with Idempotency-Key Safe client and gateway retries
Optimistic concurrency ETag plus If-Match, 412 on conflict No lost updates without locks
Conditional reads If-None-Match, 304 Less bandwidth and server work
Honest status codes 429 with Retry-After, 503 during maintenance Clients back off instead of hammering
Standard error bodies ProblemDetails Consistent client handling and logs
Version Best fit Watch out for
HTTP/1.1 Simple clients, legacy proxies Connection count, no multiplexing
HTTP/2 Service-to-service, gRPC L4 balancing pins connections
HTTP/3 Mobile apps, global users on lossy networks (CDNs such as Cloudflare and Azure Front Door support it) UDP 443 blocked, msquic on Linux

Underneath, the bigger wins usually come from connection reuse, sensible timeouts, conditional requests and idempotent design.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says HTTP/3 uses UDP Why: Checks TCP vs UDP and QUIC
    How to answer
    • TCP is connection-oriented and gives a reliable, ordered byte stream: handshake, acknowledgements, retransmission, flow control and congestion control. Cost: a setup round trip and head-of-line blocking (one lost packet stalls everything behind it).
    • UDP sends independent datagrams with no connection, no ordering and no retransmission. It is lightweight and low-latency, and the application decides what to do about loss.

    Use TCP for most APIs, databases and file transfer. Use UDP for DNS, real-time media, games, metrics (StatsD) and as the base for QUIC. HTTP/3 runs on QUIC over UDP so it can combine TLS 1.3 with the transport handshake, avoid TCP's head-of-line blocking between streams, and survive network changes.

  2. When: Candidate mentions 502/503/504 from proxies Why: Probes reverse proxy behaviour
    How to answer
    • A forward proxy sits in front of clients (corporate egress proxy): clients send requests through it to the internet. For HTTPS it tunnels with CONNECT. HttpClient picks it up from system settings or HTTP_PROXY/HTTPS_PROXY/NO_PROXY.
    • A reverse proxy sits in front of servers (nginx, Envoy, YARP, App Gateway, ingress): it terminates TLS, routes, load balances, caches and adds headers.

    Behind a reverse proxy, the app sees the proxy's IP and often plain http. The proxy passes the originals in X-Forwarded-For and X-Forwarded-Proto, but ASP.NET Core only applies them via the ForwardedHeaders middleware, and by default only trusts proxies on loopback. So UseHttpsRedirection sees http, redirects to https, the proxy forwards as http again: a loop.

    Fix: UseForwardedHeaders first, configured with the proxy's IPs or network in KnownProxies/known networks. Do not trust all sources, or clients can spoof their IP and scheme.

  3. When: Candidate mentions HTTP/2 long-lived connections Why: Moves to load balancing and connection pinning
    How to answer

    A load balancer spreads traffic across healthy instances.

    • Layer 4 (TCP/UDP: Azure Load Balancer, AWS NLB, Kubernetes Services via kube-proxy) picks a backend per connection using IPs and ports. Fast, protocol-agnostic, no HTTP awareness.
    • Layer 7 (HTTP: App Gateway, ALB, Front Door, ingress controllers, Envoy, YARP) picks per request, can route by path/host/header, terminate TLS, retry and add headers.

    Health checks remove unhealthy instances (in Kubernetes: readiness for traffic, liveness for restarts). Algorithms: round robin, least connections/requests, hashing. Sticky sessions pin a user to one instance, which hurts scaling and failover. Connection draining stops new traffic to an instance before shutdown so in-flight requests finish.

    gRPC uses long-lived HTTP/2 connections, so an L4 balancer balances connections, not calls: one client connection carries all its calls to one pod. Use L7 balancing (mesh/Envoy) or client-side load balancing.

🛠 Real-world problems: how you handle them

Duplicate orders from client retries of POST
Situation
[Mobile ordering app] calling an ASP.NET Core [10] API; the client retried on timeout after [10 s].
Signals
Support tickets for double charges during a [Friday peak]; database showed pairs of identical orders created seconds apart.
Investigation
Correlated client retry logs with server logs: the first request completed after [12 s], the retry created a second order.
Resolution
Added a required `Idempotency-Key` per checkout, stored with the response and a unique constraint; repeats return the original response. Added 429 with Retry-After under overload.
Prevention
API guidelines: non-idempotent endpoints that clients retry must accept idempotency keys; contract tests for retries.

How to tell it (STAR)

**S:** During a [Friday peak], customers of our [ordering app] were charged twice. **T:** I owned the orders API and had to stop duplicates. **A:** Logs showed the app retried a slow `POST`, and because POST is not idempotent, the retry created a second order. I added an idempotency key per checkout, stored with the result under a unique constraint, and returned 429 with Retry-After when overloaded. **R:** Duplicate orders went to [zero] and refunds for that issue stopped.

↩ Usually asked after

More in Networking Basics

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me