Backend Development 26 min read

Envoy Outbound Request Flow: Source‑Code Analysis of Receiving, Sending, and Responding

This article provides a detailed source‑code walkthrough of Envoy's outbound request processing, explaining how the proxy receives client data, parses HTTP requests, selects routes and clusters, forwards traffic upstream, handles retries and shadowing, and finally returns responses to downstream clients.

Cloud Native Technology Community
Cloud Native Technology Community
Cloud Native Technology Community
Envoy Outbound Request Flow: Source‑Code Analysis of Receiving, Sending, and Responding

Envoy is a high‑performance network proxy built for service mesh, running as a sidecar alongside applications to abstract networking functions such as routing, traffic control, and observability.

This third part of the series dives into the outbound request path at the source‑code level, covering request reception, request transmission, response reception, and response return.

Receiving a request – The client writes data to the socket; the event loop triggers transport_socket_.doRead , which reads into read_buffer_ until EAGAIN . ConnectionImpl::onReadReady processes the buffered data, handling half‑close semantics, dispatching reads, and closing the socket on remote close.

void ConnectionImpl::onReadReady() {
  ENVOY_CONN_LOG(trace, "read ready. dispatch_buffered_data={}", *this, dispatch_buffered_data_);
  const bool latched_dispatch_buffered_data = dispatch_buffered_data_;
  dispatch_buffered_data_ = false;
  ASSERT(!connecting_);
  // ... read loop and error handling ...
  IoResult result = transport_socket_->doRead(*read_buffer_);
  // ... update stats, handle end‑stream, close if needed ...
}

The buffered data is then passed to Envoy::Http::ConnectionManagerImpl::onData , which creates the HTTP codec (HTTP/1, HTTP/2, or HTTP/3) if necessary and dispatches the data to the parser.

Network::FilterStatus ConnectionManagerImpl::onData(Buffer::Instance& data, bool) {
  if (!codec_) { createCodec(data); }
  bool redispatch;
  do {
    redispatch = false;
    const Status status = codec_->dispatch(data);
    if (isBufferFloodError(status) || isInboundFramesWithEmptyPayloadError(status)) {
      handleCodecError(status.message());
      return Network::FilterStatus::StopIteration;
    } else if (isCodecProtocolError(status)) {
      stats_.named_.downstream_cx_protocol_error_.inc();
      handleCodecError(status.message());
      return Network::FilterStatus::StopIteration;
    }
    ASSERT(status.ok());
  } while (redispatch);
  return Network::FilterStatus::Continue;
}

The HTTP parser (originally http_parser , now moving to llhttp ) invokes callbacks such as onMessageBegin , onUrl , onHeaderField , onHeaderValue , and onHeadersComplete to build the request header map.

http_parser_settings ConnectionImpl::settings_{
  [](http_parser* parser) -> int { static_cast
(parser->data)->onMessageBeginBase(); return 0; },
  [](http_parser* parser, const char* at, size_t length) -> int { static_cast
(parser->data)->onUrl(at, length); return 0; },
  nullptr, // on_status
  [](http_parser* parser, const char* at, size_t length) -> int { static_cast
(parser->data)->onHeaderField(at, length); return 0; },
  // ... other callbacks ...
};

After headers are parsed, Envoy creates an ActiveStream , records SNI for TLS, and runs the filter chain (e.g., CORS, fault, router). The router builds a RouteMatcher that selects a virtual host based on the Host header, handling exact, suffix‑wildcard, prefix‑wildcard, and default virtual hosts.

const VirtualHostImpl* RouteMatcher::findVirtualHost(const Http::RequestHeaderMap& headers) const {
  if (virtual_hosts_.empty() && wildcard_virtual_host_suffixes_.empty() && wildcard_virtual_host_prefixes_.empty()) {
    return default_virtual_host_.get();
  }
  const std::string host = absl::AsciiStrToLower(headers.getHostValue());
  auto iter = virtual_hosts_.find(host);
  if (iter != virtual_hosts_.end()) return iter->second.get();
  // ... wildcard lookup ...
  return default_virtual_host_.get();
}

Once a route is resolved, Envoy determines the upstream cluster, creates or reuses a connection pool, and selects a host via the load‑balancer. The request is encoded, optional tracing headers are added, and the request buffer is written to the upstream socket.

If no idle connection exists, Envoy checks max_pending_requests and max_connections limits, creates a new client connection, and queues the request as a PendingRequest . Upon successful connection, the pending request is attached to the new client.

Responses from upstream are read by the event loop, parsed by the same HTTP parser, and passed to UpstreamRequest::decodeHeaders . Envoy updates outlier detection statistics based on the response code, handles retries, internal redirects, and shadow traffic as configured.

Finally, Envoy stops the request timer, resets the idle timer, and sends the response downstream through the same filter chain, completing the outbound request cycle.

The article concludes with a summary of key take‑aways: Envoy heavily uses inheritance, templates, and composition; understanding the log order helps trace the request flow; and a combination of logs, packet captures, and selective source reading resolves most issues.

Proxycroutingservice meshhttpEnvoySource Code
Cloud Native Technology Community
Written by

Cloud Native Technology Community

The Cloud Native Technology Community, part of the CNBPA Cloud Native Technology Practice Alliance, focuses on evangelizing cutting‑edge cloud‑native technologies and practical implementations. It shares in‑depth content, case studies, and event/meetup information on containers, Kubernetes, DevOps, Service Mesh, and other cloud‑native tech, along with updates from the CNBPA alliance.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.