Part 01 · The Backend Mindset

From a Script to a Server

ProcessServer

Growing a one-off script into a program that keeps running and answers requests.

⏱ 1-2 hours · Station 9 of 153

Your script works. Your friend can't use it.

You write a small program that counts the words in a piece of text. You run it, it prints 4, and it's done. Useful!

A friend wants to use it from their phone. A teammate wants to call it from their website. Neither can: your program ran for 20 milliseconds on your laptop and then stopped existing. There was nothing to call.

Every backend you'll ever build started as a function like this one. This lesson is about what changes when a program stops being something you run and becomes something that stays running and answers other programs: a server.

The problem: a script ends

A script and a server can contain the same useful function. What differs is the life around it.

ScriptServer
How it startsYou run itSomething starts it (you, systemd, Docker, a platform) and it keeps going
Where input comes fromCommand-line arguments, a file, stdinRequests arriving over the network
How long it livesUntil the work is doneUntil it's told to stop, ideally for months
How many jobsOneThousands, one after another and at the same time
Who can use itWhoever runs it, on that machineAnyone who can reach its address and port
What it can rememberNothing after it exitsAnything, for as long as the process lives (and loses it when it stops, lesson 5)

Turning a script into a server is mostly about the right-hand column: an address, a loop that waits for work, and the discipline to keep running when something goes wrong.

Think of a tea seller

A script is making tea at home: you boil water, make one cup, wash the kettle, done. If a neighbour wants tea an hour later, there's no tea-making going on. They'd have to come and ask you to start again.

A server is a tea stall at the bus stand. It opens in the morning at a known spot, and then the seller waits. A customer arrives, a cup is made, the seller goes back to waiting. The stall doesn't close after one cup. It closes at night, after finishing the cups in hand.

The recipe for tea is identical in both. What makes the stall a stall is the fixed address, the waiting, and staying open.

scriptstartcount wordsprintexitthe process is gone; nobody can reach itserverstartlisten on a portwait → handle → waitclientsrequest 1request 2request 3
The script starts, counts the words, prints and exits; after that there's nothing to reach. The server starts, listens on a port and goes around the same loop again and again: wait for a request, handle it, wait again.

Where the analogy stops working. A tea seller serves one person at a time. A server can handle many requests at once (lesson 1), and the "address" is a pair of numbers, an IP and a port (Module 2), instead of a spot on the pavement. And unlike the stall, a server can be started several times in different places, as identical copies (lesson 5).

The building blocks

  1. A process. When you run a program, the operating system creates a process: the running instance, with its own memory. A script's process ends when main returns. A server's process is designed not to. (Module 4, lesson 2.)
  2. Listening on a port. A server asks the operating system for a port (a number from 1 to 65535) on an IP address. From then on, connections sent to that IP and port are handed to this process. Port 0 means "pick any free port for me", which is handy in tests. (Module 4, lesson 4.)
  3. The accept loop. The heart of every server is a loop: wait for a connection → read the request → do the work → write the response → wait again. Frameworks hide it, but it's always there.
  4. Long-lived state. Because the process keeps running, it can keep things in memory between requests: a counter, a cache, a pool of database connections. Useful, and dangerous: it all disappears when the process stops (lessons 4 and 5).
  5. Failure is per request, not per process. A script that hits an error can simply crash. A server must not: one bad request shouldn't take down everyone else's. Servers catch errors per request, log them, return a 500, and carry on.
  6. Stopping on purpose. A server is stopped by a signal (Ctrl+C, a deploy). A good one finishes the requests it's in the middle of before exiting: a graceful shutdown. (Module 4, lesson 13.)
Predict first

You turn a word-count script into a server that also counts how many requests it has served. It answers 3 requests, then you restart it and send 1 more. What does it say for that last request?

Behind the scenes: the life of a server

Step through it

From start to shutdown

Operating systemServer processClient

Step 1/6 The process starts

Someone runs the program. The operating system creates a process, loads the code and calls main. So far it's just like a script.

state
starting

The picture to keep in your head

A server is a script wrapped in three layers:

  1. An address: an IP and a port, so others can find it.
  2. A loop: wait, handle, respond, wait again.
  3. A safety net: per-request error handling, timeouts, and a graceful shutdown, so it keeps running for months.

The useful function in the middle doesn't change. When you read a framework's "hello world", look for the three layers: the listen call is the address, the framework runs the loop for you, and middleware usually provides the safety net.

Try it: script or server?

Try each case

Should this be a script or a server?

resize ./photos --width 800
works: A script. It has one job, a clear end, and nobody else needs to call it.

Code from first principles

The same countWords function used twice:

  1. As a script: call it once, print, done.
  2. As a server: a real HTTP server from the standard library, on a port the OS picks. It keeps a counter of requests served, in memory. Three requests are sent to it by hand, then it's shut down.
package main

import (
	"bufio"
	"fmt"
	"net"
	"net/http"
	"strings"
	"sync/atomic"
)

// The useful work. A script and a server can share exactly the same function.
func countWords(text string) int { return len(strings.Fields(text)) }

// get sends one request by hand and returns the response body.
func get(addr, path string) string {
	conn, err := net.Dial("tcp", addr)
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	fmt.Fprintf(conn, "GET %s HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", path)
	r := bufio.NewReader(conn)
	for { // skip the status line and headers: they end at the first empty line
		line, err := r.ReadString('\n')
		if line == "\r\n" || err != nil {
			break
		}
	}
	body, _ := r.ReadString(0)
	return body
}

func main() {
	// 1. As a script: get input, do the work once, print, and the program ends.
	fmt.Printf("script: count(%q) = %d, then exit\n", "the quick brown fox", countWords("the quick brown fox"))

	// 2. As a server: a process that stays alive and answers request after request.
	var served atomic.Int64 // state that lives as long as the process does
	mux := http.NewServeMux()
	mux.HandleFunc("/count", func(w http.ResponseWriter, r *http.Request) {
		n := served.Add(1)
		fmt.Fprintf(w, "%d words (request %d since start)", countWords(r.URL.Query().Get("text")), n)
	})
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "ok (request %d since start)", served.Add(1))
	})
	ln, err := net.Listen("tcp", "127.0.0.1:0") // port 0: let the OS pick a free port
	if err != nil {
		panic(err)
	}
	srv := &http.Server{Handler: mux}
	go srv.Serve(ln) // the accept loop: wait for a connection, handle it, wait again
	fmt.Println("server: listening on 127.0.0.1 (port chosen by the OS)")

	for _, path := range []string{"/count?text=hello+world", "/count?text=to+be+or+not+to+be", "/health"} {
		fmt.Printf("GET %-32s -> %s\n", path, get(ln.Addr().String(), path))
	}
	srv.Close()
	fmt.Printf("server: shutting down after %d requests\n", served.Load())
}

Output (the same in all five languages):

script: count("the quick brown fox") = 4, then exit
server: listening on 127.0.0.1 (port chosen by the OS)
GET /count?text=hello+world          -> 2 words (request 1 since start)
GET /count?text=to+be+or+not+to+be   -> 6 words (request 2 since start)
GET /health                          -> ok (request 3 since start)
server: shutting down after 3 requests

Reading the output:

  • script: one call, one answer, and in a real script the program would end here. There's nothing left running to talk to.
  • server: listening: the process now has an address. It's waiting, using almost no CPU.
  • The three requests: each one goes around the same loop. The counter climbs from 1 to 3 because the process stays alive between requests and keeps its memory.
  • /health: a second route in the same server. Real servers expose one so load balancers and platforms can check they're alive (Module 7, lesson 14).
  • shutting down after 3 requests: stopping is now a deliberate act. Start the program again and the counter begins at 1: memory belongs to the process.

Real engineering

  • Something must keep the process running. In production, servers are started and restarted by a process manager: systemd, Docker, Kubernetes or a platform. If the process crashes, it's started again within seconds. (Module 10.)
  • Configuration comes from outside. Scripts often take arguments; servers read their port, database URL and secrets from environment variables or config files, so the same build runs everywhere. (Module 10, lesson 4.)
  • Start-up cost is paid once. Servers load configuration, open database connection pools, warm caches and compile templates at start-up, so each request is cheap. This is also why the first request after a deploy can be slower.
  • Log, don't print. A script prints its result. A server writes structured logs with a timestamp and a request id for every request, because nobody is watching its terminal. (Module 7, lesson 13.)
  • Plan for stopping. Deploys stop servers all the time. Handle SIGTERM, stop accepting new work, finish what's in flight, then exit. (Module 4, lesson 13.)

When it breaks

Try each case

Pick a failure

listen tcp :8080: bind: address already in use
breaks: Another process (often an old copy of the same server) already holds the port. Find it with ss -ltnp | grep 8080 or lsof -i :8080, stop it, or use a different port.

Think before you read on

Your word counter needs a 2 GB dictionary that takes 4 seconds to load. As a script, every call takes about 4 seconds. As a server, the first request after start-up is fast too. Why, and what new problem did you just create?

Reveal

The server loads the dictionary once at start-up, before it starts listening, so every request (including the first) only pays the millisecond of counting. The new problem: start-up now takes 4 seconds and 2 GB of memory per copy. Deploys and restarts are slower, each extra copy needs 2 GB, and the platform must not send traffic until loading is done. That's what readiness checks are for (Module 7, lesson 14).

Check yourself

1. What are the three things that turn a function into a server?

Answer

An address (listening on an IP and port), a loop that waits for and handles requests, and the safety net that keeps it running (per-request error handling, timeouts, graceful shutdown).

2. What does listening on port 0 do?

Answer

It asks the operating system to choose any free port. The program then reads which port it got. Useful in tests, so many servers can run without clashing.

3. Why should a server never exit because of one bad request?

Answer

Because the process is shared by everyone. Exiting disconnects every other user. The bad request should get an error response, and the server should carry on.

4. Why did the request counter reset after a restart?

Answer

It lived in the process's memory. A restart creates a new process with fresh memory. Data that must survive belongs outside the process.

Your challenge: give it a real job

Extend the server with a second endpoint, /top?text=..., that returns the most common word in the text and how many times it appears. For example, /top?text=to+be+or+not+to+be returns to (2).

  • Keep the word-counting logic in plain functions that the script part can also call.
  • Make it so that a request with no text gets a clear error with status 400, and the server keeps running.

Before you run it, predict what /top?text=b+a+b+a should return when two words tie, and decide on a rule (for example, the one that appears first).

You've succeeded when /top?text=to+be+or+not+to+be returns to (2), /top with no text returns status 400, the request after it still works, and the request counter counts all of them.

The mental model

A script does one job and ends. A server is the same useful code wrapped in an address, a loop and a safety net: it listens on a port, waits, handles a request, and waits again, for months. Because it stays alive it can pay start-up costs once and keep things in memory, but that memory dies with the process, and every error must be contained to one request.

Station stamp

Finish the chapter

Done reading? Press Complete to save your progress. XP is Pro-only — on Pro this chapter is worth +10 XP.

Go Pro to earn XP →