Step 01 of 06
Learn the concept
A server can resolve, accept TCP, pass TLS and still serve 503 Service Unavailable. That is not a contradiction. The lower layers proved reachability and identity; HTTP is where the application finally gives its verdict.
The ideas this is made of
The request names the application, not just the machine
An HTTP request has a method, request target, protocol version, headers and maybe a body. The Host header is mandatory in HTTP/1.1 because many sites share one IP address. HTTP/2 carries the same idea in pseudo-headers. Reaching the socket is not enough; the request must identify the virtual host and path you actually want checked.
Headers and body measure different waiting
Time to first byte ends when response headers begin arriving. It includes server queueing and handler work before the status is known. Total time includes reading the body too. A health endpoint may make those nearly equal; a large download will not. Monitors usually care about status and header latency more than turning every check into a bandwidth test.
The client and transport have separate jobs
http.Request is one question. http.Transport obtains connections, handles proxies, performs TLS and manages pooling. http.Client applies policy: redirects, cookies and broad timeouts. http.Get hides all of that behind global defaults. Beacon owns a client because a monitor must own redirect behaviour, cancellation and reuse rather than inheriting them by accident.
Connection reuse depends on the body
HTTP/1.1 keeps connections alive by default; HTTP/2 multiplexes streams over one connection. Go can reuse a connection only when the response body is consumed or safely discarded and closed. io.Copy(io.Discard, resp.Body) before Close is not busywork. It prevents each check from paying a fresh TCP and TLS tax.
Redirects are status codes with consequences
Go follows redirects by default. That is friendly for browsers and surprising for monitors. A watched URL returning 302 to a login page may become a final 200, hiding the configured target's answer. Set CheckRedirect deliberately: stop at the first response, or record every hop. Do not let the default decide what health means.
package main
import (
"fmt"
"io"
"net/http"
"time"
)
func main() {
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
if err != nil {
panic(err)
}
req.Header.Set("User-Agent", "beacon-learning/1.0")
start := time.Now()
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
_, _ = io.Copy(io.Discard, resp.Body)
fmt.Println("status:", resp.StatusCode)
fmt.Println("total_ms:", time.Since(start).Milliseconds())
}The code builds an explicit request, uses one reusable client, disables automatic redirects and drains the body. That is the smallest shape that still resembles production HTTP probing.
Status classes without folklore
| Class | Meaning | Monitor action |
|---|---|---|
1xx | Informational | Rare in simple checks |
2xx | Handled successfully | Usually success |
3xx | Redirect | Record or follow by policy |
4xx | Request problem | Config, auth or path |
5xx | Server problem | Service is unhealthy |
What these are called on the job
Request line — The HTTP/1.1 method, target and version at the start of a request.
Host header — The header that identifies the virtual host being requested.
Transport — Go's HTTP component for connections, TLS, proxies and pooling.
TTFB — Time to first byte: delay until response bytes start arriving.
Drain — Read and discard a body so the connection can be reused.
