diff --git a/server/djrandom/frontend/frontend.py b/server/djrandom/frontend/frontend.py
index 8608c80468e3870d8ef647bcd2397b350fee6787..8129be6152caf1988b177754f9188d248a802f54 100644
--- a/server/djrandom/frontend/frontend.py
+++ b/server/djrandom/frontend/frontend.py
@@ -39,6 +39,13 @@ def run_frontend(opts):
 
     # Start the WSGI profiling middleware, if requested.
     if opts.profile:
+        from djrandom.frontend.latency import LatencyProfilerMiddleware
+        app.wsgi_app = LatencyProfilerMiddleware(
+            app.wsgi_app,
+            ['/json/album', '/json/song', '/album_image',
+             '/json/playlist/get', '/json/playlist/by_title',
+             '/json/playlist/list', '/user/activate'])
+
         from repoze.profile.profiler import AccumulatingProfileMiddleware
         app.wsgi_app = AccumulatingProfileMiddleware(
             app.wsgi_app,
diff --git a/server/djrandom/frontend/latency.py b/server/djrandom/frontend/latency.py
new file mode 100644
index 0000000000000000000000000000000000000000..b8f3a7ea6a6302647b46023695c512603d1b183f
--- /dev/null
+++ b/server/djrandom/frontend/latency.py
@@ -0,0 +1,81 @@
+import collections
+import time
+import pygooglechart
+
+
+LATENCY_BUCKETS = [
+    1, 2, 5,
+    10, 20, 50,
+    100, 200, 500,
+    ]
+
+N_BUCKETS = len(LATENCY_BUCKETS) + 1
+
+
+def _build_buckets():
+    return [0] * N_BUCKETS
+
+def _percent(counts):
+    tot = float(sum(counts))
+    return [(x, 100 * x / tot) for x in counts]
+
+
+class LatencyProfilerMiddleware(object):
+
+    def __init__(self, app, urls=[]):
+        self._app = app
+        self._urls = sorted(urls, key=lambda x: len(x), reverse=True)
+        self._buckets = collections.defaultdict(_build_buckets)
+
+    def __call__(self, environ, start_response):
+        start = time.time()
+        url = environ['PATH_INFO']
+        if url.endswith('/__latency__'):
+            return self.handler(environ, start_response)
+
+        try:
+            return self._app(environ, start_response)
+        finally:
+            elapsed_ms = 1000 * (time.time() - start)
+            for url_prefix in self._urls:
+                if url.startswith(url_prefix):
+                    url = url_prefix
+                    break
+            for bkt, threshold in enumerate(LATENCY_BUCKETS):
+                if elapsed_ms < threshold:
+                    break
+            else:
+                bkt = len(LATENCY_BUCKETS)
+            self._buckets[url][bkt] += 1
+
+    def handler(self, environ, start_response):
+        headers = [('Content-type', 'text/html')]
+        start_response('200 OK', headers)
+
+        result = [
+            '<!doctype html>\n'
+            '<html><head><title>Latency Breakdown</title>\n'
+            '<style type="text/css">\n'
+            'body { background: white; font-family: sans-serif; }\n'
+            '</style></head><body>\n'
+            '<h3>Latency report by URL</h3>\n'
+            '<table><thead><tr>'
+            '<th rowspan="2">URL</th>',
+            '<th colspan="%d">Latencies (ms)</th>' % N_BUCKETS,
+            '</tr><tr>'
+            ]
+
+        for ms in enumerate(LATENCY_BUCKETS):
+            result.append('<th>&lt;%d</th>' % ms)
+        result.append('<th>&gt;%d</th>' % ms)
+        result.append('</tr></thead><tbody>')
+
+        for url in sorted(self._buckets.keys()):
+            result.append('<tr><td class="url">%s</td>' % url)
+            for count, pct in _percent(self._buckets[url]):
+                result.append('<td>%d (%.3g%%)</td>' % (count, pct))
+            result.append('</tr>')
+
+        result.append('</tbody></table>')
+        result.append('</body></html>\n')
+        return result