diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
index 9c6439264d7c0a113615e25721176c2d8c9a3709..d36ac58f1233cbba38f1ba2cac9057be222ca924 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
@@ -225,7 +225,7 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
                         }
                         if (!usingInternalOutproxy) {
                             // The request must be forwarded to a outproxy
-                            currentProxy = selectProxy();
+                            currentProxy = selectProxy(hostLowerCase);
                             if (currentProxy == null) {
                                 if (_log.shouldLog(Log.WARN))
                                     _log.warn(getPrefix(requestId) + "Host wants to be outproxied, but we dont have any!");
@@ -347,8 +347,13 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
                 data = newRequest.toString().getBytes("ISO-8859-1");
             else
                 response = SUCCESS_RESPONSE.getBytes("UTF-8");
-            OnTimeout onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, currentProxy, requestId);
-            Thread t = new I2PTunnelRunner(s, i2ps, sockLock, data, response, mySockets, onTimeout);
+            OnTimeout onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy,
+                                                currentProxy, requestId, targetRequest, false);
+            I2PTunnelRunner t = new I2PTunnelRunner(s, i2ps, sockLock, data, response, mySockets, onTimeout);
+            if (usingWWWProxy) {
+                // isSSL must be false for ConnectClient
+                t.setSuccessCallback(new OnProxySuccess(currentProxy, host, false));
+            }
             // we are called from an unlimited thread pool, so run inline
             //t.start();
             t.run();
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
index ea18fc8f0cbc50f56ccb61fb3ec0ca8cb6c5eafa..a2603c24f663218aeac060ed852e2f0aa5435b33 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
@@ -367,8 +367,6 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
     public static final String PROP_VIA = "i2ptunnel.httpclient.sendVia";
     public static final String PROP_JUMP_SERVERS = "i2ptunnel.httpclient.jumpServers";
     public static final String PROP_DISABLE_HELPER = "i2ptunnel.httpclient.disableAddressHelper";
-    /** @since 0.9.11 */
-    public static final String PROP_SSL_OUTPROXIES = "i2ptunnel.httpclient.SSLOutproxies";
     /** @since 0.9.14 */
     public static final String PROP_ACCEPT = "i2ptunnel.httpclient.sendAccept";
     /** @since 0.9.14, overridden to true as of 0.9.35 unlesss PROP_SSL_SET is set */
@@ -406,6 +404,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
             out = s.getOutputStream();
             InputReader reader = new InputReader(s.getInputStream());
             String line, method = null, protocol = null, host = null, destination = null;
+            String hostLowerCase = null;
             StringBuilder newRequest = new StringBuilder();
             boolean ahelperPresent = false;
             boolean ahelperNew = false;
@@ -581,7 +580,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
                     // in our addressbook (all naming is local),
                     // and it is removed from the request line.
 
-                    String hostLowerCase = host.toLowerCase(Locale.US);
+                    hostLowerCase = host.toLowerCase(Locale.US);
                     if(hostLowerCase.equals(LOCAL_SERVER)) {
                         // so we don't do any naming service lookups
                         destination = host;
@@ -869,9 +868,9 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
                             }
                             if ("https".equals(protocol) ||
                                 method.toUpperCase(Locale.US).equals("CONNECT"))
-                                currentProxy = selectSSLProxy();
+                                currentProxy = selectSSLProxy(hostLowerCase);
                             else
-                                currentProxy = selectProxy();
+                                currentProxy = selectProxy(hostLowerCase);
                             if(_log.shouldLog(Log.DEBUG)) {
                                 _log.debug("After selecting outproxy for " + host + ": " + currentProxy);
                             }
@@ -945,8 +944,8 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
                         // Note that we only pass the original Host: line through to the outproxy
                         // But we don't create a Host: line if it wasn't sent to us
                         line = "Host: " + host;
-                        if(_log.shouldLog(Log.INFO)) {
-                            _log.info(getPrefix(requestId) + "Setting host = " + host);
+                        if (_log.shouldDebug()) {
+                            _log.debug(getPrefix(requestId) + "Setting host = " + host);
                         }
                     } else if(lowercaseLine.startsWith("user-agent: ")) {
                         // save for deciding whether to offer address book form
@@ -1306,9 +1305,11 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
             if (remotePort > 0)
                 sktOpts.setPort(remotePort);
             i2ps = createI2PSocket(clientDest, sktOpts);
-            OnTimeout onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, currentProxy, requestId);
-            Thread t;
-            if (method.toUpperCase(Locale.US).equals("CONNECT")) {
+            boolean isConnect = method.toUpperCase(Locale.US).equals("CONNECT");
+            OnTimeout onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy,
+                                                currentProxy, requestId, hostLowerCase, isConnect);
+            I2PTunnelRunner t;
+            if (isConnect) {
                 byte[] data;
                 byte[] response;
                 if (usingWWWProxy) {
@@ -1323,6 +1324,9 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
                 byte[] data = newRequest.toString().getBytes("ISO-8859-1");
                 t = new I2PTunnelHTTPClientRunner(s, i2ps, sockLock, data, mySockets, onTimeout);
             }
+            if (usingWWWProxy) {
+                t.setSuccessCallback(new OnProxySuccess(currentProxy, hostLowerCase, isConnect));
+            }
             // we are called from an unlimited thread pool, so run inline
             //t.start();
             t.run();
@@ -1347,26 +1351,6 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
         }
     }
 
-    /**
-     *  Unlike selectProxy(), we parse the option on the fly so it
-     *  can be changed. selectProxy() requires restart...
-     *  @return null if none
-     *  @since 0.9.11
-     */
-    private String selectSSLProxy() {
-        String s = getTunnel().getClientOptions().getProperty(PROP_SSL_OUTPROXIES);
-        if (s == null)
-            return null;
-        String[] p = DataHelper.split(s, "[,; \r\n\t]");
-        if (p.length == 0)
-            return null;
-        // todo doesn't check for ""
-        if (p.length == 1)
-            return p[0];
-        int i = _context.random().nextInt(p.length);
-        return p[i];
-    }
-
     /** @since 0.8.7 */
     private void writeHelperSaveForm(OutputStream outs, String destination, String ahelperKey,
                                      String targetRequest, String referer) throws IOException {
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
index 3e64286aed9b9d8f8794724afa366b8ad1e324ae..744abde1b4c61d2b979e26029a1184a1997260dd 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
@@ -18,6 +18,7 @@ import java.net.Socket;
 import java.net.SocketTimeoutException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.BitSet;
 import java.util.Date;
@@ -42,6 +43,7 @@ import net.i2p.data.i2cp.MessageStatusMessage;
 import net.i2p.util.EepGet;
 import net.i2p.util.EventDispatcher;
 import net.i2p.util.InternalSocket;
+import net.i2p.util.LHMCache;
 import net.i2p.util.Log;
 import net.i2p.util.PasswordManager;
 import net.i2p.util.PortMapper;
@@ -64,6 +66,9 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
     private static final int MAX_NONCE_COUNT = 1024;
     /** @since 0.9.11, moved to Base in 0.9.29 */
     public static final String PROP_USE_OUTPROXY_PLUGIN = "i2ptunnel.useLocalOutproxy";
+    /** @since 0.9.11, moved to Base in 0.9.39 */
+    public static final String PROP_SSL_OUTPROXIES = "i2ptunnel.httpclient.SSLOutproxies";
+
     /**
      *  This is a standard soTimeout, not a total timeout.
      *  We have no slowloris protection on the client side.
@@ -122,19 +127,142 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
     private final byte[] _proxyNonce;
     private final ConcurrentHashMap<String, NonceInfo> _nonces;
     private final AtomicInteger _nonceCleanCounter = new AtomicInteger();
+    // clearnet host to proxy
+    private final Map<String, String> _proxyCache = new LHMCache<String, String>(32);
+    // very simple, remember last-failed only
+    private String _lastFailedProxy;
+    // clearnet host to proxy
+    private final Map<String, String> _proxySSLCache = new LHMCache<String, String>(32);
+    // very simple, remember last-failed only
+    private String _lastFailedSSLProxy;
 
     protected String getPrefix(long requestId) {
         return "HTTPClient[" + _clientId + '/' + requestId + "]: ";
     }
+
+    // TODO standard proxy config changes require tunnel restart;
+    // SSL proxy config is parsed on the fly;
+    // allow both to be changed and store the SSL proxy list.
+    // TODO should track more than one failed proxy
     
-    protected String selectProxy() {
+    /**
+     *  Simple random selection, with caching by hostname,
+     *  and avoidance of the last one to fail.
+     *
+     *  @param host the clearnet hostname we're targeting
+     *  @return null if none configured
+     */
+    protected String selectProxy(String host) {
+        String rv;
         synchronized (_proxyList) {
             int size = _proxyList.size();
             if (size <= 0)
                 return null;
-            int index = _context.random().nextInt(size);
-            return _proxyList.get(index);
+            if (size == 1)
+                return _proxyList.get(0);
+            rv = _proxyCache.get(host);
+            if (rv == null) {
+                List<String> tmpList;
+                if (_lastFailedProxy != null) {
+                    // don't use last failed one
+                    tmpList = new ArrayList<String>(_proxyList);
+                    tmpList.remove(_lastFailedProxy);
+                    size = tmpList.size();
+                } else {
+                    tmpList = _proxyList;
+                }
+                int index = _context.random().nextInt(size);
+                rv = tmpList.get(index);
+                _proxyCache.put(host, rv);
+            }
+        }
+        if (_log.shouldInfo())
+            _log.info("Selected proxy for " + host + ": " + rv);
+        return rv;
+    }
+
+    /**
+     *  Only for SSL via HTTPClient. ConnectClient should use selectProxy()
+     *
+     *  Unlike selectProxy(), we parse the option on the fly so it
+     *  can be changed. selectProxy() requires restart...
+     *
+     *  @return null if none configured
+     *  @since 0.9.11, moved from I2PTunnelHTTPClient in 0.9.39
+     */
+    protected String selectSSLProxy(String host) {
+        String s = getTunnel().getClientOptions().getProperty(PROP_SSL_OUTPROXIES);
+        if (s == null)
+            return null;
+        String[] p = DataHelper.split(s, "[,; \r\n\t]");
+        int size = p.length;
+        if (size == 0)
+            return null;
+        // todo doesn't check for ""
+        if (size == 1)
+            return p[0];
+        String rv;
+        synchronized (_proxySSLCache) {
+            rv = _proxySSLCache.get(host);
+            if (rv == null) {
+                List<String> tmpList;
+                if (_lastFailedSSLProxy != null) {
+                    // don't use last failed one
+                    tmpList = new ArrayList<String>(Arrays.asList(p));
+                    tmpList.remove(_lastFailedSSLProxy);
+                    size = tmpList.size();
+                } else {
+                    tmpList = Arrays.asList(p);
+                }
+                int index = _context.random().nextInt(size);
+                rv = tmpList.get(index);
+                _proxySSLCache.put(host, rv);
+            }
+        }
+        if (_log.shouldInfo())
+            _log.info("Selected SSL proxy for " + host + ": " + rv);
+        return rv;
+    }
+    
+    /**
+     *  Update the cache and note if failed.
+     *
+     *  @param proxy which
+     *  @param host clearnet hostname targeted
+     *  @param isSSL set to FALSE for ConnectClient
+     *  @param ok success or failure
+     *  @since 0.9.39
+     */
+    protected void noteProxyResult(String proxy, String host, boolean isSSL, boolean ok) {
+        if (isSSL) {
+            synchronized (_proxySSLCache) {
+                if (ok) {
+                    if (proxy.equals(_lastFailedSSLProxy))
+                        _lastFailedSSLProxy = null;
+                    _proxySSLCache.put(host, proxy);
+                } else {
+                    _lastFailedSSLProxy = proxy;
+                    if (proxy.equals(_proxySSLCache.get(host)))
+                        _proxySSLCache.remove(host);
+                }
+            }
+        } else {
+            synchronized (_proxyList) {
+                if (_proxyList.size() > 1) {
+                    if (ok) {
+                        if (proxy.equals(_lastFailedProxy))
+                            _lastFailedProxy = null;
+                        _proxyCache.put(host, proxy);
+                    } else {
+                        _lastFailedProxy = proxy;
+                        if (proxy.equals(_proxyCache.get(host)))
+                            _proxyCache.remove(host);
+                    }
+                }
+            }
         }
+        if (_log.shouldInfo())
+            _log.info("Proxy result: to " + host + " through " + proxy + " SSL? " + isSSL + " success? " + ok);
     }
 
     /**
@@ -610,7 +738,12 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
         private final boolean _usingProxy;
         private final String _wwwProxy;
         private final long _requestId;
+        private final String _targetHost;
+        private final boolean _isSSL;
 
+        /**
+         *  @param target the URI for an HTTP request, or the host name for CONNECT
+         */
         public OnTimeout(Socket s, OutputStream out, String target, boolean usingProxy, String wwwProxy, long id) {
             _socket = s;
             _out = out;
@@ -618,12 +751,35 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
             _usingProxy = usingProxy;
             _wwwProxy = wwwProxy;
             _requestId = id;
+            _targetHost = null;
+            _isSSL = false;
+        }
+
+        /**
+         *  @param target the URI for an HTTP request, or the host name for CONNECT
+         *  @param targetHost if non-null, call noteProxyResult() with this as host
+         *  @param isSSL to pass to noteProxyResult(). FALSE for ConnectClient.
+         *  @since 0.9.39
+         */
+        public OnTimeout(Socket s, OutputStream out, String target, boolean usingProxy,
+                         String wwwProxy, long id, String targetHost, boolean isSSL) {
+            _socket = s;
+            _out = out;
+            _target = target;
+            _usingProxy = usingProxy;
+            _wwwProxy = wwwProxy;
+            _requestId = id;
+            _targetHost = targetHost;
+            _isSSL = isSSL;
         }
 
         /**
          *  @param ex may be null
          */
         public void onFail(Exception ex) {
+            if (_usingProxy && _targetHost != null) {
+                noteProxyResult(_wwwProxy, _targetHost, _isSSL, false);
+            }
             Throwable cause = ex != null ? ex.getCause() : null;
             if (cause != null && cause instanceof I2PSocketException) {
                 I2PSocketException ise = (I2PSocketException) cause;
@@ -635,6 +791,23 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
         }
     }
 
+    /**
+     *  @since 0.9.39
+     */
+    protected class OnProxySuccess implements I2PTunnelRunner.SuccessCallback {
+        private final String _proxy, _host;
+        private final boolean _isSSL;
+
+        /** @param isSSL FALSE for ConnectClient */
+        public OnProxySuccess(String proxy, String host, boolean isSSL) {
+            _proxy = proxy; _host = host; _isSSL = isSSL;
+        }
+
+        public void onSuccess() {
+            noteProxyResult(_proxy, _host, _isSSL, true);
+        }
+    }
+
     /**
      *  @param ex may be null
      *  @since 0.9.14 moved from subclasses
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java
index fd422adeada31bf05f46d1adf266813cc84d049a..a6f81bf0dbd31fbb228f345ab89423f2a51ebe99 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java
@@ -60,6 +60,7 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr
     /** if we die before receiving any data, run this job */
     private final Runnable onTimeout;
     private final FailCallback _onFail;
+    private SuccessCallback _onSuccess;
     private long totalSent;
     private long totalReceived;
 
@@ -74,6 +75,16 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr
         public void onFail(Exception e);
     }
 
+    /**
+     *  @since 0.9.39
+     */
+    public interface SuccessCallback {
+        /**
+         *  @param e may be null
+         */
+        public void onSuccess();
+    }
+
     /**
      *  Starts itself
      *
@@ -228,6 +239,17 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr
         return startedOn;
     }
 
+    /**
+     * Will be called if we get any data back.
+     * This is called after the first byte of data is received, not on completion.
+     * Only one of SuccessCallback, onTimeout, or onFail will be called.
+     *
+     * @since 0.9.39
+     */
+    public void setSuccessCallback(SuccessCallback sc) {
+        _onSuccess = sc;
+    }
+
     protected InputStream getSocketIn() throws IOException { return s.getInputStream(); }
     protected OutputStream getSocketOut() throws IOException { return s.getOutputStream(); }
     
@@ -282,8 +304,8 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr
                            + " written to the socket, starting forwarders");
             if (!(s instanceof InternalSocket))
                 in = new BufferedInputStream(in, 2*NETWORK_BUFFER_SIZE);
-            toI2P = new StreamForwarder(in, i2pout, true);
-            fromI2P = new StreamForwarder(i2pin, out, false);
+            toI2P = new StreamForwarder(in, i2pout, true, null);
+            fromI2P = new StreamForwarder(i2pin, out, false, _onSuccess);
             toI2P.start();
             // We are already a thread, so run the second one inline
             //fromI2P.start();
@@ -294,13 +316,13 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr
                 }
             }
             if (_log.shouldLog(Log.DEBUG))
-                _log.debug("At least one forwarder completed, closing and joining");
+                _log.debug("Both forwarders completed, sent: " + totalSent + " received: " + totalReceived);
             
             // this task is useful for the httpclient
             if ((onTimeout != null || _onFail != null) && totalReceived <= 0) {
-                if (_log.shouldLog(Log.DEBUG))
-                    _log.debug("runner has a timeout job, totalReceived = " + totalReceived
-                               + " totalSent = " + totalSent + " job = " + onTimeout);
+                //if (_log.shouldLog(Log.DEBUG))
+                //     _log.debug("runner has a timeout job, totalReceived = " + totalReceived
+                //                + " totalSent = " + totalSent + " job = " + onTimeout);
                 // Run even if totalSent > 0, as that's probably POST data.
                 // This will be run even if initialSocketData != null, it's the timeout job's
                 // responsibility to know that and decide whether or not to write to the socket.
@@ -460,15 +482,18 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr
         private final String direction;
         private final boolean _toI2P;
         private final ByteCache _cache;
+        private final SuccessCallback _callback;
         private volatile Exception _failure;
 
         /**
          *  Does not start itself. Caller must start()
+         *  @param cb may be null, only used for toI2P == false
          */
-        public StreamForwarder(InputStream in, OutputStream out, boolean toI2P) {
+        public StreamForwarder(InputStream in, OutputStream out, boolean toI2P, SuccessCallback cb) {
             this.in = in;
             this.out = out;
             _toI2P = toI2P;
+            _callback = cb;
             direction = (toI2P ? "toI2P" : "fromI2P");
             _cache = ByteCache.getInstance(32, NETWORK_BUFFER_SIZE);
             setName("StreamForwarder " + _runnerId + '.' + direction);
@@ -495,10 +520,13 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr
                 while ((len = in.read(buffer)) != -1) {
                     if (len > 0) {
                         out.write(buffer, 0, len);
-                        if (_toI2P)
+                        if (_toI2P) {
                             totalSent += len;
-                        else
+                        } else {
+                            if (totalReceived == 0 && _callback != null)
+                                _callback.onSuccess();
                             totalReceived += len;
+                        }
                         //updateActivity();
                     }