The Complete Journey of a Request Before It Reaches Your Backend
Arya Pathak
Backend Dev ? Open Source ? Compilers ? .Net ? Go ? Angular ? Azure ? AWS ? Cybersecurity ? Ethereum???VIT?Pune '26
In this blog, we will explore the detailed path of a request from the moment it leaves the client (front-end or another service) until it enters your backend application’s user space. We often focus on the final arrival of an HTTP request handler (like an http.HandlerFunc in Go or the “on request” event in Node.js), but beneath that, there is a wealth of complexity.
Why should you care about these hidden steps? Because understanding them lets you troubleshoot performance issues, handle security concerns better, and scale more wisely. Too many engineers simply add more servers when facing bottlenecks—sometimes prematurely—without pinpointing the real cause (like slow acceptance, TLS overhead, partial reading, or inefficient parsing).
Below, we detail each step:
By the time your app sees a “request object,” countless steps have quietly happened under the hood. Let’s take a deep dive.
A Quick Note on What “Request” Really Means
In networking terms, a “request” is a unit of work or an operation that a client or frontend (which could be a web browser, a mobile app, or another microservice) sends to your server. This request is typically governed by a protocol that defines how to start and end a request, what headers or metadata exist, and how the body is structured.
The most common protocol is HTTP (in flavors like HTTP/1.1, HTTP/2, HTTP/3 via QUIC, etc.). But you can define your own. For instance:
All these differences matter because they influence how you parse, decode, and handle data. Even if you never write your own protocol, understanding how existing ones (like HTTP) work helps you troubleshoot real-world issues.
Step 1: Accept
1.1 The Kernel’s Role in Accepting Connections
Before your backend process “knows” about a connection, the OS kernel does several things:
When a client sends a SYN packet to your server, the kernel places it in the SYN Queue, replies with SYN/ACK, and waits for the client’s ACK. If the client acknowledges, that connection moves to the Accept Queue.
1.2 Accepting in the Application
In Go, the code to listen and accept connections looks deceptively simple:
Notice:
1.3 Tuning the Backlog
When you call ln, _ := net.Listen("tcp", ":8080"), you can (in lower-level APIs) specify a “backlog” parameter. This controls how many connections can accumulate in the Accept Queue. If the backlog is exceeded, new SYNs might be dropped or delayed. On some systems, you can tune it via:
(A typical default might be 128 or 256, which can be too small if you experience heavy bursts.)
1.4 Multiple Acceptors and Scaling
You can accept connections in multiple goroutines or even multiple processes. In Linux, you might do:
This addresses scenarios with extremely high connection rates, but it also complicates concurrency.
Step 2: Read
2.1 TCP Streams and Kernel Buffers
Once a connection is established, the client can start sending data. This data arrives at your server’s network interface and is moved into the kernel’s receive queue for that specific connection.
2.2 Copy to User Space
Your Go code eventually calls something like:
When conn.Read(buffer) executes, it copies data from the kernel receive buffer into your user-space buffer (buffer). This is a blocking call unless there’s data available.
2.3 Partial Reads and Protocol Considerations
TCP is a stream protocol, not message-based. This means:
In many frameworks, you do not see this loop because the library handles it. However, under the hood, it’s doing exactly that.
2.4 Performance Implications
If your code is slow to read data, the kernel’s receive buffer may fill up. This can lead to TCP backpressure (window size reduction, potential retransmissions). In high-load systems, reading quickly and properly is crucial to maintain throughput.
Also consider your concurrency model:
Step 3: Decrypt (Optional, but Common with HTTPS)
3.1 TLS Handshake Basics
If you are running HTTPS or any TLS-encrypted protocol, the handshake (key exchange) has already taken place after the Accept step but before actual data is read as plaintext. This handshake involves:
3.2 CPU Overhead of Decryption
Each packet is encrypted by the client and must be decrypted by your server. This typically uses algorithms like AES (in GCM mode, CBC, or others). Decryption is CPU-expensive compared to plain TCP. Under heavy loads (thousands of concurrent HTTPS connections), CPU usage can spike just from encryption/decryption tasks.
3.3 Go Example with TLS
In Go, you might do something like:
Notice that we don’t manually handle encryption or decryption. The Go TLS library does it automatically. But the CPU overhead is still your server’s responsibility.
3.4 Memory Copies and Modern Solutions
Data is typically copied multiple times (kernel memory → user space encrypted buffer → user space decrypted buffer). Innovations like io_uring aim to reduce these copies in Linux, but they’re still in relatively early adoption.
Step 4: Parse
4.1 Parsing Protocol Structures
Now that you (potentially) have plaintext data in user space, the next step is to figure out how to interpret it. For HTTP/1.1:
With HTTP/2, parsing is more involved because data is framed. You must read frame headers, handle stream identifiers, window updates, etc.
Every protocol has to define how to separate “packets” or “requests” from the raw byte stream. This is called message framing.
4.2 Example: Naive Parsing in Go
Here’s a complete example that very naively parses a single HTTP/1.1 request. It reads once, looks for the request line and headers, and prints them. (In a real server, you must handle partial reads, chunked bodies, etc.)
This is oversimplified but shows how you might approach protocol parsing. In production, you rely on robust libraries (net/http, nghttp2, etc.).
4.3 CPU Costs of Parsing
Parsing can be CPU-intensive if you have:
HTTP/2 is more expensive than HTTP/1.1 because it uses compression for headers (HPACK or QPACK in HTTP/3) and frames. This is beneficial for multiplexing but costs more CPU to handle.
Step 5: Decode
5.1 Beyond Parsing: Decoding the Body
Once the protocol is parsed, you likely have a body in raw text or binary. Now you must decode or deserialize it into something meaningful for your application.
This can be an expensive operation if the body is large or if you do repeated decoding.
5.2 JSON Example
Below is a complete sample that reads a raw HTTP-like request, extracts a JSON body, and decodes it into a Go struct:
5.3 Watch Out for Large JSON
Also be mindful of memory usage. Some languages and frameworks use streaming parsers to avoid loading the entire body in memory at once.
Step 6: Process
Finally, after all the above steps, you have identified a complete request and turned it into application-friendly data (like a struct, an object, or key-value pairs). Now you can do the real logic:
6.1 CPU-Bound vs. IO-Bound
Your processing might be:
Understanding whether your requests are CPU- or IO-bound shapes how you scale (number of worker goroutines, concurrency patterns, etc.).
6.2 Handling Multiple Requests
Modern servers often handle many concurrent requests. In Go, concurrency via goroutines is relatively light. However, each request flow includes:
If you reach thousands of concurrent requests, you might saturate:
Optimizing each step is essential before deciding to spin up more machines.
Why Understanding These Steps Matters
Example: A “Full” (Yet Simplistic) Go Server Demonstrating All Steps
Below is a single program that tries to incorporate all six steps in one place. In real production, you would likely use net/http or another fully-featured library, but let’s do it manually to illustrate the entire journey:
Explanations:
Notice how many steps are hidden by libraries. If you used the standard net/http library, you would see just http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){...}), but behind the scenes, the library is doing everything we described.
Final Thoughts
There is a vast amount of hidden complexity before your backend sees a “request.” Each of the six steps—Accept, Read, Decrypt, Parse, Decode, and Process—can itself be a bottleneck or a source of errors.
Key Points to Remember
By shining light on these stages, you can identify where your bottlenecks truly lie. Rather than always scaling horizontally with more machines, you might discover that you can optimize your acceptance strategy, tune your TLS settings, or handle decoding more efficiently.
Additional Tips and Insights
Conclusion
We’ve seen how every request travels through multiple stages before your code calls it a “request.” By recognizing the Accept → Read → Decrypt → Parse → Decode → Process flow, you get greater visibility into why performance, security, or scaling problems may arise.
When trouble hits, or you need to scale, you now have the mental framework to say, “Okay, which step is the real culprit?” Sometimes, small adjustments—like increasing the Accept backlog, switching to more efficient JSON decoding, or caching TLS sessions—can yield huge gains.
Thank you for reading, and happy engineering!
Pre-final year student at MKSSS's Cummins college of engineering for women
2 周Insightful!