diff --git a/fe/dns.go b/fe/dns.go
index 309d3bbd9abfe866bcc3eee777208af0d581d8af..b2a827e309b2b5611f8d6a38d97c6630012dde85 100644
--- a/fe/dns.go
+++ b/fe/dns.go
@@ -1,6 +1,7 @@
 package fe
 
 import (
+	"expvar"
 	"fmt"
 	"log"
 	"math/rand"
@@ -8,8 +9,8 @@ import (
 	"strings"
 	"time"
 
-	"github.com/miekg/dns"
 	"git.autistici.org/ale/radioai"
+	"github.com/miekg/dns"
 )
 
 var (
@@ -29,6 +30,8 @@ var (
 // DNS server.
 type DnsRedirector struct {
 	client         *radioai.RadioAPI
+	queryStats     *expvar.Map
+	targetStats    *expvar.Map
 	origin         string
 	originNumParts int
 	publicIp       string
@@ -60,14 +63,20 @@ func NewDnsRedirector(client *radioai.RadioAPI, origin, publicIp string, ttl int
 		Minttl:  uint32(ttl),
 	}
 
-	return &DnsRedirector{
+	d := &DnsRedirector{
 		client:         client,
 		origin:         origin,
 		originNumParts: len(dns.SplitDomainName(origin)),
 		publicIp:       publicIp,
 		ttl:            ttl,
 		soa:            soa,
+		queryStats:     new(expvar.Map).Init(),
+		targetStats:    new(expvar.Map).Init(),
 	}
+	statsMap := expvar.NewMap("dns")
+	statsMap.Set("queries", d.queryStats)
+	statsMap.Set("targets", d.targetStats)
+	return d
 }
 
 // Randomly shuffle a list of strings.
@@ -179,6 +188,7 @@ func (d *DnsRedirector) serveDNS(w dns.ResponseWriter, req *dns.Msg) {
 		for _, ip := range ips {
 			rec := d.recordForIp(query, ip)
 			m.Answer = append(m.Answer, rec)
+			d.targetStats.Add(ip, 1)
 		}
 		responseMsg = fmt.Sprintf("%v", ips)
 
@@ -188,6 +198,12 @@ func (d *DnsRedirector) serveDNS(w dns.ResponseWriter, req *dns.Msg) {
 		responseMsg = "NXDOMAIN"
 	}
 
+	if responseMsg == "NXDOMAIN" {
+		d.queryStats.Add("errors", 1)
+	} else {
+		d.queryStats.Add("ok", 1)
+	}
+
 	log.Printf("[%d] %s.%s %s (from %s) -> %s", req.MsgHdr.Id, query, d.origin, dns.TypeToString[req.Question[0].Qtype], w.RemoteAddr(), responseMsg)
 
 	ednsFromRequest(req, m)
diff --git a/fe/http.go b/fe/http.go
index 0097e181569e02a6d9b51880ca60c242e5fae22d..863244c96aaee4d47280ab8ad732ff5e92665922 100644
--- a/fe/http.go
+++ b/fe/http.go
@@ -2,6 +2,7 @@ package fe
 
 import (
 	"bytes"
+	"expvar"
 	"fmt"
 	"html/template"
 	"io"
@@ -20,19 +21,47 @@ import (
 	"github.com/PuerkitoBio/ghost/handlers"
 )
 
+type statsResponseWriter struct {
+	http.ResponseWriter
+	code int
+}
+
+func (w *statsResponseWriter) WriteHeader(code int) {
+	w.code = code
+	w.ResponseWriter.WriteHeader(code)
+}
+
+func statsHandler(h http.Handler, stats *expvar.Map) http.HandlerFunc {
+	// Add a sub-map to hold aggregate HTTP status stats.
+	statusMap := new(expvar.Map).Init()
+	stats.Set("status", statusMap)
+	return func(w http.ResponseWriter, r *http.Request) {
+		wrapw := &statsResponseWriter{w, 200}
+		h.ServeHTTP(wrapw, r)
+		statusMap.Add(strconv.Itoa(wrapw.code), 1)
+	}
+}
+
 // HTTP redirector.
 type HttpRedirector struct {
-	domain   string
-	client   *radioai.RadioAPI
-	template *template.Template
-	lb       LoadBalancingPolicy
+	domain      string
+	lb          LoadBalancingPolicy
+	client      *radioai.RadioAPI
+	template    *template.Template
+	stats       *expvar.Map
+	targetStats *expvar.Map
 }
 
 func NewHttpRedirector(client *radioai.RadioAPI, domain string, lbpolicy string) *HttpRedirector {
+	targetStats := new(expvar.Map).Init()
+	stats := expvar.NewMap("http")
+	stats.Set("targets", targetStats)
 	return &HttpRedirector{
-		client: client,
-		domain: domain,
-		lb:     getNamedLoadBalancingPolicy(lbpolicy),
+		client:      client,
+		domain:      domain,
+		lb:          getNamedLoadBalancingPolicy(lbpolicy),
+		stats:       stats,
+		targetStats: targetStats,
 	}
 }
 
@@ -89,6 +118,7 @@ func (h *HttpRedirector) serveRelay(w http.ResponseWriter, r *http.Request) {
 		http.Error(w, "No active nodes", http.StatusServiceUnavailable)
 		return
 	}
+	h.targetStats.Add(relayAddr, 1)
 
 	// Create the m3u response.
 	m3u := fmt.Sprintf("http://%s%s\n", makeIcecastUrl(relayAddr), mount.Name)
@@ -104,14 +134,41 @@ func (h *HttpRedirector) serveSource(w http.ResponseWriter, r *http.Request) {
 	if err != nil {
 		log.Printf("source: error retrieving mount for %+v: %s", r, err)
 		http.Error(w, "Not Found", http.StatusNotFound)
+		h.stats.Add("source_404", 1)
 		return
 	}
 
+	h.stats.Add("source_connections", 1)
+
+	// Handy function to report a source error (the ones we care
+	// the most about because they measure failures in the
+	// coordination infrastructure).
+	sendErr := func(err error) {
+		http.Error(w, err.Error(), http.StatusServiceUnavailable)
+		h.stats.Add("source_errors", 1)
+	}
+
 	// Find the current master node.
 	masterAddr, err := h.client.GetMasterAddr()
 	if err != nil {
 		log.Printf("source: no master: %s", err)
-		http.Error(w, err.Error(), http.StatusServiceUnavailable)
+		sendErr(err)
+		return
+	}
+
+	// Create the upstream connection, and write the original
+	// request to it as-is (the URL path on the backend is the
+	// same, and the headers do not need to change).
+	upstream, err := net.Dial("tcp", makeIcecastUrl(masterAddr))
+	if err != nil {
+		log.Printf("source: dial upstream: %v", err)
+		sendErr(err)
+		return
+	}
+	defer upstream.Close()
+	if err := r.Write(upstream); err != nil {
+		log.Printf("source: write upstream request: %v", err)
+		sendErr(err)
 		return
 	}
 
@@ -131,7 +188,7 @@ func (h *HttpRedirector) serveSource(w http.ResponseWriter, r *http.Request) {
 	conn, _, err := w.(http.Hijacker).Hijack()
 	if err != nil {
 		log.Printf("source: hijack failed: %v", err)
-		http.Error(w, err.Error(), http.StatusServiceUnavailable)
+		sendErr(err)
 		return
 	}
 	defer conn.Close()
@@ -139,20 +196,6 @@ func (h *HttpRedirector) serveSource(w http.ResponseWriter, r *http.Request) {
 		log.Printf("source: could not reset deadline: %v", err)
 	}
 
-	// Create the upstream connection, and write the original
-	// request to it as-is (the URL path on the backend is the
-	// same, and the headers do not need to change).
-	upstream, err := net.Dial("tcp", makeIcecastUrl(masterAddr))
-	if err != nil {
-		log.Printf("source: dial upstream: %v", err)
-		return
-	}
-	defer upstream.Close()
-	if err := r.Write(upstream); err != nil {
-		log.Printf("source: write upstream request: %v", err)
-		return
-	}
-
 	// Start two copiers, one for the source data, one for the
 	// replies. Wait until both are done.
 	var wg sync.WaitGroup
@@ -224,10 +267,12 @@ func (h *HttpRedirector) Run(addr, staticDir, templateDir string) {
 	wraph := handlers.GZIPHandler(mux, nil)
 	logopts := handlers.NewLogOptions(nil, handlers.Lshort)
 	wraph = handlers.LogHandler(wraph, logopts)
+	wraph = statsHandler(wraph, h.stats)
 
 	// Serve SOURCE requests bypassing the logging and gzip
 	// handlers: since they wrap the ResponseWriter, we would be
 	// unable to hijack the underlying connection for proxying.
+	// TODO: look into using WrapWriter.
 	rooth := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		if r.Method == "SOURCE" {
 			h.serveSource(w, r)