Sitemap

How to Build a HTTP server in C: Easiest Guide

12 min readJul 23, 2026

--

HTTP or HyperText Transfer Protocol, is a system that allows communication between web browsers and web servers. It’s the engine that load HTML websites all over the internet using a strict set of rules.

Despite how huge and complex it sounds, it runs on a standard client-server request and response model (very big name for a simple concept).
In simplest terms, client sends a request to server and server responds accordingly (atleast server responded, unlike her :’( ).

So, In this guide, I am gonna show you how things work under the hood. To really understand we are gonna make a http server in C!!
(yeah half of the audience just left hearing that monstrosity of language)

Why C?
Well the shortest answer is: It allows us to build networking programs from the absolute basics.
In HTTP, it all starts with a basic tcp connection and http is applied on top of that, so in C we will implement a TCP server which takes HTTP request and responds a HTML file. SIMPLE.

See it is so easy, so let’s begin.

How HTTP works?

A typical http session consist of 3 phases:
1. Establish a TCP connection between client & server.
2. Client sends it’s request and server recieves it.
3. The server processes the request, sending back it’s answer, providing a status code and appropriate data.

Generally TCP or older HTTP connections closes after third phase. As of HTTP/1.1, the connection is no longer close after completing the third phase, and client is now granted a further request.

Phase 1: Establishing a TCP connection

So, to communicate with client we are gonna use sockets.
Now, What are sockets?
-> In simple words, it is a way to speak to other programs using standard Unix file descriptors.
Now, What the hell is a file descriptor?
-> A file descriptor is simply an integer associated with an open file. But that file can be a network connection, a FIFO, a pipe, a terminal, an actual file on hard disk, etc.
You might ask, “Why is everything a file?” Well, answering that in this blog would make things way too complex (some UNIX shii), and we simply don’t want that. Otherwise, HTTP won’t get any attention, and it will get sad.
So, for the deep lore, refer to: https://beej.us/guide/bgnet/html/#what-is-a-socket

Now, create a C file named server.cor whatever name you want (I don't care) write your standard boilerplate code, and add these headers:

#include<sys/types.h>
#include<sys/socket.h>

Next, we are going to call socket() to get our socket file descriptor, like this:

int socketfd = socket(AF_INET,SOCK_STREAM,0);

The socket() call takes three arguments:

  • int domain: Is it IPv4 or IPv6?
  • int type: Is it a stream or datagram?
  • int protocol: Is it TCP or UDP?

Here, AF_INET means IPv4, SOCK_STREAM means we want a two-way connected stream, and 0 tells the system to automatically choose the suitable protocol for that type (which, in this case, is TCP).

On success, it will return some integer (this is your shiny new file descriptor!). But if there is an error, it will return -1.

For more info, you can refer for now: https://beej.us/guide/bgnet/html/#two-types-of-internet-sockets or in future, i am going to write a more detailed blog on tcp connections using sockets in c.

Before we can actually map our socket to a specific port (like port 8000), we need to set up the data structures that hold our network configuration. In the old days of C, developers had to pack these structures entirely by hand. It was miserable.
Thankfully, modern C gives us a superstar function called getaddrinfo() which automatically loads up everything we need. But to use it, we have to talk about two important structures: hints and res.
Think of hints as a blank checklist you fill out to tell the OS what kind of connection you are looking for. Think of res (results) as the packet of information the OS hands back to you once it finds a match.
First, add network database header to your file:

#include<netdb.h>
#include<string.h> //we are gonna use memset and strlen so we need that

Then, we define address structs and fill the values:

struct addrinfo hints,*res;

memset(&hints,0,sizeof(hints) //Clear any garbage value present
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;

And, we call the getaddrinfo() call:

getaddrinfo(NULL,"8000",&hints,&res);

Here, getaddrinfo()take 4 arguments:

  • const char *node : It basically means the hostname like www.example.com. Here, we pass it NULL as AI_PASSIVE handles the ip address.
  • const char *service: It means PORT number like 8080, 8000,etc.
  • And the other two are hints and *res.

The system goes to work, finds a perfect network setup, allocates the memory, and points res directly to a beautiful, completed network profile that we can use to bind our socket.

Once you have a socket created, you need to lock it onto a specific port. Otherwise, if you type http://localhost:8000 into your web browser, the OS will just shrug and wonder which program was supposed to handle that incoming request.
To make that association, we use the bind() system call:

bind(socketfd,res->ai_addr,res->ai_addrlen);

Under the hood, bind() takes three specific arguments:

  • int sockfd : the socket file descriptor
  • struct sockaddr *my_addr : A pointer to a structure containing your address information (namely, your IP address and port number). Notice how we just pass res->ai_addr here.
  • int addrlen : The length (in bytes) of that address structure. Again, res->ai_addrlen has us covered.

Now, a raw tcp socket has been created and now we have to enable it to recieve requests in next phase.

Phase 2: Recieveing Requests from the client

Now, to recieve requests the server first have to “listen” and then “accept” the request.

“Listen” here means waiting for the incoming connections and handle them in some way.

This can be done using very simple listen() call:

listen(socketfd,5);

This call takes 2 arguments:

  • int sockfd : The socket file descriptor you created (yes, again).
  • int backlog : backlog is the number of connections allowed on the incoming queue. What does that mean? Well, incoming connections are going to wait in this queue until you “accept” them and this is the limit on how many can queue up. Here, we set it to 5.

After listening, the server “accepts” the connection. Now, what happens when server “accepts”?

When a server “accepts” the request, it basically means it connects to one of the request in the incoming queue.

Imagine you’re running a pizza shop.

  • listen() is like opening the shop and waiting for customers.
  • Customers start lining up outside.
  • accept() is when you finally call "Next!" and let one customer inside.

The same thing happens with our server. Clients (in our case browsers) line up requesting a connection, and accept() picks one of them and starts talking to it.

Before we call accept() , we need to create:

struct sockaddr_storage their_addr;
socklen_t addr_size;

their_addrsimply a container where the operating system stores information about the client that just connected, such as its IP address and other networking details.

addr_size tells accept() how big the address structure is.

Finally, we call accept() :

 int newfd = accept(socketfd,(struct sockaddr *) &their_addr,&addr_size);

Something very important happens here.
Notice that accept() returns another file descriptor.

Wait… didn’t we already have one?

Yes! But they serve different purposes.

  • socketfd is the listening socket. Its only job is to sit there and wait for new clients.
  • newfd is the connected socket. This is the one we'll use to receive requests and send responses.

Now we can send a request to our server, which can be accepted. To send a request, type http://localhost:8000 and hit enter. It will not show anything but the request has been recieved by the server.

To see the message from the client, we use recv() call:

char buf[1024];
int recv_bytes = recv(newfd,&buf,sizeof(buf), 0);

Let’s break it down.

  • newfd : The connected socket we got from accept(). Remember, this is the socket that's actually talking to the client.
  • buf : A character array where the incoming data will be stored. We pass it by reference so it directly read and writes on the buf array.
  • sizeof(buf) : The maximum number of bytes we want to receive. Since our buffer is 1024 bytes long, we tell recv() not to write beyond that. (Please don't let your programs scribble outside the lines.)
  • 0 : No special flags. We’re keeping things simple for now.

recv() returns the number of bytes it actually received.

We can print those bytes:

printf("Received Bytes: %d\n", recv_bytes);

Now we hit http://localhost:8000 again, on terminal it will show:

But what exactly is the browser sending us?

If you print the buffer,

printf("%s\n", buf);

you’ll see something like this:

Press enter or click to view image in full size

This message is called an HTTP Request. It’s simply the browser asking the server for something.

The very first line is called request-line,

GET / HTTP/1.1

it consist of three parts:

  • GET → HTTP method
  • / → Request Target or URL
  • HTTP/1.1 → Protocol (HTTP version)

The lines below it (like Host, User-Agent, and Accept) are called headers. They give the server extra information about the request, but we don't need to worry about them just yet.

Get Keshavmehra’s stories in your inbox

Join Medium for free to get updates from this writer.

Right now, our server can successfully receive a request.

The only problem?

We’ve read the browser’s message… but we haven’t replied.

And browsers are terrible at waiting forever (Zero patience, just like her).

So in the next phase, let’s send back a proper HTTP response.

Phase 3: Sending a HTTP Response

Before sending a response, we first need to understand how to form a HTTP response.

Same as HTTP request, a HTTP response majorly consist of three things:

HTTP/1.1 200 OK        --> Status Line
Content-Type: text/html | --> Headers
Content-Length: 21 |

<h1>Hello From C</h1> --> Response Body

First, the start-line HTTP/1.1 200 OKis called a "status line" in responses, and has three parts:

<protocol> <status-code> <reason-phrase>
  • <protocol>: The HTTP version of the message.
  • <status-code>: Status Code indicates whether the request succeeded or failed. Common status codes are 200, 201, 404, etc.
  • <reason-phrase>: A human-readable description of the status code. It’s mainly there for us humans to read (This is completely optional btw). Browsers mostly care about the status code itself.

Then, we have headers. They basically shows what kind of data we are sending.

Here, Content-Type: text/html tells that the content is HTML. Without this, the browser might not know how to display our response correctly.

And, Content-Length: 21 tells the browser when our response ends. Imagine someone sends you a letter without telling you how many pages it has.How would you know if you’ve reached the last page?That’s exactly why we send the content length.

There are many other headers like Server, Date, Cache-Control, Connection, and dozens more, but we don't need to worry about them just yet. Two headers are more than enough to build our first web server.

Finally comes the Response Body. This is the actual data the browser wants.
Depending on the request, it could be:

  • an HTML webpage,
  • a JSON response from an API,
  • a CSS file,
  • a JavaScript file,
  • an image,
  • or pretty much anything else.

In our case, we’re keeping it simple and sending an HTML string:

char *html = "<h1>Hello From C</h1>";
int html_len = strlen(html); // We need this for content-length header

In Future, we’ll go one step further and serve an actual HTML file instead of hardcoding the HTML inside our C program.

So, to send a response we use send() call.

But before sending the response, we first need to construct it.

First, the status line:

 char *sl = "HTTP/1.1 200 OK\r\n";

then we define headers:

char *header1 = "Content-Type: text/html\r\n";
char header2[36];
snprintf(header2,sizeof(header2),"Content-Length: %d\r\n\r\n",html_len);

For header2, we use snprintf() to format a string by inserting the value of html_len into the Content-Length header.

Notice something interesting.

\r\n\r\n

Yes, there are two \r\ns.

The first one ends the Content-Length header.

The second one creates the blank line that separates the headers from the response body.

Forget this blank line, and your browser will stare at your response like,

“Bro… are you done sending headers or not?”

Now we have a correct response for the request, so we are gonna send it using send() call.

A send() call, similarly like recv() , looks like this:

int send(int sockfd, const void *msg, int len, int flags); 

where:

  • int sockfd : Socket File Descriptor. Right now, we are gonna put newfd .
  • const void *msg : Pointer to the data we want to send.
  • int len : Length of the message.
  • int flags : Any extra flags. In our case, 0.

So, let’s send our response:

 send(newfd,sl,strlen(sl),0);
send(newfd,header1,strlen(header1),0);
send(newfd,header2,strlen(header2),0);
send(newfd,html,html_len,0);

We send the status line, headers, and body separately instead of combining them into one large string. This avoids creating an extra buffer and is a common approach used by HTTP servers.

Now, compile, run and hit http://localhost:8000 and this time you will see:

Boom! Now you have a running HTTP server in C! Demn, That’s an achievement.

Once we’ve finished responding, we close newfd to release the resources associated with that client connection.

First, put header:

#include<unistd.h>

then,

close(newfd);

SIMPLE. No much drama.

Now, our server responds to the request. The problem is that it returns the same response regardless of which URL is requested. It gives same response to GET / or GET /hello . So, we need to define the routes and responses according to request type. But, it will extend this blog more so we are gonna do a simple thing.

 if(strncmp("GET",buf,3)==0){

char *html = "<h1>Hello From C</h1>";
int html_len = strlen(html);

char *sl = "HTTP/1.1 200 OK\r\n";
char *header1 = "Content-Type: text/html\r\n";
char header2[36];
snprintf(header2,sizeof(header2),"Content-Length: %d\r\n\r\n",html_len);

send(newfd,sl,strlen(sl),0);
send(newfd,header1,strlen(header1),0);
send(newfd,header2,strlen(header2),0);
send(newfd,html,html_len,0);

close(newfd);
}

Here, we just put a if statement that check either the request is GET or not by doing simple strncmp() . (I know it’s not professional way to do it but it works!)

In Future blogs, I will teach routing and implementing different request types

Until now, our server has one tiny problem.

It accepts one connection, sends a response, and then says,

“My work here is done.”

and exits.

That’s not how real web servers behave.

Imagine opening Google, searching something, and then Google shuts down after serving your request. The next person trying to search would get nothing.

A web server is expected to keep running and continuously listen for new clients.

That’s exactly where while(1) comes in:

while(1){
struct sockaddr_storage their_addr;
socklen_t addr_size;

int newfd = accept(socketfd,(struct sockaddr *) &their_addr,&addr_size);

char buf[1024];
int recv_bytes = recv(newfd,&buf,sizeof(buf),0);

printf("Recieved Bytes: %d\n",recv_bytes);
printf("%s\n",buf);

if(strncmp("GET",buf,3)==0){

char *html = "<h1>Hello From C</h1>";
int html_len = strlen(html);

char *sl = "HTTP/1.1 200 OK\r\n";
char *header1 = "Content-Type: text/html\r\n";
char header2[36];
snprintf(header2,sizeof(header2),"Content-Length: %d\r\n\r\n",html_len);

send(newfd,sl,strlen(sl),0);
send(newfd,header1,strlen(header1),0);
send(newfd,header2,strlen(header2),0);
send(newfd,html,html_len,0);

close(newfd);
}
}

while(1) simply means run forever. Since 1 is always true, the loop keeps repeating.

Now the server follows this cycle:

Wait for a client
↓
Accept connection
↓
Receive request
↓
Send response
↓
Close client socket
↓
Wait for the next client

Notice that we only call close(newfd). That's because newfd is the socket connected to the current client. Once we've responded, we don't need that connection anymore.

The listening socket (socketfd) is never closed inside the loop, so the server continues listening for new incoming connections.

And just like that, you’ve built your very first HTTP server in C.

It may not be as powerful as Nginx or Apache (yet), but don’t underestimate what you’ve just created. You’ve written a program that listens for incoming connections, understands an HTTP request, and sends back a valid HTTP response that a web browser can render.

That’s exactly how every web server starts.

Of course, there’s still a lot we can improve. Right now, our server responds with the same page for every GET request, handles only one client at a time, and doesn’t support features like routing, static files, POST requests, or multithreading. But that’s completely okay every production-grade web server is just a collection of these same building blocks, implemented on a much larger scale. We will learn all these in future blogd

The goal of this guide wasn’t to build the fastest or the most feature-rich server. It was to remove the mystery behind HTTP and show that, at its core, it’s just a conversation between a client and a server following a simple set of rules.

I hope this blog helped you understand what actually happens when you type a URL into your browser.

In the next part, we’ll start making our server smarter by adding proper routing, serving HTML files, handling different endpoints, and slowly turning this tiny server into something much closer to a real web server.

Till then, Goodbye!

References:
Beej’s Guide: https://beej.us/guide/bgnet/html/
MDN docs: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Overview

Github Repo: https://github.com/Airbone25/http-server-in-c

Follow me on:

X: https://x.com/Keshav_Mehra200
Github: https://github.com/Airbone25

--

--