Lesson 1 of 1 · 22 min
Repository Bootstrap
Establish a clean Go repository and the unrestricted HTTP baseline that every later rate-limiting decision will be measured against.
Learning objectives
- Explain what a rate limiter protects and which identity it limits.
- Create a minimal, idiomatic Go repository without premature abstractions.
- Run and verify an unrestricted baseline endpoint.
- Record the HTTP and algorithm decisions that later lessons must make explicit.
What you are building
This study builds a rate limiter from first principles. The final system will need a clear HTTP contract, a tested algorithm, safe concurrency, useful observability, and a path from one process to distributed enforcement.
This first lesson deliberately implements no rate limiting. Instead, it creates the smallest trustworthy baseline: a clean repository and one unrestricted endpoint. That baseline matters because every later behavior—rejection, retry guidance, fairness, and burst handling—must be observable as a change from something known.
Lesson boundary
Create the repository, run a plain HTTP server, and verify the endpoint. Do not add counters, middleware, Redis, or an algorithm yet.
Before code: define the contract
A rate limiter is not simply “a counter.” It is an overload and fairness policy attached to a request boundary. Before selecting an algorithm, answer four questions:
- Who is limited? An API key, authenticated user, tenant, client IP, or a composite identity?
- What is limited? The entire API, a route group, one expensive endpoint, or a business operation?
- What happens at the limit? Usually
429 Too Many Requests, often withRetry-After. - What traffic shape is allowed? A sustained rate, a short burst, or both?
429 communicates that the caller exceeded an application policy. 503 Service Unavailable communicates temporary service incapacity. The distinction affects client retry behavior, dashboards, and incident response.
Burst versus sustained rate
“Ten requests per second” is incomplete. A strict fixed window, token bucket, and sliding window can all enforce roughly that rate while producing different user experiences. Later lessons will make burst capacity explicit rather than hiding it inside an implementation.
Local versus distributed enforcement
A process-local limiter is fast and simple, but each replica owns a different counter. A shared store gives a coordinated limit across replicas at the cost of latency and a new failure mode. We will begin locally, make the boundary replaceable, and move to distributed coordination only after the contract is stable.
Create the repository
This study uses Go 1.22 or newer.
mkdir go-rate-limitercd go-rate-limitergit initgo mod init github.com/TonmoyTalukder/go-rate-limitermkdir -p cmd/serverStart with this intentionally small layout:
go-rate-limiter/├── cmd/│ └── server/│ └── main.go├── .gitignore├── LICENSE├── README.md└── go.modThe cmd/server package is the executable boundary. Algorithm and transport packages will appear only when a later lesson gives them a real responsibility.
Add the unrestricted server
Create cmd/server/main.go:
package mainimport ( "fmt" "log" "net/http")func main() { mux := http.NewServeMux() mux.HandleFunc("GET /api/demo", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) _, _ = fmt.Fprintln(w, "ok") }) const address = ":8080" log.Printf("listening on http://localhost%s", address) server := &http.Server{ Addr: address, Handler: mux, } if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("server failed: %v", err) }}The standard library is enough for the baseline. The method-aware pattern GET /api/demo uses the Go 1.22 router syntax and makes the endpoint contract visible at registration.
Document the baseline
Your README should state:
- the project goal;
- the current lesson boundary;
- the required Go version;
- how to run and verify the server;
- the endpoint and its expected response;
- the fact that requests are currently unrestricted.
That last point prevents an important review mistake: interpreting the absence of a limiter as an unfinished or broken implementation.
Format and verify
gofmt -w cmd/server/main.gogo build ./...go vet ./...go test ./...go run ./cmd/serverIn a second terminal:
curl -i http://localhost:8080/api/demoExpected response:
HTTP/1.1 200 OKContent-Type: text/plain; charset=utf-8okConfirm that the endpoint is unrestricted:
for i in $(seq 1 100); do curl -s http://localhost:8080/api/demodoneAll 100 requests should return ok. There should be no 429 response, no rate-limit headers, and no limiter log entries.
Optional container check
If Go is not installed locally:
docker run --rm \ -v "$PWD":/app \ -w /app \ golang:1.22 \ bash -lc 'gofmt -w . && go build ./... && go vet ./... && go test ./...'Record the engineering decisions
Create a short learner note before moving on:
| Decision | Current choice | Why |
|---|---|---|
| Runtime | Go 1.22+ | Method-aware standard-library routing |
| HTTP framework | net/http | Keeps the baseline dependency-free |
| Endpoint | GET /api/demo | Small, deterministic verification surface |
| Limit identity | Undecided | Will be explicit in the HTTP contract lesson |
| Algorithm | None | The unrestricted baseline is intentional |
| Coordination | None | Local and distributed designs come later |
Exit checklist
- The repository has only the files the baseline needs.
go build ./...,go vet ./..., andgo test ./...succeed.GET /api/demoreturns200 OKandok.- Repeated requests remain unrestricted.
- The README explains both the project goal and current boundary.
- No limiter logic has been introduced prematurely.
The next lesson turns these open decisions into an explicit HTTP rate-limiting contract.