diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java
index 1e68254c32a58e04f44d88fc5d200879146fde36..1cb635ad06825b8642cabc4de739ec8131a12f51 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java
@@ -2,11 +2,14 @@ package net.i2p.router.web;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
 
 import net.i2p.router.RouterContext;
 
 public class NetDbHelper {
     private RouterContext _context;
+    private Writer _out;
     /**
      * Configure this bean to query a particular router context
      *
@@ -23,13 +26,21 @@ public class NetDbHelper {
     
     public NetDbHelper() {}
     
+    public void setWriter(Writer writer) { _out = writer; }
+    
     public String getNetDbSummary() {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream(32*1024);
         try {
-            _context.netDb().renderStatusHTML(baos);
+            if (_out != null) {
+                _context.netDb().renderStatusHTML(_out);
+                return "";
+            } else {
+                ByteArrayOutputStream baos = new ByteArrayOutputStream(32*1024);
+                _context.netDb().renderStatusHTML(new OutputStreamWriter(baos));
+                return new String(baos.toByteArray());
+            }
         } catch (IOException ioe) {
             ioe.printStackTrace();
+            return "";
         }
-        return new String(baos.toByteArray());
     }
 }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/OldConsoleHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/OldConsoleHelper.java
index 88f4c86c6e572deec795786c9b71fb1325a02a0d..c6d9a743090c1e1a7d44138b32cf420113425589 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/OldConsoleHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/OldConsoleHelper.java
@@ -2,6 +2,8 @@ package net.i2p.router.web;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
 import java.util.List;
 
 import net.i2p.router.RouterContext;
@@ -9,6 +11,7 @@ import net.i2p.router.admin.StatsGenerator;
 
 public class OldConsoleHelper {
     private RouterContext _context;
+    private Writer _out;
     /**
      * Configure this bean to query a particular router context
      *
@@ -25,11 +28,20 @@ public class OldConsoleHelper {
     
     public OldConsoleHelper() {}
     
+    public void setWriter(Writer writer) { 
+        _out = writer; 
+    }
+    
     public String getConsole() {
         try {
-            ByteArrayOutputStream baos = new ByteArrayOutputStream(128*1024);
-            _context.router().renderStatusHTML(baos);
-            return baos.toString();
+            if (_out != null) {
+                _context.router().renderStatusHTML(_out);
+                return "";
+            } else {
+                ByteArrayOutputStream baos = new ByteArrayOutputStream(128*1024);
+                _context.router().renderStatusHTML(new OutputStreamWriter(baos));
+                return baos.toString();
+            }
         } catch (IOException ioe) {
             return "<b>Error rending the console</b>";
         }
@@ -38,9 +50,14 @@ public class OldConsoleHelper {
     public String getStats() {
         StatsGenerator gen = new StatsGenerator(_context);
         try {
-            ByteArrayOutputStream baos = new ByteArrayOutputStream(32*1024);
-            gen.generateStatsPage(baos);
-            return baos.toString();
+            if (_out != null) {
+                gen.generateStatsPage(_out);
+                return "";
+            } else {
+                ByteArrayOutputStream baos = new ByteArrayOutputStream(32*1024);
+                gen.generateStatsPage(new OutputStreamWriter(baos));
+                return baos.toString();
+            }
         } catch (IOException ioe) {
             return "<b>Error rending the console</b>";
         }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ProfilesHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ProfilesHelper.java
index d8b0b5b96c36a41162314b902ef7a8dd029e3cea..4db1010a570d160d296da60c1cf02d907d10a195 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ProfilesHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ProfilesHelper.java
@@ -2,6 +2,7 @@ package net.i2p.router.web;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.OutputStreamWriter;
 
 import net.i2p.router.RouterContext;
 
@@ -26,7 +27,7 @@ public class ProfilesHelper {
     public String getProfileSummary() {
         ByteArrayOutputStream baos = new ByteArrayOutputStream(16*1024);
         try {
-            _context.profileOrganizer().renderStatusHTML(baos);
+            _context.profileOrganizer().renderStatusHTML(new OutputStreamWriter(baos));
         } catch (IOException ioe) {
             ioe.printStackTrace();
         }
@@ -36,7 +37,7 @@ public class ProfilesHelper {
     public String getShitlistSummary() {
         ByteArrayOutputStream baos = new ByteArrayOutputStream(4*1024);
         try {
-            _context.shitlist().renderStatusHTML(baos);
+            _context.shitlist().renderStatusHTML(new OutputStreamWriter(baos));
         } catch (IOException ioe) {
             ioe.printStackTrace();
         }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
index e55ff9683485eb6155287bdd367b56e53327fd31..5d920fa07047e420655040c58b3f9969f36b2b00 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
@@ -2,6 +2,7 @@ package net.i2p.router.web;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.OutputStreamWriter;
 import java.text.DecimalFormat;
 
 import net.i2p.data.DataHelper;
@@ -334,7 +335,7 @@ public class SummaryHelper {
     public String getDestinations() {
         ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
         try {
-            _context.clientManager().renderStatusHTML(baos);
+            _context.clientManager().renderStatusHTML(new OutputStreamWriter(baos));
             return new String(baos.toByteArray());
         } catch (IOException ioe) {
             _context.logManager().getLog(SummaryHelper.class).error("Error rendering client info", ioe);
@@ -397,8 +398,7 @@ public class SummaryHelper {
         if (_context == null) 
             return "0ms";
         
-        Rate delayRate = _context.statManager().getRate("transport.sendProcessingTime").getRate(60*1000);
-        return ((int)delayRate.getAverageValue()) + "ms";
+        return _context.throttle().getMessageDelay() + "ms";
     }
     
     /**
@@ -410,7 +410,6 @@ public class SummaryHelper {
         if (_context == null) 
             return "0ms";
         
-        Rate lagRate = _context.statManager().getRate("tunnel.testSuccessTime").getRate(10*60*1000);
-        return ((int)lagRate.getAverageValue()) + "ms";
+        return _context.throttle().getTunnelLag() + "ms";
     }
 }
\ No newline at end of file
diff --git a/apps/routerconsole/jsp/netdb.jsp b/apps/routerconsole/jsp/netdb.jsp
index aac78a4d15d6ca3e18f7a4e11609c302caf1ea8c..0933c7b22cde553b29b6f59bf6bdb20832c7df0c 100644
--- a/apps/routerconsole/jsp/netdb.jsp
+++ b/apps/routerconsole/jsp/netdb.jsp
@@ -13,6 +13,7 @@
 <div class="main" id="main">
  <jsp:useBean class="net.i2p.router.web.NetDbHelper" id="netdbHelper" scope="request" />
  <jsp:setProperty name="netdbHelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
+ <jsp:setProperty name="netdbHelper" property="writer" value="<%=out%>" />
  <jsp:getProperty name="netdbHelper" property="netDbSummary" />
 </div>
 
diff --git a/apps/routerconsole/jsp/oldconsole.jsp b/apps/routerconsole/jsp/oldconsole.jsp
index bd501a471a103c01358aa5ce78fb7c419607fb50..3d1ec31cd0762b43fccc3b4b512c2980060f15e5 100644
--- a/apps/routerconsole/jsp/oldconsole.jsp
+++ b/apps/routerconsole/jsp/oldconsole.jsp
@@ -12,6 +12,7 @@
 
 <jsp:useBean class="net.i2p.router.web.OldConsoleHelper" id="conhelper" scope="request" />
 <jsp:setProperty name="conhelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
+<jsp:setProperty name="conhelper" property="writer" value="<%=out%>" />
 
 <div class="main" id="main">
  <jsp:getProperty name="conhelper" property="console" />
diff --git a/apps/routerconsole/jsp/oldstats.jsp b/apps/routerconsole/jsp/oldstats.jsp
index bf19e4cc4d1c56ea5c9ad2cc0eacdeb9be349c34..15a39fc53a340203a99487ca862e920a66b21a44 100644
--- a/apps/routerconsole/jsp/oldstats.jsp
+++ b/apps/routerconsole/jsp/oldstats.jsp
@@ -12,6 +12,7 @@
 
 <jsp:useBean class="net.i2p.router.web.OldConsoleHelper" id="oldhelper" scope="request" />
 <jsp:setProperty name="oldhelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
+<jsp:setProperty name="oldhelper" property="writer" value="<%=out%>" />
 
 <div class="main" id="main">
  <jsp:getProperty name="oldhelper" property="stats" />
diff --git a/router/java/src/net/i2p/router/ClientManagerFacade.java b/router/java/src/net/i2p/router/ClientManagerFacade.java
index 980e6aeea322d79943b186e9f11bc61118b755ff..de3b8e51c5658143eb4932344726e5dbb175c4d0 100644
--- a/router/java/src/net/i2p/router/ClientManagerFacade.java
+++ b/router/java/src/net/i2p/router/ClientManagerFacade.java
@@ -9,7 +9,7 @@ package net.i2p.router;
  */
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 
 import net.i2p.data.Destination;
 import net.i2p.data.Hash;
@@ -70,7 +70,7 @@ public abstract class ClientManagerFacade implements Service {
      *
      */
     public abstract SessionConfig getClientSessionConfig(Destination dest);
-    public void renderStatusHTML(OutputStream out) throws IOException { }
+    public void renderStatusHTML(Writer out) throws IOException { }
 }
 
 class DummyClientManagerFacade extends ClientManagerFacade {
diff --git a/router/java/src/net/i2p/router/CommSystemFacade.java b/router/java/src/net/i2p/router/CommSystemFacade.java
index 5c6f42878afeac76f66934729f4646f7d9ee89ad..9a03ebcb914f9b6813051df0bc7cf9786bd53f01 100644
--- a/router/java/src/net/i2p/router/CommSystemFacade.java
+++ b/router/java/src/net/i2p/router/CommSystemFacade.java
@@ -9,7 +9,7 @@ package net.i2p.router;
  */
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -23,7 +23,7 @@ import java.util.Set;
 public abstract class CommSystemFacade implements Service {
     public abstract void processMessage(OutNetMessage msg);
     
-    public void renderStatusHTML(OutputStream out) throws IOException { }
+    public void renderStatusHTML(Writer out) throws IOException { }
     
     /** Create the set of RouterAddress structures based on the router's config */
     public Set createAddresses() { return new HashSet(); }
diff --git a/router/java/src/net/i2p/router/JobQueue.java b/router/java/src/net/i2p/router/JobQueue.java
index c714d12858479c634e0fc541c808809fd52d5d1b..beee90f381f63c279b1eb7863de8e3b1d3713ead 100644
--- a/router/java/src/net/i2p/router/JobQueue.java
+++ b/router/java/src/net/i2p/router/JobQueue.java
@@ -9,7 +9,7 @@ package net.i2p.router;
  */
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
@@ -558,18 +558,18 @@ public class JobQueue {
     // the remainder are utility methods for dumping status info
     ////
     
-    public void renderStatusHTML(OutputStream out) throws IOException {
+    public void renderStatusHTML(Writer out) throws IOException {
         ArrayList readyJobs = null;
         ArrayList timedJobs = null;
         ArrayList activeJobs = new ArrayList(1);
         ArrayList justFinishedJobs = new ArrayList(4);
-        out.write("<!-- jobQueue rendering -->\n".getBytes());
+        out.write("<!-- jobQueue rendering -->\n");
         out.flush();
         synchronized (_readyJobs) { readyJobs = new ArrayList(_readyJobs); }
-        out.write("<!-- jobQueue rendering: after readyJobs sync -->\n".getBytes());
+        out.write("<!-- jobQueue rendering: after readyJobs sync -->\n");
         out.flush();
         synchronized (_timedJobs) { timedJobs = new ArrayList(_timedJobs); }
-        out.write("<!-- jobQueue rendering: after timedJobs sync -->\n".getBytes());
+        out.write("<!-- jobQueue rendering: after timedJobs sync -->\n");
         out.flush();
         int numRunners = 0;
         synchronized (_queueRunners) {
@@ -586,7 +586,7 @@ public class JobQueue {
             numRunners = _queueRunners.size();
         }
         
-        out.write("<!-- jobQueue rendering: after queueRunners sync -->\n".getBytes());
+        out.write("<!-- jobQueue rendering: after queueRunners sync -->\n");
         out.flush();
         
         StringBuffer buf = new StringBuffer(32*1024);
@@ -631,15 +631,15 @@ public class JobQueue {
         }
         buf.append("</ol>\n");
         
-        out.write("<!-- jobQueue rendering: after main buffer, before stats -->\n".getBytes());
+        out.write("<!-- jobQueue rendering: after main buffer, before stats -->\n");
         out.flush();
         
         getJobStats(buf);
         
-        out.write("<!-- jobQueue rendering: after stats -->\n".getBytes());
+        out.write("<!-- jobQueue rendering: after stats -->\n");
         out.flush();
         
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
     }
     
     /** render the HTML for the job stats */
diff --git a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java
index 371cf26775e4c8e0846c12937fa698708a4b5261..9c6ef0a8b57ce8a60ec922591707d0798f4aad8b 100644
--- a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java
@@ -9,7 +9,7 @@ package net.i2p.router;
  */
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -95,5 +95,5 @@ class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade {
     
     public Set findNearestRouters(Hash key, int maxNumRouters, Set peersToIgnore) { return new HashSet(_routers.values()); }
 
-    public void renderStatusHTML(OutputStream out) throws IOException {}
+    public void renderStatusHTML(Writer out) throws IOException {}
 }
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index d835aa1766bd45b02657741dff0d3b8cc49ccf19..7b6c009c3b4c46d153d50c1c2393675319ca8453 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -12,7 +12,7 @@ import java.io.File;
 import java.io.FileOutputStream;
 import java.io.FileInputStream;
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 import java.text.DecimalFormat;
 import java.util.Calendar;
 import java.util.Date;
@@ -315,8 +315,8 @@ public class Router {
         _context.inNetMessagePool().registerHandlerJobBuilder(TunnelMessage.MESSAGE_TYPE, new TunnelMessageHandler(_context));
     }
     
-    public void renderStatusHTML(OutputStream out) throws IOException {
-        out.write(("<h1>Router console</h1>\n" +
+    public void renderStatusHTML(Writer out) throws IOException {
+        out.write("<h1>Router console</h1>\n" +
                    "<i><a href=\"/oldconsole.jsp\">console</a> | <a href=\"/oldstats.jsp\">stats</a></i><br>\n" +
                    "<form action=\"/oldconsole.jsp\">" +
                    "<select name=\"go\" onChange='location.href=this.value'>" +
@@ -331,7 +331,7 @@ public class Router {
                    "<option value=\"/oldconsole.jsp#netdb\">Network Database</option>\n" +
                    "<option value=\"/oldconsole.jsp#logs\">Log messages</option>\n" +
                    "</select> <input type=\"submit\" value=\"GO\" /> </form>" +
-                   "<hr />\n").getBytes());
+                   "<hr />\n");
 
         StringBuffer buf = new StringBuffer(32*1024);
         
@@ -453,39 +453,39 @@ public class Router {
         buf.append("trying to transfer data.  Lifetime averages count how many elephants there are on the moon [like anyone reads this text]</i>");
         buf.append("\n");
         
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
         
         _context.bandwidthLimiter().renderStatusHTML(out);
 
-        out.write("<hr /><a name=\"clients\"> </a>\n".getBytes());
+        out.write("<hr /><a name=\"clients\"> </a>\n");
         
         _context.clientManager().renderStatusHTML(out);
         
-        out.write("\n<hr /><a name=\"transports\"> </a>\n".getBytes());
+        out.write("\n<hr /><a name=\"transports\"> </a>\n");
         
         _context.commSystem().renderStatusHTML(out);
         
-        out.write("\n<hr /><a name=\"profiles\"> </a>\n".getBytes());
+        out.write("\n<hr /><a name=\"profiles\"> </a>\n");
         
         _context.peerManager().renderStatusHTML(out);
         
-        out.write("\n<hr /><a name=\"tunnels\"> </a>\n".getBytes());
+        out.write("\n<hr /><a name=\"tunnels\"> </a>\n");
         
         _context.tunnelManager().renderStatusHTML(out);
         
-        out.write("\n<hr /><a name=\"jobs\"> </a>\n".getBytes());
+        out.write("\n<hr /><a name=\"jobs\"> </a>\n");
         
         _context.jobQueue().renderStatusHTML(out);
         
-        out.write("\n<hr /><a name=\"shitlist\"> </a>\n".getBytes());
+        out.write("\n<hr /><a name=\"shitlist\"> </a>\n");
         
         _context.shitlist().renderStatusHTML(out);
         
-        out.write("\n<hr /><a name=\"pending\"> </a>\n".getBytes());
+        out.write("\n<hr /><a name=\"pending\"> </a>\n");
         
         _context.messageRegistry().renderStatusHTML(out);
         
-        out.write("\n<hr /><a name=\"netdb\"> </a>\n".getBytes());
+        out.write("\n<hr /><a name=\"netdb\"> </a>\n");
         
         _context.netDb().renderStatusHTML(out);
         
@@ -500,7 +500,7 @@ public class Router {
             buf.append("</pre></td></tr>\n");
         }
         buf.append("</table>\n");
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
     }
     
     private static int MAX_MSG_LENGTH = 120;
diff --git a/router/java/src/net/i2p/router/RouterThrottle.java b/router/java/src/net/i2p/router/RouterThrottle.java
index 45d8bf9f545a8a7ccc9b30e5eb425d4d2be58a80..4b22394a0dbc56f32a161c26589531d0ecfeb975 100644
--- a/router/java/src/net/i2p/router/RouterThrottle.java
+++ b/router/java/src/net/i2p/router/RouterThrottle.java
@@ -31,4 +31,15 @@ public interface RouterThrottle {
      *
      */
     public boolean acceptNetDbLookupRequest(Hash key);
+    
+    /** How backed up we are at the moment processing messages (in milliseconds) */
+    public long getMessageDelay();
+    /** How backed up our tunnels are at the moment (in milliseconds) */
+    public long getTunnelLag();
+    /** 
+     * How much faster (or if negative, slower) we are receiving data as 
+     * opposed to our longer term averages?
+     *
+     */
+    public double getInboundRateDelta();
 }
diff --git a/router/java/src/net/i2p/router/RouterThrottleImpl.java b/router/java/src/net/i2p/router/RouterThrottleImpl.java
index 05fafe99329b1767e0e286eaaa5edaf9849aa477..5e8a6f556e7dbc52aff6b75a1cc7c374930ec923 100644
--- a/router/java/src/net/i2p/router/RouterThrottleImpl.java
+++ b/router/java/src/net/i2p/router/RouterThrottleImpl.java
@@ -154,5 +154,35 @@ class RouterThrottleImpl implements RouterThrottle {
         return true;
     }
     
+    
+    public long getMessageDelay() {
+        Rate delayRate = _context.statManager().getRate("transport.sendProcessingTime").getRate(60*1000);
+        return (long)delayRate.getAverageValue();
+    }
+    
+    public long getTunnelLag() {
+        Rate lagRate = _context.statManager().getRate("tunnel.testSuccessTime").getRate(10*60*1000);
+        return (long)lagRate.getAverageValue();
+    }
+    
+    public double getInboundRateDelta() {
+        RateStat receiveRate = _context.statManager().getRate("transport.sendMessageSize");
+        double nowBps = getBps(receiveRate.getRate(60*1000));
+        double fiveMinBps = getBps(receiveRate.getRate(5*60*1000));
+        double hourBps = getBps(receiveRate.getRate(60*60*1000));
+        double dailyBps = getBps(receiveRate.getRate(24*60*60*1000));
+        
+        if (nowBps < 0) return 0;
+        if (dailyBps > 0) return nowBps - dailyBps;
+        if (hourBps > 0) return nowBps - hourBps;
+        if (fiveMinBps > 0) return nowBps - fiveMinBps;
+        return 0;
+    }
+    private double getBps(Rate rate) {
+        if (rate == null) return -1;
+        double bytes = rate.getLastTotalValue();
+        return (bytes*1000.0d)/rate.getPeriod(); 
+    }
+    
     protected RouterContext getContext() { return _context; }
 }
diff --git a/router/java/src/net/i2p/router/Service.java b/router/java/src/net/i2p/router/Service.java
index 0fe2f4639dcc20782e6af41b6c2606f800f4aaef..31340ebee2516e3a4e7eb9681a546e7fdff99148 100644
--- a/router/java/src/net/i2p/router/Service.java
+++ b/router/java/src/net/i2p/router/Service.java
@@ -9,7 +9,7 @@ package net.i2p.router;
  */
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 
 /**
  * Define the manageable service interface for the subsystems in the I2P router
@@ -37,5 +37,5 @@ public interface Service {
      */
     public void restart();
     
-    public void renderStatusHTML(OutputStream out) throws IOException;
+    public void renderStatusHTML(Writer out) throws IOException;
 }
diff --git a/router/java/src/net/i2p/router/SessionKeyPersistenceHelper.java b/router/java/src/net/i2p/router/SessionKeyPersistenceHelper.java
index 03de1bfd9039e56c78185e0f7ff60c997b0d5cf3..93a35aba5f9288a458c42734f85fd6f44a9837c0 100644
--- a/router/java/src/net/i2p/router/SessionKeyPersistenceHelper.java
+++ b/router/java/src/net/i2p/router/SessionKeyPersistenceHelper.java
@@ -4,7 +4,7 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 
 import net.i2p.crypto.PersistentSessionKeyManager;
 import net.i2p.crypto.SessionKeyManager;
@@ -97,7 +97,7 @@ public class SessionKeyPersistenceHelper implements Service {
         }
     }
     
-    public void renderStatusHTML(OutputStream out) { }
+    public void renderStatusHTML(Writer out) { }
     
     private class SessionKeyWriterJob extends JobImpl {
         public SessionKeyWriterJob() {
diff --git a/router/java/src/net/i2p/router/Shitlist.java b/router/java/src/net/i2p/router/Shitlist.java
index cda6b21eaf4eec143995ada208e4d2d13a77b4b0..adca46ec78669f4a52b9f48d06d8527ad6126684 100644
--- a/router/java/src/net/i2p/router/Shitlist.java
+++ b/router/java/src/net/i2p/router/Shitlist.java
@@ -9,7 +9,7 @@ package net.i2p.router;
  */
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -123,7 +123,7 @@ public class Shitlist {
     }
 
     
-    public void renderStatusHTML(OutputStream out) throws IOException {
+    public void renderStatusHTML(Writer out) throws IOException {
         StringBuffer buf = new StringBuffer(1024);
         buf.append("<h2>Shitlist</h2>");
         Map shitlist = null;
@@ -152,6 +152,6 @@ public class Shitlist {
             buf.append("</li>\n");
         }
         buf.append("</ul>\n");
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
     }
 }
diff --git a/router/java/src/net/i2p/router/StatisticsManager.java b/router/java/src/net/i2p/router/StatisticsManager.java
index bf4ed721885dd61113fb652f02013da324429f70..e1ae639305503d6cbef6f042b9c6efce911ed68a 100644
--- a/router/java/src/net/i2p/router/StatisticsManager.java
+++ b/router/java/src/net/i2p/router/StatisticsManager.java
@@ -8,7 +8,7 @@ package net.i2p.router;
  *
  */
 
-import java.io.OutputStream;
+import java.io.Writer;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
 import java.util.Locale;
@@ -107,32 +107,33 @@ public class StatisticsManager implements Service {
             includeRate("jobQueue.jobLag", stats, new long[] { 60*1000, 60*60*1000 });
             includeRate("jobQueue.jobRun", stats, new long[] { 60*1000, 60*60*1000 });
             includeRate("crypto.elGamal.encrypt", stats, new long[] { 60*1000, 60*60*1000 });
-            includeRate("crypto.garlic.decryptFail", stats, new long[] { 60*60*1000, 24*60*60*1000 });
+            //includeRate("crypto.garlic.decryptFail", stats, new long[] { 60*60*1000, 24*60*60*1000 });
             includeRate("tunnel.unknownTunnelTimeLeft", stats, new long[] { 60*60*1000, 24*60*60*1000 });
             includeRate("jobQueue.readyJobs", stats, new long[] { 60*1000, 60*60*1000 });
             //includeRate("jobQueue.droppedJobs", stats, new long[] { 60*60*1000, 24*60*60*1000 });
             includeRate("inNetPool.dropped", stats, new long[] { 60*60*1000, 24*60*60*1000 });
             includeRate("tunnel.participatingTunnels", stats, new long[] { 5*60*1000, 60*60*1000 });
             includeRate("tunnel.testSuccessTime", stats, new long[] { 60*60*1000l, 24*60*60*1000l });
-            includeRate("tunnel.outboundMessagesProcessed", stats, new long[] { 10*60*1000, 60*60*1000 });
-            includeRate("tunnel.inboundMessagesProcessed", stats, new long[] { 10*60*1000, 60*60*1000 });
+            //includeRate("tunnel.outboundMessagesProcessed", stats, new long[] { 10*60*1000, 60*60*1000 });
+            //includeRate("tunnel.inboundMessagesProcessed", stats, new long[] { 10*60*1000, 60*60*1000 });
             includeRate("tunnel.participatingMessagesProcessed", stats, new long[] { 10*60*1000, 60*60*1000 });
-            includeRate("tunnel.expiredAfterAcceptTime", stats, new long[] { 10*60*1000l, 60*60*1000l, 24*60*60*1000l });
+            includeRate("tunnel.participatingMessagesProcessedActive", stats, new long[] { 10*60*1000, 60*60*1000 });
+            //includeRate("tunnel.expiredAfterAcceptTime", stats, new long[] { 10*60*1000l, 60*60*1000l, 24*60*60*1000l });
             includeRate("tunnel.bytesAllocatedAtAccept", stats, new long[] { 10*60*1000l, 60*60*1000l, 24*60*60*1000l });
             includeRate("netDb.lookupsReceived", stats, new long[] { 5*60*1000, 60*60*1000 });
             includeRate("netDb.lookupsHandled", stats, new long[] { 5*60*1000, 60*60*1000 });
             includeRate("netDb.lookupsMatched", stats, new long[] { 5*60*1000, 60*60*1000 });
-            includeRate("netDb.storeSent", stats, new long[] { 5*60*1000, 60*60*1000 });
+            //includeRate("netDb.storeSent", stats, new long[] { 5*60*1000, 60*60*1000 });
             includeRate("netDb.successPeers", stats, new long[] { 60*60*1000 });
             includeRate("netDb.failedPeers", stats, new long[] { 60*60*1000 });
-            includeRate("router.throttleNetDbDoSSend", stats, new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
-            includeRate("router.throttleNetDbDoS", stats, new long[] { 10*60*1000, 60*60*1000 });
+            //includeRate("router.throttleNetDbDoSSend", stats, new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
+            //includeRate("router.throttleNetDbDoS", stats, new long[] { 10*60*1000, 60*60*1000 });
             //includeRate("netDb.searchCount", stats, new long[] { 3*60*60*1000});
             //includeRate("netDb.searchMessageCount", stats, new long[] { 5*60*1000, 10*60*1000, 60*60*1000 });
             //includeRate("inNetMessage.timeToDiscard", stats, new long[] { 5*60*1000, 10*60*1000, 60*60*1000 });
             //includeRate("outNetMessage.timeToDiscard", stats, new long[] { 5*60*1000, 10*60*1000, 60*60*1000 });
-            includeRate("router.throttleNetworkCause", stats, new long[] { 10*60*1000, 60*60*1000 });
-            includeRate("transport.receiveMessageSize", stats, new long[] { 5*60*1000, 60*60*1000 });
+            //includeRate("router.throttleNetworkCause", stats, new long[] { 10*60*1000, 60*60*1000 });
+            //includeRate("transport.receiveMessageSize", stats, new long[] { 5*60*1000, 60*60*1000 });
             //includeRate("transport.sendMessageSize", stats, new long[] { 5*60*1000, 60*60*1000 });
             //includeRate("transport.sendMessageSmall", stats, new long[] { 5*60*1000, 60*60*1000 });
             //includeRate("transport.sendMessageMedium", stats, new long[] { 5*60*1000, 60*60*1000 });
@@ -141,6 +142,10 @@ public class StatisticsManager implements Service {
             //includeRate("transport.receiveMessageMedium", stats, new long[] { 5*60*1000, 60*60*1000 });
             //includeRate("transport.receiveMessageLarge", stats, new long[] { 5*60*1000, 60*60*1000 });
             includeRate("client.sendAckTime", stats, new long[] { 60*60*1000, 24*60*60*1000l }, true);
+            includeRate("client.sendsPerFailure", stats, new long[] { 60*60*1000, 24*60*60*1000l }, true);
+            includeRate("client.timeoutCongestionTunnel", stats, new long[] { 60*60*1000, 24*60*60*1000l }, true);
+            includeRate("client.timeoutCongestionMessage", stats, new long[] { 60*60*1000, 24*60*60*1000l }, true);
+            includeRate("client.timeoutCongestionInbound", stats, new long[] { 60*60*1000, 24*60*60*1000l }, true);
             stats.setProperty("stat_uptime", DataHelper.formatDuration(_context.router().getUptime()));
             stats.setProperty("stat__rateKey", "avg;maxAvg;pctLifetime;[sat;satLim;maxSat;maxSatLim;][num;lifetimeFreq;maxFreq]");
             _log.debug("Publishing peer rankings");
@@ -266,5 +271,5 @@ public class StatisticsManager implements Service {
     private final String num(double num) { synchronized (_fmt) { return _fmt.format(num); } }
     private final String pct(double num) { synchronized (_pct) { return _pct.format(num); } }
    
-    public void renderStatusHTML(OutputStream out) { }
+    public void renderStatusHTML(Writer out) { }
 }
diff --git a/router/java/src/net/i2p/router/admin/AdminManager.java b/router/java/src/net/i2p/router/admin/AdminManager.java
index 06d8de5941961a28d3bb017ae62fd4b891695cb3..531b5efce63a82a84a3b18cda997fa15bd8856bc 100644
--- a/router/java/src/net/i2p/router/admin/AdminManager.java
+++ b/router/java/src/net/i2p/router/admin/AdminManager.java
@@ -1,6 +1,6 @@
 package net.i2p.router.admin;
 
-import java.io.OutputStream;
+import java.io.Writer;
 
 import net.i2p.router.RouterContext;
 import net.i2p.router.Service;
@@ -20,7 +20,7 @@ public class AdminManager implements Service {
         _log = context.logManager().getLog(AdminManager.class);
     }
     
-    public void renderStatusHTML(OutputStream out) { }
+    public void renderStatusHTML(Writer out) { }
     
     public void shutdown() {
         if (_listener != null) {
diff --git a/router/java/src/net/i2p/router/admin/AdminRunner.java b/router/java/src/net/i2p/router/admin/AdminRunner.java
index d8424e70b49ea49a3e9f1798d48bb9a0182c0a55..0ded465eb314526bd5045f854440be462c38641c 100644
--- a/router/java/src/net/i2p/router/admin/AdminRunner.java
+++ b/router/java/src/net/i2p/router/admin/AdminRunner.java
@@ -5,6 +5,7 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
+import java.io.OutputStreamWriter;
 import java.net.Socket;
 import java.util.Iterator;
 import java.util.Set;
@@ -47,7 +48,7 @@ class AdminRunner implements Runnable {
         } else if ( (command.indexOf("routerStats.html") >= 0) || (command.indexOf("oldstats.jsp") >= 0) ) {
             try {
                 out.write("HTTP/1.1 200 OK\nConnection: close\nCache-control: no-cache\nContent-type: text/html\n\n".getBytes());
-                _generator.generateStatsPage(out);
+                _generator.generateStatsPage(new OutputStreamWriter(out));
                 out.close();
             } catch (IOException ioe) {
                 if (_log.shouldLog(Log.WARN))
@@ -61,7 +62,7 @@ class AdminRunner implements Runnable {
         } else if (true || command.indexOf("routerConsole.html") > 0) {
             try {
                 out.write("HTTP/1.1 200 OK\nConnection: close\nCache-control: no-cache\nContent-type: text/html\n\n".getBytes());
-                _context.router().renderStatusHTML(out);
+                _context.router().renderStatusHTML(new OutputStreamWriter(out));
                 out.close();
             } catch (IOException ioe) {
                 if (_log.shouldLog(Log.WARN))
diff --git a/router/java/src/net/i2p/router/admin/StatsGenerator.java b/router/java/src/net/i2p/router/admin/StatsGenerator.java
index 3a84bad95714ac9c5a2aa35de693512ec03cd9bf..e7bdfd71990dd48073c05503fd32697c45f752a0 100644
--- a/router/java/src/net/i2p/router/admin/StatsGenerator.java
+++ b/router/java/src/net/i2p/router/admin/StatsGenerator.java
@@ -2,6 +2,7 @@ package net.i2p.router.admin;
 
 import java.io.IOException;
 import java.io.OutputStream;
+import java.io.Writer;
 import java.text.DecimalFormat;
 import java.util.Arrays;
 import java.util.Iterator;
@@ -27,13 +28,13 @@ public class StatsGenerator {
         _log = context.logManager().getLog(StatsGenerator.class);
     }
     
-    public void generateStatsPage(OutputStream out) throws IOException {
+    public void generateStatsPage(Writer out) throws IOException {
         StringBuffer buf = new StringBuffer(16*1024);
         buf.append("<h1>Router statistics</h1>");
         buf.append("<i><a href=\"/oldconsole.jsp\">console</a> | <a href=\"/oldstats.jsp\">stats</a></i><hr />");
         buf.append("<form action=\"/oldstats.jsp\">");
         buf.append("<select name=\"go\" onChange='location.href=this.value'>");
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
         buf.setLength(0);
         
         Map groups = _context.statManager().getStatsByGroup();
@@ -50,7 +51,7 @@ public class StatsGenerator {
                 buf.append(stat);
                 buf.append("</option>\n");
             }
-            out.write(buf.toString().getBytes());
+            out.write(buf.toString());
             buf.setLength(0);
         }
         buf.append("</select> <input type=\"submit\" value=\"GO\" />");
@@ -61,7 +62,7 @@ public class StatsGenerator {
         buf.append(DataHelper.formatDuration(uptime));
         buf.append(").  The data gathered is quantized over a 1 minute period, so should just be used as an estimate<p />");
 
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
         buf.setLength(0);
         
         for (Iterator iter = groups.keySet().iterator(); iter.hasNext(); ) {
@@ -73,7 +74,7 @@ public class StatsGenerator {
             buf.append(group);
             buf.append("</a></h2>");
             buf.append("<ul>");
-            out.write(buf.toString().getBytes());
+            out.write(buf.toString());
             buf.setLength(0);
             for (Iterator statIter = stats.iterator(); statIter.hasNext(); ) {
                 String stat = (String)statIter.next();
@@ -86,10 +87,10 @@ public class StatsGenerator {
                     renderFrequency(stat, buf);
                 else
                     renderRate(stat, buf);
-                out.write(buf.toString().getBytes());
+                out.write(buf.toString());
                 buf.setLength(0);
             }
-            out.write("</ul><hr />".getBytes());
+            out.write("</ul><hr />");
         }
     }
     
diff --git a/router/java/src/net/i2p/router/client/ClientManager.java b/router/java/src/net/i2p/router/client/ClientManager.java
index d5ab9c98c315a220cdb5bca3ab91e2423c7e5b1b..abe32939d0d01db588d19acfdebdf894fb9aa075 100644
--- a/router/java/src/net/i2p/router/client/ClientManager.java
+++ b/router/java/src/net/i2p/router/client/ClientManager.java
@@ -9,7 +9,7 @@ package net.i2p.router.client;
  */
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -332,7 +332,7 @@ public class ClientManager {
         }
     }
     
-    public void renderStatusHTML(OutputStream out) throws IOException {
+    public void renderStatusHTML(Writer out) throws IOException {
         StringBuffer buf = new StringBuffer(8*1024);
         buf.append("<u><b>Local destinations</b></u><br />");
         
@@ -376,7 +376,7 @@ public class ClientManager {
         }
         
         buf.append("\n<hr />\n");
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
     }
     
     public void messageReceived(ClientMessage msg) {
diff --git a/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java b/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java
index 43b870aaa99370c899d1c8fef4582c2152024a6a..ac2ee7f74dcb97b4b364b2c5692a3dc5e9ce2ac6 100644
--- a/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java
+++ b/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java
@@ -9,7 +9,7 @@ package net.i2p.router.client;
  */
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 
 import net.i2p.data.Destination;
 import net.i2p.data.Hash;
@@ -158,7 +158,7 @@ public class ClientManagerFacadeImpl extends ClientManagerFacade {
         }
     }
     
-    public void renderStatusHTML(OutputStream out) throws IOException { 
+    public void renderStatusHTML(Writer out) throws IOException { 
         if (_manager != null)
             _manager.renderStatusHTML(out); 
     }
diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageJob.java
index 0bd1d459e304642b6019984d518aea831c739bba..b63b5443d677704220d11e6af3499520087adf6f 100644
--- a/router/java/src/net/i2p/router/message/OutboundClientMessageJob.java
+++ b/router/java/src/net/i2p/router/message/OutboundClientMessageJob.java
@@ -104,7 +104,11 @@ public class OutboundClientMessageJob extends JobImpl {
         ctx.statManager().createRateStat("client.sendMessageSize", "How large are messages sent by the client?", "Client Messages", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
         ctx.statManager().createRateStat("client.sendAttemptAverage", "How many different tunnels do we have to try when sending a client message?", "Client Messages", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
         ctx.statManager().createRateStat("client.sendAckTime", "How long does it take to get an ACK back from a message?", "Client Messages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
-        
+        ctx.statManager().createRateStat("client.sendsPerFailure", "How many send attempts do we make when they all fail?", "Client Messages", new long[] { 60*60*1000l, 24*60*60*1000l });
+        ctx.statManager().createRateStat("client.timeoutCongestionTunnel", "How lagged our tunnels are when a send times out?", "Client Messages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+        ctx.statManager().createRateStat("client.timeoutCongestionMessage", "How fast we process messages locally when a send times out?", "Client Messages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+        ctx.statManager().createRateStat("client.timeoutCongestionInbound", "How much faster we are receiving data than our average bps when a send times out?", "Client Messages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+
         long timeoutMs = OVERALL_TIMEOUT_MS_DEFAULT;
         
         String param = msg.getSenderConfig().getOptions().getProperty(OVERALL_TIMEOUT_MS_PARAM);
@@ -122,7 +126,7 @@ public class OutboundClientMessageJob extends JobImpl {
         }
         
         _overallExpiration = timeoutMs + getContext().clock().now();
-        _status = new OutboundClientMessageStatus(msg);
+        _status = new OutboundClientMessageStatus(ctx, msg);
         _nextStep = new NextStepJob();
         _lookupLeaseSetFailed = new LookupLeaseSetFailedJob();
         _shouldBundle = getShouldBundle();
@@ -423,6 +427,7 @@ public class OutboundClientMessageJob extends JobImpl {
         getContext().clientManager().messageDeliveryStatusUpdate(msg.getFromDestination(), msg.getMessageId(), false);
         getContext().statManager().updateFrequency("client.sendMessageFailFrequency");
         getContext().statManager().addRateData("client.sendAttemptAverage", _status.getNumSent(), sendTime);
+        getContext().statManager().addRateData("client.sendsPerFailure", _status.getNumSent(), sendTime);
     }
     
     /** build the payload clove that will be used for all of the messages, placing the clove in the status structure */
@@ -455,126 +460,6 @@ public class OutboundClientMessageJob extends JobImpl {
             _log.debug(getJobId() + ": Built payload clove with id " + clove.getId());
     }
     
-    /**
-     * Good ol' fashioned struct with the send status
-     *
-     */
-    private class OutboundClientMessageStatus {
-        private ClientMessage _msg;
-        private PayloadGarlicConfig _clove;
-        private LeaseSet _leaseSet;
-        private Set _sent;
-        private int _numLookups;
-        private boolean _success;
-        private boolean _failure;
-        private long _start;
-        private int _previousSent;
-        
-        public OutboundClientMessageStatus(ClientMessage msg) {
-            _msg = msg;
-            _clove = null;
-            _leaseSet = null;
-            _sent = new HashSet(4);
-            _success = false;
-            _failure = false;
-            _numLookups = 0;
-            _previousSent = 0;
-            _start = getContext().clock().now();
-        }
-        
-        /** raw payload */
-        public Payload getPayload() { return _msg.getPayload(); }
-        /** clove, if we've built it */
-        public PayloadGarlicConfig getClove() { return _clove; }
-        public void setClove(PayloadGarlicConfig clove) { _clove = clove; }
-        public ClientMessage getMessage() { return _msg; }
-        /** date we started the process on */
-        public long getStart() { return _start; }
-        
-        public int getNumLookups() { return _numLookups; }
-        public void incrementLookups() { _numLookups++; }
-        public void clearAlreadySent() {
-            synchronized (_sent) {
-                _previousSent += _sent.size();
-                _sent.clear();
-            }
-        }
-        
-        /** who sent the message? */
-        public Destination getFrom() { return _msg.getFromDestination(); }
-        /** who is the message going to? */
-        public Destination getTo() { return _msg.getDestination(); }
-        /** what is the target's current leaseSet (or null if we don't know yet) */
-        public LeaseSet getLeaseSet() { return _leaseSet; }
-        public void setLeaseSet(LeaseSet ls) { _leaseSet = ls; }
-        /** have we already sent the message down this tunnel? */
-        public boolean alreadySent(Hash gateway, TunnelId tunnelId) {
-            Tunnel t = new Tunnel(gateway, tunnelId);
-            synchronized (_sent) {
-                return _sent.contains(t);
-            }
-        }
-        public void sent(Hash gateway, TunnelId tunnelId) {
-            Tunnel t = new Tunnel(gateway, tunnelId);
-            synchronized (_sent) {
-                _sent.add(t);
-            }
-        }
-        /** how many messages have we sent through various leases? */
-        public int getNumSent() {
-            synchronized (_sent) {
-                return _sent.size() + _previousSent;
-            }
-        }
-        /** did we totally fail? */
-        public boolean getFailure() { return _failure; }
-        /** we failed.  returns true if we had already failed before */
-        public boolean failed() {
-            boolean already = _failure;
-            _failure = true;
-            return already;
-        }
-        /** have we totally succeeded? */
-        public boolean getSuccess() { return _success; }
-        /** we succeeded.  returns true if we had already succeeded before */
-        public boolean success() {
-            boolean already = _success;
-            _success = true;
-            return already;
-        }
-        
-        /** represent a unique tunnel at any given time */
-        private class Tunnel {
-            private Hash _gateway;
-            private TunnelId _tunnel;
-            
-            public Tunnel(Hash tunnelGateway, TunnelId tunnel) {
-                _gateway = tunnelGateway;
-                _tunnel = tunnel;
-            }
-            
-            public Hash getGateway() { return _gateway; }
-            public TunnelId getTunnel() { return _tunnel; }
-            
-            public int hashCode() {
-                int rv = 0;
-                if (_gateway != null)
-                    rv += _gateway.hashCode();
-                if (_tunnel != null)
-                    rv += 7*_tunnel.getTunnelId();
-                return rv;
-            }
-            
-            public boolean equals(Object o) {
-                if (o == null) return false;
-                if (o.getClass() != Tunnel.class) return false;
-                Tunnel t = (Tunnel)o;
-                return (getTunnel() == t.getTunnel()) &&
-                getGateway().equals(t.getGateway());
-            }
-        }
-    }
-    
     /**
      * Keep an eye out for any of the delivery status message tokens that have been
      * sent down the various tunnels to deliver this message
@@ -712,6 +597,14 @@ public class OutboundClientMessageJob extends JobImpl {
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug(OutboundClientMessageJob.this.getJobId()
                            + ": Soft timeout through the lease " + _lease);
+            
+            long messageDelay = getContext().throttle().getMessageDelay();
+            long tunnelLag = getContext().throttle().getTunnelLag();
+            long inboundDelta = (long)getContext().throttle().getInboundRateDelta();
+            getContext().statManager().addRateData("client.timeoutCongestionTunnel", tunnelLag, 1);
+            getContext().statManager().addRateData("client.timeoutCongestionMessage", messageDelay, 1);
+            getContext().statManager().addRateData("client.timeoutCongestionInbound", inboundDelta, 1);
+            
             _lease.setNumFailure(_lease.getNumFailure()+1);
             sendNext();
         }
diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageStatus.java b/router/java/src/net/i2p/router/message/OutboundClientMessageStatus.java
new file mode 100644
index 0000000000000000000000000000000000000000..f309dca7d5e89e6b76ee4f97d885a7ede9ae8423
--- /dev/null
+++ b/router/java/src/net/i2p/router/message/OutboundClientMessageStatus.java
@@ -0,0 +1,133 @@
+package net.i2p.router.message;
+
+import java.util.HashSet;
+import java.util.Set;
+import net.i2p.data.Destination;
+import net.i2p.data.Hash;
+import net.i2p.data.LeaseSet;
+import net.i2p.data.Payload;
+import net.i2p.data.TunnelId;
+import net.i2p.router.ClientMessage;
+import net.i2p.router.RouterContext;
+
+/**
+ * Good ol' fashioned struct with the send status
+ *
+ */
+class OutboundClientMessageStatus {
+    private RouterContext _context;
+    private ClientMessage _msg;
+    private PayloadGarlicConfig _clove;
+    private LeaseSet _leaseSet;
+    private Set _sent;
+    private int _numLookups;
+    private boolean _success;
+    private boolean _failure;
+    private long _start;
+    private int _previousSent;
+
+    public OutboundClientMessageStatus(RouterContext ctx, ClientMessage msg) {
+        _context = ctx;
+        _msg = msg;
+        _clove = null;
+        _leaseSet = null;
+        _sent = new HashSet(4);
+        _success = false;
+        _failure = false;
+        _numLookups = 0;
+        _previousSent = 0;
+        _start = ctx.clock().now();
+    }
+
+    /** raw payload */
+    public Payload getPayload() { return _msg.getPayload(); }
+    /** clove, if we've built it */
+    public PayloadGarlicConfig getClove() { return _clove; }
+    public void setClove(PayloadGarlicConfig clove) { _clove = clove; }
+    public ClientMessage getMessage() { return _msg; }
+    /** date we started the process on */
+    public long getStart() { return _start; }
+
+    public int getNumLookups() { return _numLookups; }
+    public void incrementLookups() { _numLookups++; }
+    public void clearAlreadySent() {
+        synchronized (_sent) {
+            _previousSent += _sent.size();
+            _sent.clear();
+        }
+    }
+
+    /** who sent the message? */
+    public Destination getFrom() { return _msg.getFromDestination(); }
+    /** who is the message going to? */
+    public Destination getTo() { return _msg.getDestination(); }
+    /** what is the target's current leaseSet (or null if we don't know yet) */
+    public LeaseSet getLeaseSet() { return _leaseSet; }
+    public void setLeaseSet(LeaseSet ls) { _leaseSet = ls; }
+    /** have we already sent the message down this tunnel? */
+    public boolean alreadySent(Hash gateway, TunnelId tunnelId) {
+        Tunnel t = new Tunnel(gateway, tunnelId);
+        synchronized (_sent) {
+            return _sent.contains(t);
+        }
+    }
+    public void sent(Hash gateway, TunnelId tunnelId) {
+        Tunnel t = new Tunnel(gateway, tunnelId);
+        synchronized (_sent) {
+            _sent.add(t);
+        }
+    }
+    /** how many messages have we sent through various leases? */
+    public int getNumSent() {
+        synchronized (_sent) {
+            return _sent.size() + _previousSent;
+        }
+    }
+    /** did we totally fail? */
+    public boolean getFailure() { return _failure; }
+    /** we failed.  returns true if we had already failed before */
+    public boolean failed() {
+        boolean already = _failure;
+        _failure = true;
+        return already;
+    }
+    /** have we totally succeeded? */
+    public boolean getSuccess() { return _success; }
+    /** we succeeded.  returns true if we had already succeeded before */
+    public boolean success() {
+        boolean already = _success;
+        _success = true;
+        return already;
+    }
+
+    /** represent a unique tunnel at any given time */
+    private class Tunnel {
+        private Hash _gateway;
+        private TunnelId _tunnel;
+
+        public Tunnel(Hash tunnelGateway, TunnelId tunnel) {
+            _gateway = tunnelGateway;
+            _tunnel = tunnel;
+        }
+
+        public Hash getGateway() { return _gateway; }
+        public TunnelId getTunnel() { return _tunnel; }
+
+        public int hashCode() {
+            int rv = 0;
+            if (_gateway != null)
+                rv += _gateway.hashCode();
+            if (_tunnel != null)
+                rv += 7*_tunnel.getTunnelId();
+            return rv;
+        }
+
+        public boolean equals(Object o) {
+            if (o == null) return false;
+            if (o.getClass() != Tunnel.class) return false;
+            Tunnel t = (Tunnel)o;
+            return (getTunnel() == t.getTunnel()) &&
+            getGateway().equals(t.getGateway());
+        }
+    }
+}
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
index 589cd1a2e6c0955ac0c91df56468ccae94fa7dab..52357e2ea49548e369376b5113cd6232acb3b2ed 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
@@ -11,7 +11,7 @@ package net.i2p.router.networkdb.kademlia;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
@@ -810,17 +810,17 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
         return routers;
     }
     
-    public void renderStatusHTML(OutputStream out) throws IOException {
+    public void renderStatusHTML(Writer out) throws IOException {
         StringBuffer buf = new StringBuffer(10*1024);
         buf.append("<h2>Kademlia Network DB Contents</h2>\n");
         if (!_initialized) {
             buf.append("<i>Not initialized</i>\n");
-            out.write(buf.toString().getBytes());
+            out.write(buf.toString());
             return;
         }
         Set leases = getLeases();
         buf.append("<h3>Leases</h3>\n");
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
         buf.setLength(0);
         long now = _context.clock().now();
         for (Iterator iter = leases.iterator(); iter.hasNext(); ) {
@@ -838,17 +838,17 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
                 buf.append("</i> tunnelId <i>").append(ls.getLease(i).getTunnelId().getTunnelId()).append("</i><br />\n");
             }
             buf.append("<hr />\n");
-            out.write(buf.toString().getBytes());
+            out.write(buf.toString());
             buf.setLength(0);
         }
         
         Hash us = _context.routerHash();
         Set routers = getRouters();
-        out.write("<h3>Routers</h3>\n".getBytes());
+        out.write("<h3>Routers</h3>\n");
         
         RouterInfo ourInfo = _context.router().getRouterInfo();
         renderRouterInfo(buf, ourInfo, true);
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
         buf.setLength(0);
         
         /* coreVersion to Map of routerVersion to Integer */
@@ -860,7 +860,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
             boolean isUs = key.equals(us);
             if (!isUs) {
                 renderRouterInfo(buf, ri, false);
-                out.write(buf.toString().getBytes());
+                out.write(buf.toString());
                 buf.setLength(0);
                 String coreVersion = ri.getOptions().getProperty("coreVersion");
                 String routerVersion = ri.getOptions().getProperty("router.version");
@@ -895,7 +895,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
             }
             buf.append("</table>\n");
         }
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
     }
     
     private void renderRouterInfo(StringBuffer buf, RouterInfo info, boolean isUs) {
diff --git a/router/java/src/net/i2p/router/peermanager/PeerManager.java b/router/java/src/net/i2p/router/peermanager/PeerManager.java
index 9dd2a4ac5f4a9fb30270662418601acc3bf029e9..2506abe720b6ef4588241af85df86751b3356d32 100644
--- a/router/java/src/net/i2p/router/peermanager/PeerManager.java
+++ b/router/java/src/net/i2p/router/peermanager/PeerManager.java
@@ -9,7 +9,7 @@ package net.i2p.router.peermanager;
  */
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -116,7 +116,7 @@ class PeerManager {
         return new ArrayList(peers);
     }
     
-    public void renderStatusHTML(OutputStream out) throws IOException { 
+    public void renderStatusHTML(Writer out) throws IOException { 
         _organizer.renderStatusHTML(out); 
     }
 }
diff --git a/router/java/src/net/i2p/router/peermanager/PeerManagerFacadeImpl.java b/router/java/src/net/i2p/router/peermanager/PeerManagerFacadeImpl.java
index a8bbee49193f1c85bb7c3f3bfd4d83007630a8f2..13b0adf1ea9017ba7e301d3e1a91bf6c06b2cb7c 100644
--- a/router/java/src/net/i2p/router/peermanager/PeerManagerFacadeImpl.java
+++ b/router/java/src/net/i2p/router/peermanager/PeerManagerFacadeImpl.java
@@ -9,7 +9,7 @@ package net.i2p.router.peermanager;
  */
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -59,7 +59,7 @@ public class PeerManagerFacadeImpl implements PeerManagerFacade {
         return _manager.selectPeers(criteria);
     }
     
-    public void renderStatusHTML(OutputStream out) throws IOException { 
+    public void renderStatusHTML(Writer out) throws IOException { 
         _manager.renderStatusHTML(out); 
     }
 }
diff --git a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
index 05eecda1c24bb6b5350a222f0223ea9e50fd7964..30ba0dfacdd486c62155305416474eb3355c1f45 100644
--- a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
+++ b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
@@ -2,6 +2,7 @@ package net.i2p.router.peermanager;
 
 import java.io.IOException;
 import java.io.OutputStream;
+import java.io.Writer;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
 import java.util.ArrayList;
@@ -203,7 +204,7 @@ public class ProfileOrganizer {
             _persistenceHelper.writeProfile(prof, out);
     }
     
-    public void renderStatusHTML(OutputStream out) throws IOException {
+    public void renderStatusHTML(Writer out) throws IOException {
         ProfileOrganizerRenderer rend = new ProfileOrganizerRenderer(this, _context);
         rend.renderStatusHTML(out);
     }
diff --git a/router/java/src/net/i2p/router/peermanager/ProfileOrganizerRenderer.java b/router/java/src/net/i2p/router/peermanager/ProfileOrganizerRenderer.java
index 3d929afd8e274c1c2f02c476d3dd6734081fd80c..909d183d419277824c31a433c2c6c071fe2faa0b 100644
--- a/router/java/src/net/i2p/router/peermanager/ProfileOrganizerRenderer.java
+++ b/router/java/src/net/i2p/router/peermanager/ProfileOrganizerRenderer.java
@@ -1,7 +1,7 @@
 package net.i2p.router.peermanager;
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
@@ -27,7 +27,7 @@ class ProfileOrganizerRenderer {
         _context = context;
         _organizer = organizer;
     }
-    public void renderStatusHTML(OutputStream out) throws IOException {
+    public void renderStatusHTML(Writer out) throws IOException {
         Set peers = _organizer.selectAllPeers();
         
         long hideBefore = _context.clock().now() - 3*60*60*1000;
@@ -126,7 +126,7 @@ class ProfileOrganizerRenderer {
         buf.append("<b>Speed:</b> ").append(num(_organizer.getSpeedThreshold())).append(" (").append(fast).append(" fast peers)<br />");
         buf.append("<b>Capacity:</b> ").append(num(_organizer.getCapacityThreshold())).append(" (").append(reliable).append(" high capacity peers)<br />");
         buf.append("<b>Integration:</b> ").append(num(_organizer.getIntegrationThreshold())).append(" (").append(integrated).append(" well integrated peers)<br />");
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
     }
     
     private final static DecimalFormat _fmt = new DecimalFormat("###,##0.00", new DecimalFormatSymbols(Locale.UK));
diff --git a/router/java/src/net/i2p/router/transport/FIFOBandwidthLimiter.java b/router/java/src/net/i2p/router/transport/FIFOBandwidthLimiter.java
index dc0f33406f1ecbc8a5d0e33674b83af421936f16..17d0942ecc4edc7c02a428b40edc432eeaeb84d0 100644
--- a/router/java/src/net/i2p/router/transport/FIFOBandwidthLimiter.java
+++ b/router/java/src/net/i2p/router/transport/FIFOBandwidthLimiter.java
@@ -1,7 +1,7 @@
 package net.i2p.router.transport;
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -407,7 +407,7 @@ public class FIFOBandwidthLimiter {
         return satisfied;
     }
     
-    public void renderStatusHTML(OutputStream out) throws IOException {
+    public void renderStatusHTML(Writer out) throws IOException {
         long now = _context.clock().now();
         StringBuffer buf = new StringBuffer(4096);
         buf.append("<br /><b>Pending bandwidth requests (with ");
@@ -436,7 +436,7 @@ public class FIFOBandwidthLimiter {
             }
         }
         buf.append("</ol></li></ul>\n");
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
     }
     
     private static long __requestId = 0;
diff --git a/router/java/src/net/i2p/router/transport/OutboundMessageRegistry.java b/router/java/src/net/i2p/router/transport/OutboundMessageRegistry.java
index 7fe5462d6f0ef2d9d0a019e5e644bbb9da8a2f96..ccada8769132f737cb81040b177bec9cf50ead52 100644
--- a/router/java/src/net/i2p/router/transport/OutboundMessageRegistry.java
+++ b/router/java/src/net/i2p/router/transport/OutboundMessageRegistry.java
@@ -9,7 +9,7 @@ package net.i2p.router.transport;
  */
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.Iterator;
@@ -275,7 +275,7 @@ public class OutboundMessageRegistry {
         }
     }
     
-    public void renderStatusHTML(OutputStream out) throws IOException {
+    public void renderStatusHTML(Writer out) throws IOException {
         StringBuffer buf = new StringBuffer(8192);
         buf.append("<h2>Pending messages</h2>\n");
         Map msgs = null;
@@ -295,7 +295,7 @@ public class OutboundMessageRegistry {
             buf.append("</li>\n");
         }
         buf.append("</ul>");
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
     }
 
     /**
diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java
index 35beef902a4388472ced944c58c93300ca6b862e..be97c2eef5463d7802d16ef355108e4e7a50a806 100644
--- a/router/java/src/net/i2p/router/transport/TransportManager.java
+++ b/router/java/src/net/i2p/router/transport/TransportManager.java
@@ -9,7 +9,7 @@ package net.i2p.router.transport;
  */
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -265,7 +265,7 @@ public class TransportManager implements TransportEventListener {
         return rv;
     }
     
-    public void renderStatusHTML(OutputStream out) throws IOException {
+    public void renderStatusHTML(Writer out) throws IOException {
         StringBuffer buf = new StringBuffer(8*1024);
         buf.append("<h2>Transport Manager</h2>\n");
         buf.append("Listening on: <br /><pre>\n");
@@ -283,6 +283,6 @@ public class TransportManager implements TransportEventListener {
             if (str != null)
                 buf.append(str);
         }
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
     }
 }
diff --git a/router/java/src/net/i2p/router/tunnelmanager/PoolingTunnelManagerFacade.java b/router/java/src/net/i2p/router/tunnelmanager/PoolingTunnelManagerFacade.java
index b7cfbc15107ae03cff776265b16c73d25961c37b..fae3e83d83f00f832c17fc015fc3fe30b063506c 100644
--- a/router/java/src/net/i2p/router/tunnelmanager/PoolingTunnelManagerFacade.java
+++ b/router/java/src/net/i2p/router/tunnelmanager/PoolingTunnelManagerFacade.java
@@ -1,7 +1,7 @@
 package net.i2p.router.tunnelmanager;
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.List;
@@ -229,7 +229,7 @@ public class PoolingTunnelManagerFacade implements TunnelManagerFacade {
      * Aint she pretty?
      *
      */
-    public void renderStatusHTML(OutputStream out) throws IOException {
+    public void renderStatusHTML(Writer out) throws IOException {
         if (_pool != null)
             _pool.renderStatusHTML(out);
     }
diff --git a/router/java/src/net/i2p/router/tunnelmanager/TunnelPool.java b/router/java/src/net/i2p/router/tunnelmanager/TunnelPool.java
index d7f7da1063e79b37035a61fbe19ac94fdaf9fd30..e087f641730cb44611812c74cc1e68bbb181685d 100644
--- a/router/java/src/net/i2p/router/tunnelmanager/TunnelPool.java
+++ b/router/java/src/net/i2p/router/tunnelmanager/TunnelPool.java
@@ -1,7 +1,7 @@
 package net.i2p.router.tunnelmanager;
 
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.Writer;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -66,7 +66,8 @@ class TunnelPool {
         _context.statManager().createRateStat("tunnel.inboundMessagesProcessed", "How many messages does an inbound tunnel process in its lifetime?", "Tunnels", new long[] { 10*60*1000l, 60*60*1000l, 24*60*60*1000l });
         _context.statManager().createRateStat("tunnel.outboundMessagesProcessed", "How many messages does an inbound tunnel process in its lifetime?", "Tunnels", new long[] { 10*60*1000l, 60*60*1000l, 24*60*60*1000l });
         _context.statManager().createRateStat("tunnel.participatingMessagesProcessed", "How many messages does an inbound tunnel process in its lifetime?", "Tunnels", new long[] { 10*60*1000l, 60*60*1000l, 24*60*60*1000l });
-
+        _context.statManager().createRateStat("tunnel.participatingMessagesProcessedActive", "How many messages beyond the average were processed in a more-than-average tunnel's lifetime?", "Tunnels", new long[] { 10*60*1000l, 60*60*1000l, 24*60*60*1000l });
+        
         _isLive = true;
         _persistenceHelper = new TunnelPoolPersistenceHelper(_context);
         _tunnelBuilder = new TunnelBuilder(_context);
@@ -603,8 +604,8 @@ class TunnelPool {
     
     public void shutdown() {
         if (_log.shouldLog(Log.INFO)) _log.info("Shutting down tunnel pool");
-        if (_persistenceHelper != null)
-            _persistenceHelper.writePool(this);
+        //if (_persistenceHelper != null)
+        //    _persistenceHelper.writePool(this);
         _isLive = false; // the subjobs [should] check getIsLive() on each run 
         _outboundTunnels = null;
         _freeInboundTunnels = null;
@@ -633,10 +634,17 @@ class TunnelPool {
                                                        info.getSettings().getCreated());
                     break;
                 case TunnelId.TYPE_PARTICIPANT:
+                    long numMsgs = info.getMessagesProcessed();
+                    long lastAvg = (long)_context.statManager().getRate("tunnel.participatingMessagesProcessed").getRate(10*60*1000l).getAverageValue();
                     _context.statManager().addRateData("tunnel.participatingMessagesProcessed", 
-                                                       info.getMessagesProcessed(), 
+                                                       numMsgs, 
                                                        info.getSettings().getExpiration() -
                                                        info.getSettings().getCreated());
+                    if (numMsgs > lastAvg)
+                        _context.statManager().addRateData("tunnel.participatingMessagesProcessedActive", 
+                                                           numMsgs-lastAvg, 
+                                                           info.getSettings().getExpiration() -
+                                                           info.getSettings().getCreated());
                     break;
                 case TunnelId.TYPE_UNSPECIFIED:
                 default:
@@ -651,9 +659,9 @@ class TunnelPool {
         return settings;
     }
  
-    public void renderStatusHTML(OutputStream out) throws IOException {
+    public void renderStatusHTML(Writer out) throws IOException {
         if (!_isLive) return;
-        out.write("<h2>Tunnel Pool</h2>\n".getBytes());
+        out.write("<h2>Tunnel Pool</h2>\n");
         StringBuffer buf = new StringBuffer(4096);
         renderTunnels(out, buf, "Free inbound tunnels", getFreeTunnels());
         renderTunnels(out, buf, "Outbound tunnels", getOutboundTunnels());
@@ -665,19 +673,19 @@ class TunnelPool {
         }
     }
     
-    private void renderTunnels(OutputStream out, StringBuffer buf, String msg, Set tunnelIds) throws IOException {
+    private void renderTunnels(Writer out, StringBuffer buf, String msg, Set tunnelIds) throws IOException {
         buf.append("<b>").append(msg).append(":</b> <i>(").append(tunnelIds.size()).append(" tunnels)</i><ul>\n");
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
         buf.setLength(0);
         for (Iterator iter = tunnelIds.iterator(); iter.hasNext(); ) {
             TunnelId id = (TunnelId)iter.next();
             TunnelInfo tunnel = getTunnelInfo(id);
             renderTunnel(out, buf, id, tunnel);
         }
-        out.write("</ul>\n".getBytes());
+        out.write("</ul>\n");
     }
     
-    private final void renderTunnel(OutputStream out, StringBuffer buf, TunnelId id, TunnelInfo tunnel) throws IOException {
+    private final void renderTunnel(Writer out, StringBuffer buf, TunnelId id, TunnelInfo tunnel) throws IOException {
         buf.setLength(0);
         if (tunnel == null) {
             buf.append("<li>Tunnel: ").append(id.getTunnelId()).append(" is not known</li>\n");
@@ -713,7 +721,7 @@ class TunnelPool {
 
             buf.append("\n</pre>");
         }
-        out.write(buf.toString().getBytes());
+        out.write(buf.toString());
         buf.setLength(0);
     }