diff --git a/history.txt b/history.txt
index 1e4e7de6068e23ed59720c3c1568ec5dd990af2c..3eaea4745d5431f949153c975aa7adfe458009f5 100644
--- a/history.txt
+++ b/history.txt
@@ -1,3 +1,9 @@
+2011-02-22 zzz
+    * BuildHandler: Prelmiinary participating tunnel throttler
+    * I2PTunnel:
+      - Add spellcheck=false to textareas
+      - Fix HTML error in 503 error page
+
 2011-02-19 zzz
     * I2PTunnel: Fix standalone server tunnels
                  http://forum.i2p/viewtopic.php?t=5376
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index 421ff4f630110e645f24b485d06a7151f306e1cd..098e16d499456339b4699536706ff45ebf0c4769 100644
--- a/router/java/src/net/i2p/router/RouterVersion.java
+++ b/router/java/src/net/i2p/router/RouterVersion.java
@@ -18,7 +18,7 @@ public class RouterVersion {
     /** deprecated */
     public final static String ID = "Monotone";
     public final static String VERSION = CoreVersion.VERSION;
-    public final static long BUILD = 16;
+    public final static long BUILD = 17;
 
     /** for example "-test" */
     public final static String EXTRA = "-rc";
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
index 7be46e0c73e555fe707c23c3178b775eb9b34e08..ca9c6ad635916c0211f3a52777466ebdf3267506 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
@@ -37,18 +37,19 @@ import net.i2p.util.Log;
  *
  */
 class BuildHandler {
-    private RouterContext _context;
-    private Log _log;
-    private BuildExecutor _exec;
-    private Job _buildMessageHandlerJob;
-    private Job _buildReplyMessageHandlerJob;
+    private final RouterContext _context;
+    private final Log _log;
+    private final BuildExecutor _exec;
+    private final Job _buildMessageHandlerJob;
+    private final Job _buildReplyMessageHandlerJob;
     /** list of BuildMessageState, oldest first */
     private final List<BuildMessageState> _inboundBuildMessages;
     /** list of BuildReplyMessageState, oldest first - unused unless HANDLE_REPLIES_INLINE == false */
     private final List<BuildReplyMessageState> _inboundBuildReplyMessages;
     /** list of BuildEndMessageState, oldest first - unused unless HANDLE_REPLIES_INLINE == false */
     private final List<BuildEndMessageState> _inboundBuildEndMessages;
-    private BuildMessageProcessor _processor;
+    private final BuildMessageProcessor _processor;
+    private final ParticipatingThrottler _throttler;
 
     private static final boolean HANDLE_REPLIES_INLINE = true;
     
@@ -101,6 +102,7 @@ class BuildHandler {
         ctx.inNetMessagePool().registerHandlerJobBuilder(TunnelBuildReplyMessage.MESSAGE_TYPE, tbrmhjb);
         ctx.inNetMessagePool().registerHandlerJobBuilder(VariableTunnelBuildMessage.MESSAGE_TYPE, tbmhjb);
         ctx.inNetMessagePool().registerHandlerJobBuilder(VariableTunnelBuildReplyMessage.MESSAGE_TYPE, tbrmhjb);
+        _throttler = new ParticipatingThrottler(ctx);
     }
     
     private static final int MAX_HANDLE_AT_ONCE = 2;
@@ -498,6 +500,7 @@ class BuildHandler {
         long nextId = req.readNextTunnelId();
         boolean isInGW = req.readIsInboundGateway();
         boolean isOutEnd = req.readIsOutboundEndpoint();
+
         // time is in hours, and only for log below - what's the point?
         // tunnel-alt-creation.html specifies that this is enforced +/- 1 hour but it is not.
         long time = req.readRequestTime();
@@ -531,7 +534,7 @@ class BuildHandler {
                 _context.statManager().addRateData("tunnel.acceptLoad", recvDelay, recvDelay);
             }
         }
-        
+
         /*
          * Being a IBGW or OBEP generally leads to more connections, so if we are
          * approaching our connection limit (i.e. !haveCapacity()),
@@ -550,6 +553,28 @@ class BuildHandler {
                 _context.throttle().setTunnelStatus(_x("Rejecting tunnels: Connection limit"));
                 response = TunnelHistory.TUNNEL_REJECT_BANDWIDTH;
         }
+        
+        // Check participating throttle counters for previous and next hops
+        // This is at the end as it compares to a percentage of created tunnels.
+        // We may need another counter above for requests.
+        if (response == 0 && !isInGW) {
+            Hash from = state.fromHash;
+            if (from == null)
+                from = state.from.calculateHash();
+            if (_throttler.shouldThrottle(from)) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Rejecting tunnel (hop throttle), previous hop: " + from);
+                // no setTunnelStatus() indication
+                response = TunnelHistory.TUNNEL_REJECT_BANDWIDTH;
+            }
+        }
+        if (response == 0 && (!isOutEnd) &&
+            _throttler.shouldThrottle(req.readNextIdentity())) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Rejecting tunnel (hop throttle), next hop: " + req.readNextIdentity());
+            // no setTunnelStatus() indication
+            response = TunnelHistory.TUNNEL_REJECT_BANDWIDTH;
+        }
 
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Responding to " + state.msg.getUniqueId() + "/" + ourId
diff --git a/router/java/src/net/i2p/router/tunnel/pool/ParticpatingThrottler.java b/router/java/src/net/i2p/router/tunnel/pool/ParticpatingThrottler.java
new file mode 100644
index 0000000000000000000000000000000000000000..06d6826793a30a1c7c8d6bdca643fe5acc4ded38
--- /dev/null
+++ b/router/java/src/net/i2p/router/tunnel/pool/ParticpatingThrottler.java
@@ -0,0 +1,57 @@
+package net.i2p.router.tunnel.pool;
+
+import net.i2p.data.Hash;
+import net.i2p.router.RouterContext;
+import net.i2p.util.ObjectCounter;
+import net.i2p.util.SimpleScheduler;
+import net.i2p.util.SimpleTimer;
+
+/**
+ * Count how often we have accepted a tunnel with the peer
+ * as the previous or next hop.
+ * We limit each peer to a percentage of all participating tunnels,
+ * subject to minimum and maximum values for the limit.
+ *
+ * This offers basic protection against simple attacks
+ * but is not a complete solution, as by design, we don't know
+ * the originator of a tunnel request.
+ *
+ * This also effectively limits the number of tunnels between
+ * any given pair of routers, which probably isn't a bad thing.
+ *
+ * Note that the counts are of previous + next hops, so the total will
+ * be higher than the participating tunnel count, and will also grow
+ * as the network uses more 3-hop tunnels.
+ *
+ * @since 0.8.4
+ */
+class ParticipatingThrottler {
+    private final RouterContext context;
+    private final ObjectCounter<Hash> counter;
+
+    /** portion of the tunnel lifetime */
+    private static final int LIFETIME_PORTION = 3;
+    private static final int MIN_LIMIT = 18 / LIFETIME_PORTION;
+    private static final int MAX_LIMIT = 81 / LIFETIME_PORTION;
+    private static final int PERCENT_LIMIT = 12 / LIFETIME_PORTION;
+    private static final long CLEAN_TIME = 10*60*1000 / LIFETIME_PORTION;
+
+    ParticipatingThrottler(RouterContext ctx) {
+        this.context = ctx;
+        this.counter = new ObjectCounter();
+        SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME);
+    }
+
+    /** increments before checking */
+    boolean shouldThrottle(Hash h) {
+        int numTunnels = this.context.tunnelManager().getParticipatingCount();
+        int limit = Math.max(MIN_LIMIT, Math.min(MAX_LIMIT, numTunnels * PERCENT_LIMIT / 100));
+        return this.counter.increment(h) > limit;
+    }
+
+    private class Cleaner implements SimpleTimer.TimedEvent {
+        public void timeReached() {
+            ParticipatingThrottler.this.counter.clear();
+        }
+    }
+}